Tracing Transactions
This guide will cover the following topics:
basic EVM tracing with JS
filtered EVM tracing with JS
JSON-RPC
debug_trace*
endpoints
Basic EVM Tracing with JS
Tracing a transaction means requesting an BlockX node to re-execute the desired transaction with varying degrees of data collection.
Re-executing a transaction has a few prerequisites to be met. All historical state accessed by the transaction must be available, including:
Balance, nonce, bytecode, and storage of both the recipient as well as all internally invoked contracts
Block metadata referenced during execution of the outer as well as all internally created transactions
Intermediate state generated by all preceding transactions contained in the same block as well as the one being traced
This means there are limits on the transactions that can be traced and imported based on the synchronization and pruning configuration of a node.
Archive nodes: retain all historical data back to genesis, can trace arbitrary transactions at any point in the history of the chain.
Fully synced nodes: transactions within a recent range (depending on how much history is stored) are accessible.
Light synced nodes: these nodes retrieve data on demand, so in theory they can trace transactions for which all required historical state is readily available in the network (however, data availability cannot be reasonably assumed).
Basic Traces
The simplest type of transaction trace that Geth can generate are raw EVM opcode traces. For every VM instruction the transaction executes, a structured log entry is emitted, contained all contextual metadata deemed useful. This includes:
program counter
opcode name & cost
remaining gas
execution depth
occurred errors
as well as (optionally) the execution stack, execution memory, and contract storage.
The entire output of a raw EVM opcode trace is a JSON object having a few metadata fields: consumed gas, failure status, return value, and a list of opcode entries:
An example log for a single opcode entry has the following format:
Limits of Basic Traces
Although raw opcode traces generated above are useful, having an individual log entry for every single opcode is too low level for most use cases, and will require developers to create additional tools to post-process the traces. Additionally, a single opcode trace can easily be hundreds of megabytes, making them very resource intensive to extract from the node and process extenally.
To avoid these issues, Geth supports running custom JavaScript traces within the BlockX (or any EVM-compatible) node, which have full access to the EVM stack, memory, and contract storage. This means developers only have to gather data that they actually need, and do any processing at the source.
Filtered EVM Tracing with JS
Basic traces can include the complete status of the EVM at every point in the transaction's execution, which is huge space-wise. Usually, developers are only interested in a small subset of this information, which can be obtained by specifying a JavaScript filter.
Running a Simple Trace
debug.traceTransaction
must be invoked from within the Geth console, although it can be invoked from outside the node using JSON-RPC (eg. using Curl), as seen in the following section. If developers want to use debug.traceTransaction
as it is used here, maintainence of a node is required.
Create a file,
filterTrace_1.js
, with this content:
2. Run the JavaScript console.
3. Get a hash of a recent transaction.
4. Run this command to run the script:
5. Run the tracer from the script:
The bottom of the output looks similar to:
6. Run this command to get a more readable output with each string on its own line:
The JSON.stringify function's documentation is here. If we just return the output, we get for newlines, which is why we need to use console.log
.
How Does it Work?
We call the same debug.traceTransaction
function used for basic traces, but with a new parameter, tracer
. This parameter is a string, which is the JavaScript object we use. In the case of the trace above, it is:
This object has to have three member functions:
step
, called for each opcodefault
, called if there is a problem in the executionresult
, called to produce the results that are returned bydebug.traceTransaction
after the execution is done
It can have additional members. In this case, we use retVal
to store the list of strings that we'll return in result
.
The step
function here adds to retVal
: the program counter, and the name of the opcode there. Then, in result
, we return this list to be sent to the caller.
Actual Filtering
For actual filtered tracing, we need an if
statement to only log revelant information. For example, if we are interested in the transaction's interaction with storage, we might use:
The step
function here looks at the opcode number of the op, and only pushes an entry if the opcode is SLOAD
or SSTORE
. We could have used log.op.toString
instead, but it is faster to compare numbers rather than strings.
The output looks similar to this:
Stack Information
The trace above tells us the program counter and whether the program read from storage or wrote to it. To know more, you can use the log.stack.peek
function to peek into the stack. log.stack.peek(0)
is the stack top, log.stack.peek(1)
is the entry beow it, etc.
The values returned by log.stack.peek
are Go big.int
objects. By default they are converted to JavaScript floating point numbers, so you need toString(16) to get them as hexadecimals, which is how we normally represent 256-bit values such as storage cells and their content.
There are several other facets of filtered EVM tracing, including:
determining operation results
dealing with calls between contracts
accessing memory
using the
db
parameter to know the state of the chain at the time of execution
JSON-RPC debug_trace*
Endpoints
debug_trace*
EndpointsBlockX supports the following debug_trace*
JSON-RPC Methods, which follow Geth's debug API guidelines.
debug_traceTransaction
debug_traceTransaction
The traceTransaction
debugging method will attempt to run the transaction in the exact same manner as it was executed on the network. It will replay any transaction that may have been executed prior to this one, before it will finally attempt to execute the transaction that corresponds to the given hash.
Parameters:
trace configuration
debug_traceBlockByHash
debug_traceBlockByHash
The traceBlockByNumber
endpoint accepts a block hash, and will replay the block that is already present in the database.
Parameters:
trace configuration
Last updated