Skip to main content

Debug & Trace

When a transaction fails or behaves unexpectedly, you need to see exactly what the EVM did — which opcodes executed, what values were on the stack, when storage was read or written. DevChain supports Geth-compatible debug_traceTransaction and debug_traceCall methods that give you opcode-level execution traces.

This builds on the Testing Patterns guide. If you're not yet familiar with DevChain test fixtures, start there.

Prerequisites

dotnet add package Nethereum.DevChain

Trace a Mined Transaction

After a transaction has been mined, trace its full execution:

using Nethereum.DevChain;
using Nethereum.CoreChain.Tracing;
using Nethereum.Web3.Accounts;

var account = new Account("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
var node = new DevChainNode();
await node.StartAsync(account);
var web3 = node.CreateWeb3(account);

// Send a transaction
var receipt = await web3.Eth.GetEtherTransferService()
.TransferEtherAndWaitForReceiptAsync("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 1.0m);

// Trace it
var traceConfig = new OpcodeTraceConfig
{
DisableMemory = false,
DisableStack = false,
DisableStorage = false
};

var trace = await node.TraceTransactionAsync(receipt.TransactionHash, traceConfig);

The trace result contains a StructLogs array — one entry per executed opcode. Each entry includes:

FieldDescription
PcProgram counter
OpOpcode name (PUSH1, SLOAD, CALL, etc.)
GasGas remaining
GasCostGas consumed by this opcode
DepthCall depth (1 = top-level, 2+ = internal calls)
StackStack contents (bottom to top)
MemoryMemory contents (hex)
StorageStorage changes at this step

Trace a Call Without Mining

debug_traceCall lets you trace a hypothetical call without actually mining it — useful for debugging eth_call failures or simulating contract interactions:

using Nethereum.RPC.Eth.DTOs;

var callInput = new CallInput
{
From = account.Address,
To = contractAddress,
Data = functionData
};

var trace = await node.TraceCallAsync(
callInput,
new OpcodeTraceConfig { DisableMemory = true },
stateOverrides: null
);

With State Overrides

You can modify state for the trace without affecting the actual chain — useful for "what if" scenarios:

var overrides = new Dictionary<string, StateOverride>
{
[account.Address] = new StateOverride
{
Balance = new HexBigInteger(Web3.Convert.ToWei(1_000_000))
}
};

var trace = await node.TraceCallAsync(callInput, traceConfig, overrides);

State overrides only apply to this trace call — the chain state is not modified.

Reading Traces via RPC

When using the HTTP server, trace via standard JSON-RPC:

# Trace a mined transaction
curl -X POST http://127.0.0.1:8545 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "debug_traceTransaction",
"params": ["0xTXHASH", {"disableMemory": true}],
"id": 1
}'

# Trace a call
curl -X POST http://127.0.0.1:8545 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "debug_traceCall",
"params": [
{"from": "0x...", "to": "0x...", "data": "0x..."},
"latest",
{"disableMemory": true}
],
"id": 1
}'

Trace Configuration

Control what data is captured to balance detail vs. performance:

var traceConfig = new OpcodeTraceConfig
{
DisableMemory = true, // Skip memory dumps (large, often not needed)
DisableStack = false, // Keep stack data (essential for debugging)
DisableStorage = false // Keep storage changes (useful for state debugging)
};

For simple gas analysis, disable everything except the opcodes:

var minimal = new OpcodeTraceConfig
{
DisableMemory = true,
DisableStack = true,
DisableStorage = true
};

Debugging Patterns

Find Where a Transaction Reverts

Look for the REVERT opcode in the trace:

var trace = await node.TraceTransactionAsync(txHash, traceConfig);

foreach (var step in trace.StructLogs)
{
if (step.Op == "REVERT")
{
Console.WriteLine($"Revert at PC={step.Pc}, depth={step.Depth}, gas={step.Gas}");
// The revert reason is in memory at this point
break;
}
}

Track Storage Changes

Filter for SSTORE operations to see what the contract wrote:

foreach (var step in trace.StructLogs)
{
if (step.Op == "SSTORE" && step.Storage != null)
{
foreach (var (slot, value) in step.Storage)
{
Console.WriteLine($"SSTORE slot={slot} value={value}");
}
}
}

Identify External Calls

CALL, STATICCALL, and DELEGATECALL opcodes indicate cross-contract interactions. Watch for depth changes:

var maxDepth = trace.StructLogs.Max(s => s.Depth);
Console.WriteLine($"Max call depth: {maxDepth}");

foreach (var step in trace.StructLogs.Where(s => s.Op.Contains("CALL")))
{
Console.WriteLine($"{step.Op} at depth={step.Depth}, gas={step.Gas}");
}

Connection to EVM Simulator

DevChain's tracing uses the same EVM engine described in the EVM Simulator section. For deeper analysis — call tree decoding, log extraction, state change extraction — see:

Next Steps