Skip to content

Tracing: Introduction

Péter Szilágyi edited this page Nov 16, 2017 · 21 revisions

There are two different types of transactions in Ethereum: plain value transfers and contract executions. A plain value transfer just moves Ether from one account to another and as such is uninteresting from this guide's perspective. If however the recipient of a transaction is a contract account with associated EVM (Ethereum Virtual Machine) bytecode – beside transferring any Ether – the code will also be executed as part of the transaction.

Having code associated with Ethereum accounts permits transactions to do arbitrarilly complex data storage and enables them to act on the previously stored data by further transacting internally with outside accounts and contracts. This creates an intertwined ecosystem of contracts, where a single transaction can interact with tens or hunderds of accounts.

The downside of contract execution is that it is very hard to say what a transaction actually did. A transaction receipt does contain a status code to check whether execution succeeded or not, but there's no way to see what data was modified, nor what external contracts where invoked. In order to introspect a transaction, we need to trace its execution.

Tracing prerequisites

In its simplest form, tracing a transaction entails requesting the Ethereum node to reexecute the desired transaction with varying degrees of data collection and have it return the aggregated summary for post processing. Reexecuting a transaction however has a few prerequisites to be met.

In order for an Ethereum node to reexecute a transaction, it needs to have available all historical state accessed by the transaction:

  • Balance, nonce, bytecode and storage of both the recipient as well as all internally invoked contracts.
  • Block metadata referenced during execution of both the outer as well as all internally created transactions.
  • Intermediate state generated by all preceding transactions contained in the same block as the one being traced.

Depending on your node's mode of synchronization and pruning, different configurations result in different capabilities:

  • An archive node retaining all historical data can trace arbitrary transactions at any point in time. Tracing a single transaction also entails reexecuting all preceding transactions in the same block.
  • A fast synced node retaining all historical data after initial sync can only trace transactions from blocks following the initial sync point. Tracing a single transaction also entails reexecuting all preceding transactions in the same block.
  • A fast synced node retaining only periodic state data after initial sync can only trace transactions from blocks following the initial sync point. Tracing a single transaction entails reexecuting all preceding transactions both in the same block, as well as all preceding blocks until the previous stored snapshot.
  • A light synced node retrieving data on demand can in theory trace transactions for which all required historical state is readily available in the network. In practice, data availability is not a feasible assumption.

There are exceptions to the above rules when running batch traces of entire blocks or chain segments. Those will be detailed later.

Basic EVM traces

The simplest type of transaction trace that go-ethereum can generate are raw EVM opcode traces. For every VM instruction the transaction executes, a structured log entry is emitted, containing all contextual metadata deemed useful. This includes the program counter, opcode name, opcode cost, remaining gas, execution depth and any occurred error. The structured logs can optionally also contain the content of the execution stack, execution memory and contract storage.

An example log entry for a single opcode looks like:

{
	"pc":      48,
	"op":      "DIV",
	"gasCost": 5,
	"gas":     64532,
	"depth":   1,
	"error":   null,
	"stack": [
		"00000000000000000000000000000000000000000000000000000000ffffffff",
		"0000000100000000000000000000000000000000000000000000000000000000",
		"2df07fbaabbe40e3244445af30759352e348ec8bebd4dd75467a9f29ec55d98d"
	],
	"memory": [
		"0000000000000000000000000000000000000000000000000000000000000000",
		"0000000000000000000000000000000000000000000000000000000000000000",
		"0000000000000000000000000000000000000000000000000000000000000060"
	],
	"storage": {
	}
}

The entire output of an 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 that take the above form:

{
	"gas":         25523,
	"failed":      false,
	"returnValue": "",
	"structLogs":  []
}

Generating basic traces

To generate a raw EVM opcode trace, go-ethereum provides a few RPC API endpoints, out of which the most commonly used is debug_traceTransaction.

In its simplest form, traceTransaction accepts a transaction hash as its sole argument, traces the transaction, aggregates all the generated data and returns it as a large JSON object. A sample invocation from the Geth console would be:

debug.traceTransaction("0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f")

The same call can of course be invoked from outside the node too via HTTP RPC. In this case, please make sure the HTTP endpoint is enabled via --rpc and the debug API namespace exposed via --rpcapi=debug.

$ curl -H "Content-Type: application/json" -d '{"id": 1, "method": "debug_traceTransaction", "params": ["0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f"]}' localhost:8545

Running the above operation on the Rinkeby network (with a node retaining enough history) will result in this trace dump.

Tuning basic traces

By default the raw opcode tracer emits all relevant events that occur within the EVM while processing a transaction, such as EVM stack, EVM memory and updated storage slots. Certain use cases however may not need some of these data fields reported. To cater for those use cases, these massive fields may be omitted using a second options parameter for the tracer:

{
	"disableStack": true,
	"disableMemory": true,
	"disableStorage": true
}

Running the previous tracer invocation from the Geth console with the data fields disabled would be:

debug.traceTransaction("0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f", {disableStack: true, disableMemory: true, disableStorage: true})

And analogously, using the HTTP RPC API:

$ curl -H "Content-Type: application/json" -d '{"id": 1, "method": "debug_traceTransaction", "params": ["0xfc9359e49278b7ba99f59edac0e3de49956e46e530a53c15aa71226b7aa92c6f", {"disableStack": true, "disableMemory": true, "disableStorage": true}]}' localhost:8545

Running the above operation on the Rinkeby network will result in this significantly shorter trace dump.

Clone this wiki locally