ethrex_common/tracing.rs
1//! Trace data types and their wire-format serializers.
2//!
3//! ## Architecture
4//!
5//! Capture, data, and output format are separated:
6//!
7//! - **Capture** lives in `ethrex-levm` (`LevmOpcodeTracer`, the dispatch-loop hook).
8//! It runs once per tx and produces a [`Vec<OpcodeStep>`] plus the trailing
9//! metadata in [`OpcodeTraceResult`].
10//! - **Data** are the bare structs [`OpcodeStep`] and [`OpcodeTraceResult`] in this
11//! module. They carry no `Serialize` impl — they're consumer-agnostic. The same
12//! captured data feeds every downstream wire format.
13//! - **Wire format** is a newtype wrapper around one of those data structs with its
14//! own `Serialize` impl. Two shapes coexist:
15//! - [`StructLoggerStep`] / [`StructLoggerResult`] — the geth-RPC `debug_traceTransaction`
16//! structLogger shape: `op` as string mnemonic, no `opName`, decimal `gas`, etc.
17//! Used by the RPC handler and matches what every major client (geth, besu, …) emits
18//! from this endpoint. Consumers: Blockscout, Foundry, Tenderly, anything reading
19//! `debug_traceTransaction`.
20//! - [`Eip3155Step`] — strict [EIP-3155](https://eips.ethereum.org/EIPS/eip-3155)
21//! shape: numeric `op` byte + separate `opName`, `"0xN"` hex `gas`/`gasCost`/`refund`,
22//! `stack:[]` (never null) when disabled. Used by streaming sinks that want
23//! spec-conformant per-step JSONL — e.g. the `ef-tests-statev2 statetest` subcommand
24//! feeding goevmlab.
25//!
26//! Adding a third format (Parity-style flat call, opcode-count tracers, …) means another
27//! newtype with its own `Serialize` impl. No changes to the data types or capture layer.
28//!
29//! ## Why not match geth-RPC everywhere
30//!
31//! `debug_traceTransaction` predates EIP-3155 by years and its de-facto shape diverges
32//! from the spec on three points: `op` is a string, `opName` is absent, and `gas`/`gasCost`
33//! are decimal numbers instead of `"0xN"` hex strings. Every major client matches geth's
34//! shape there for tooling compat, not EIP-3155. So:
35//! - RPC consumer expects structLogger → use [`StructLoggerStep`]/[`StructLoggerResult`].
36//! - EIP-3155-conformant CLI consumer (goevmlab, fuzzers) → use [`Eip3155Step`].
37
38use bytes::Bytes;
39use ethereum_types::H256;
40use ethereum_types::{Address, U256};
41use serde::Serialize;
42use std::collections::BTreeMap;
43
44/// Collection of traces of each call frame as defined in geth's `callTracer` output
45/// https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#call-tracer
46pub type CallTrace = Vec<CallTraceFrame>;
47
48/// Trace of each call frame as defined in geth's `callTracer` output
49/// https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#call-tracer
50#[derive(Debug, Serialize, Default)]
51#[serde(rename_all = "camelCase")]
52pub struct CallTraceFrame {
53 /// Type of the Call
54 #[serde(rename = "type")]
55 pub call_type: CallType,
56 /// Address that initiated the call
57 pub from: Address,
58 /// Address that received the call. Omitted when absent (geth's `to,omitempty`),
59 /// e.g. nil'd for a failed CREATE/CREATE2.
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub to: Option<Address>,
62 /// Amount transferred. Omitted when absent (geth's `value,omitempty`) — notably
63 /// `STATICCALL`, which geth traces with a nil value.
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub value: Option<U256>,
66 /// Gas provided for the call
67 #[serde(with = "crate::serde_utils::u64::hex_str")]
68 pub gas: u64,
69 /// Gas used by the call
70 #[serde(with = "crate::serde_utils::u64::hex_str")]
71 pub gas_used: u64,
72 /// EIP-8037 gross (pre-refund) regular gas used by the transaction — its contribution
73 /// to `block_regular_gas_used` (EIP-7778). Set only on the top-level frame of an
74 /// Amsterdam+ transaction; sub-frame attribution is unspecified.
75 #[serde(
76 default,
77 with = "crate::serde_utils::u64::hex_str_opt",
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub regular_gas_used: Option<u64>,
81 /// EIP-8037 gross (pre-refund) state gas used by the transaction, net of source-based
82 /// state-gas refunds — its contribution to `block_state_gas_used`. Set only on the
83 /// top-level frame of an Amsterdam+ transaction.
84 #[serde(
85 default,
86 with = "crate::serde_utils::u64::hex_str_opt",
87 skip_serializing_if = "Option::is_none"
88 )]
89 pub state_gas_used: Option<u64>,
90 /// EIP-3529 gas refund applied to the transaction. Set only on the top-level frame of
91 /// an Amsterdam+ transaction.
92 #[serde(
93 default,
94 with = "crate::serde_utils::u64::hex_str_opt",
95 skip_serializing_if = "Option::is_none"
96 )]
97 pub gas_refund: Option<u64>,
98 /// Call data (always present, matching geth's non-optional `input`)
99 #[serde(with = "crate::serde_utils::bytes")]
100 pub input: Bytes,
101 /// Return data. Omitted when empty (geth's `output,omitempty`).
102 #[serde(
103 with = "crate::serde_utils::bytes",
104 skip_serializing_if = "Bytes::is_empty"
105 )]
106 pub output: Bytes,
107 /// Error returned if the call failed. Omitted when absent (geth's `error,omitempty`).
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub error: Option<String>,
110 /// Revert reason if the call reverted. Omitted when absent (geth's `revertReason,omitempty`).
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub revert_reason: Option<String>,
113 /// List of nested sub-calls. Omitted when empty (geth's `calls,omitempty`).
114 #[serde(skip_serializing_if = "Vec::is_empty")]
115 pub calls: Vec<CallTraceFrame>,
116 /// Logs (if enabled)
117 #[serde(skip_serializing_if = "Vec::is_empty")]
118 pub logs: Vec<CallLog>,
119}
120
121#[derive(Serialize, Debug, Default)]
122pub enum CallType {
123 #[default]
124 CALL,
125 CALLCODE,
126 STATICCALL,
127 DELEGATECALL,
128 CREATE,
129 CREATE2,
130 SELFDESTRUCT,
131}
132
133#[derive(Serialize, Debug)]
134#[serde(rename_all = "camelCase")]
135pub struct CallLog {
136 pub address: Address,
137 pub topics: Vec<H256>,
138 #[serde(with = "crate::serde_utils::bytes")]
139 pub data: Bytes,
140 /// Block-absolute log index, matching geth's `log.Index` (a counter running across
141 /// every tx in the block). Seeded per tx from the preceding txs' log count.
142 pub index: u64,
143 /// Position of this log relative to the subcalls of the enclosing frame.
144 pub position: u64,
145}
146
147/// Per-account state entry emitted by the prestateTracer.
148///
149/// `balance` is `Option<U256>`: `None` means "field absent from output",
150/// `Some(0)` still serializes (lets diff post emit a balance that became zero).
151#[derive(Debug, Serialize, Default, Clone)]
152#[serde(rename_all = "camelCase")]
153pub struct PrestateAccountState {
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub balance: Option<U256>,
156 #[serde(default, skip_serializing_if = "is_zero_nonce")]
157 pub nonce: u64,
158 #[serde(
159 default,
160 skip_serializing_if = "Bytes::is_empty",
161 with = "crate::serde_utils::bytes"
162 )]
163 pub code: Bytes,
164 #[serde(default, skip_serializing_if = "H256::is_zero")]
165 pub code_hash: H256,
166 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
167 pub storage: BTreeMap<H256, H256>,
168}
169
170impl PrestateAccountState {
171 /// True when no field conveys information; `Some(0)` balance counts as empty.
172 pub fn is_empty(&self) -> bool {
173 self.balance.unwrap_or_default().is_zero()
174 && self.nonce == 0
175 && self.code.is_empty()
176 && self.code_hash.is_zero()
177 && self.storage.is_empty()
178 }
179}
180
181/// Per-transaction prestate trace (non-diff mode). `BTreeMap` keeps JSON output
182/// deterministic via sorted keys.
183pub type PrestateTrace = BTreeMap<Address, PrestateAccountState>;
184
185/// Result of a prestateTracer execution — either a plain prestate map or a diff.
186#[derive(Debug, Clone)]
187pub enum PrestateResult {
188 /// Non-diff mode: map of address → pre-tx account state.
189 Prestate(PrestateTrace),
190 /// Diff mode: pre-tx and post-tx state for all touched accounts.
191 Diff(PrePostState),
192}
193
194/// Per-transaction prestate trace (diff mode).
195/// Contains the pre-tx and post-tx state for all touched accounts.
196#[derive(Debug, Serialize, Default, Clone)]
197pub struct PrePostState {
198 pub pre: BTreeMap<Address, PrestateAccountState>,
199 pub post: BTreeMap<Address, PrestateAccountState>,
200}
201
202fn is_zero_nonce(n: &u64) -> bool {
203 *n == 0
204}
205
206// ─── OpcodeTracer types ──────────────────────────────────────────────────────
207
208/// Per-opcode trace entry — pure data, no `Serialize` impl.
209///
210/// To get this on the wire, wrap in one of the format newtypes:
211/// - [`StructLoggerStep`] for geth-RPC `debug_traceTransaction` shape.
212/// - [`Eip3155Step`] for EIP-3155 spec shape.
213///
214/// See the module-level doc for why both formats coexist.
215#[derive(Debug)]
216pub struct OpcodeStep {
217 pub pc: u64,
218 /// Raw opcode byte value (e.g. 0x60 for PUSH1). Each format serializer decides
219 /// how to render this (numeric byte, hex string, mnemonic string).
220 pub op: u8,
221 pub gas: u64,
222 pub gas_cost: u64,
223 /// Current memory size in bytes.
224 pub mem_size: u64,
225 pub depth: u32,
226 /// Return data from the previous sub-call.
227 pub return_data: bytes::Bytes,
228 /// Gas refund counter.
229 pub refund: u64,
230 /// `Some(vec)` when stack capture is enabled (bottom-first); `None` when disabled.
231 /// Each format serializer decides how to render `None`: structLogger emits JSON null,
232 /// EIP-3155 emits `[]` (per spec's "MUST initialize to empty array" rule).
233 pub stack: Option<Vec<U256>>,
234 /// `Some(chunks)` when memory capture is enabled; `None` when disabled (field omitted).
235 pub memory: Option<Vec<MemoryChunk>>,
236 /// `Some(map)` at SLOAD/SSTORE steps when storage capture is enabled; `None`
237 /// otherwise. The map is a cumulative snapshot of every slot touched by an
238 /// SLOAD/SSTORE so far in the transaction — matching geth's structLogger.
239 /// The tracer maintains this in `LevmOpcodeTracer::cumulative_storage`.
240 pub storage: Option<BTreeMap<H256, H256>>,
241 pub error: Option<String>,
242}
243
244/// A 32-byte chunk of EVM memory, serialized as `"0x" + 64 lowercase hex chars`.
245/// The *caller* zero-pads the last partial chunk before constructing this type.
246#[derive(Debug)]
247pub struct MemoryChunk(pub [u8; 32]);
248
249/// Top-level result of one opcode-traced transaction — pure data, no `Serialize` impl.
250///
251/// Wrap in [`StructLoggerResult`] to get the geth-RPC `{failed, gas, returnValue, structLogs}`
252/// wire shape. EIP-3155-conformant CLI consumers stream per-step [`OpcodeStep`]s
253/// directly (via [`Eip3155Step`]) and emit their own summary line, so there's no
254/// EIP-3155 wrapper newtype for the result.
255#[derive(Debug)]
256pub struct OpcodeTraceResult {
257 pub gas_used: u64,
258 /// True iff the transaction completed without error.
259 pub pass: bool,
260 pub output: bytes::Bytes,
261 pub steps: Vec<OpcodeStep>,
262}
263
264// ─── Helpers ──────────────────────────────────────────────────────────────
265
266/// Returns the opcode mnemonic for `byte`.
267///
268/// Known opcodes → their uppercase name (`"PUSH1"`, `"ADD"`, `"INVALID"` for
269/// 0xFE). Unassigned bytes → `None`; callers wanting the conventional unknown
270/// string should fall back to `format!("opcode 0x{:02x} not defined", byte)`.
271///
272/// The table is **fork-agnostic by design**, matching geth's
273/// `core/vm/opcodes.go::opCodeToString` (also a flat 256-entry table). Fork
274/// validity is enforced at *dispatch* via the VM's per-fork opcode table:
275/// e.g. byte `0x5F` (PUSH0) halts pre-Shanghai with `InvalidOpcode` before
276/// the tracer ever emits a step for it, so the name lookup never fires for
277/// invalid-for-this-fork bytes in practice.
278pub fn opcode_name(byte: u8) -> Option<&'static str> {
279 match byte {
280 0x00 => Some("STOP"),
281 0x01 => Some("ADD"),
282 0x02 => Some("MUL"),
283 0x03 => Some("SUB"),
284 0x04 => Some("DIV"),
285 0x05 => Some("SDIV"),
286 0x06 => Some("MOD"),
287 0x07 => Some("SMOD"),
288 0x08 => Some("ADDMOD"),
289 0x09 => Some("MULMOD"),
290 0x0A => Some("EXP"),
291 0x0B => Some("SIGNEXTEND"),
292 0x10 => Some("LT"),
293 0x11 => Some("GT"),
294 0x12 => Some("SLT"),
295 0x13 => Some("SGT"),
296 0x14 => Some("EQ"),
297 0x15 => Some("ISZERO"),
298 0x16 => Some("AND"),
299 0x17 => Some("OR"),
300 0x18 => Some("XOR"),
301 0x19 => Some("NOT"),
302 0x1A => Some("BYTE"),
303 0x1B => Some("SHL"),
304 0x1C => Some("SHR"),
305 0x1D => Some("SAR"),
306 0x1E => Some("CLZ"),
307 0x20 => Some("KECCAK256"),
308 0x30 => Some("ADDRESS"),
309 0x31 => Some("BALANCE"),
310 0x32 => Some("ORIGIN"),
311 0x33 => Some("CALLER"),
312 0x34 => Some("CALLVALUE"),
313 0x35 => Some("CALLDATALOAD"),
314 0x36 => Some("CALLDATASIZE"),
315 0x37 => Some("CALLDATACOPY"),
316 0x38 => Some("CODESIZE"),
317 0x39 => Some("CODECOPY"),
318 0x3A => Some("GASPRICE"),
319 0x3B => Some("EXTCODESIZE"),
320 0x3C => Some("EXTCODECOPY"),
321 0x3D => Some("RETURNDATASIZE"),
322 0x3E => Some("RETURNDATACOPY"),
323 0x3F => Some("EXTCODEHASH"),
324 0x40 => Some("BLOCKHASH"),
325 0x41 => Some("COINBASE"),
326 0x42 => Some("TIMESTAMP"),
327 0x43 => Some("NUMBER"),
328 0x44 => Some("PREVRANDAO"),
329 0x45 => Some("GASLIMIT"),
330 0x46 => Some("CHAINID"),
331 0x47 => Some("SELFBALANCE"),
332 0x48 => Some("BASEFEE"),
333 0x49 => Some("BLOBHASH"),
334 0x4A => Some("BLOBBASEFEE"),
335 0x4B => Some("SLOTNUM"),
336 0x50 => Some("POP"),
337 0x51 => Some("MLOAD"),
338 0x52 => Some("MSTORE"),
339 0x53 => Some("MSTORE8"),
340 0x54 => Some("SLOAD"),
341 0x55 => Some("SSTORE"),
342 0x56 => Some("JUMP"),
343 0x57 => Some("JUMPI"),
344 0x58 => Some("PC"),
345 0x59 => Some("MSIZE"),
346 0x5A => Some("GAS"),
347 0x5B => Some("JUMPDEST"),
348 0x5C => Some("TLOAD"),
349 0x5D => Some("TSTORE"),
350 0x5E => Some("MCOPY"),
351 0x5F => Some("PUSH0"),
352 0x60 => Some("PUSH1"),
353 0x61 => Some("PUSH2"),
354 0x62 => Some("PUSH3"),
355 0x63 => Some("PUSH4"),
356 0x64 => Some("PUSH5"),
357 0x65 => Some("PUSH6"),
358 0x66 => Some("PUSH7"),
359 0x67 => Some("PUSH8"),
360 0x68 => Some("PUSH9"),
361 0x69 => Some("PUSH10"),
362 0x6A => Some("PUSH11"),
363 0x6B => Some("PUSH12"),
364 0x6C => Some("PUSH13"),
365 0x6D => Some("PUSH14"),
366 0x6E => Some("PUSH15"),
367 0x6F => Some("PUSH16"),
368 0x70 => Some("PUSH17"),
369 0x71 => Some("PUSH18"),
370 0x72 => Some("PUSH19"),
371 0x73 => Some("PUSH20"),
372 0x74 => Some("PUSH21"),
373 0x75 => Some("PUSH22"),
374 0x76 => Some("PUSH23"),
375 0x77 => Some("PUSH24"),
376 0x78 => Some("PUSH25"),
377 0x79 => Some("PUSH26"),
378 0x7A => Some("PUSH27"),
379 0x7B => Some("PUSH28"),
380 0x7C => Some("PUSH29"),
381 0x7D => Some("PUSH30"),
382 0x7E => Some("PUSH31"),
383 0x7F => Some("PUSH32"),
384 0x80 => Some("DUP1"),
385 0x81 => Some("DUP2"),
386 0x82 => Some("DUP3"),
387 0x83 => Some("DUP4"),
388 0x84 => Some("DUP5"),
389 0x85 => Some("DUP6"),
390 0x86 => Some("DUP7"),
391 0x87 => Some("DUP8"),
392 0x88 => Some("DUP9"),
393 0x89 => Some("DUP10"),
394 0x8A => Some("DUP11"),
395 0x8B => Some("DUP12"),
396 0x8C => Some("DUP13"),
397 0x8D => Some("DUP14"),
398 0x8E => Some("DUP15"),
399 0x8F => Some("DUP16"),
400 0x90 => Some("SWAP1"),
401 0x91 => Some("SWAP2"),
402 0x92 => Some("SWAP3"),
403 0x93 => Some("SWAP4"),
404 0x94 => Some("SWAP5"),
405 0x95 => Some("SWAP6"),
406 0x96 => Some("SWAP7"),
407 0x97 => Some("SWAP8"),
408 0x98 => Some("SWAP9"),
409 0x99 => Some("SWAP10"),
410 0x9A => Some("SWAP11"),
411 0x9B => Some("SWAP12"),
412 0x9C => Some("SWAP13"),
413 0x9D => Some("SWAP14"),
414 0x9E => Some("SWAP15"),
415 0x9F => Some("SWAP16"),
416 0xA0 => Some("LOG0"),
417 0xA1 => Some("LOG1"),
418 0xA2 => Some("LOG2"),
419 0xA3 => Some("LOG3"),
420 0xA4 => Some("LOG4"),
421 0xE6 => Some("DUPN"),
422 0xE7 => Some("SWAPN"),
423 0xE8 => Some("EXCHANGE"),
424 0xF0 => Some("CREATE"),
425 0xF1 => Some("CALL"),
426 0xF2 => Some("CALLCODE"),
427 0xF3 => Some("RETURN"),
428 0xF4 => Some("DELEGATECALL"),
429 0xF5 => Some("CREATE2"),
430 0xFA => Some("STATICCALL"),
431 0xFD => Some("REVERT"),
432 0xFE => Some("INVALID"),
433 0xFF => Some("SELFDESTRUCT"),
434 _ => None,
435 }
436}
437
438/// Converts a `U256` to geth's `uint256.Int.Hex()` form: `"0x"` followed by
439/// lowercase hex with leading zeros stripped. Zero → `"0x0"` (not `"0x"`).
440pub fn geth_uint256_hex(v: &U256) -> String {
441 if v.is_zero() {
442 return "0x0".to_string();
443 }
444 // U256 words are little-endian; convert to big-endian bytes.
445 let bytes = crate::utils::u256_to_big_endian(*v);
446 let hex_str = hex::encode(bytes);
447 let stripped = hex_str.trim_start_matches('0');
448 format!("0x{}", stripped)
449}
450
451// ─── Serialize impls ──────────────────────────────────────────────────────
452
453impl serde::Serialize for MemoryChunk {
454 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
455 serializer.serialize_str(&format!("0x{}", hex::encode(self.0)))
456 }
457}
458
459// Shared utilities used by both wire-format serializers below.
460
461fn serialize_storage_map<S: serde::Serializer>(
462 serializer: S,
463 storage: &BTreeMap<H256, H256>,
464) -> Result<S::Ok, S::Error> {
465 use serde::ser::SerializeMap;
466 let mut m = serializer.serialize_map(Some(storage.len()))?;
467 for (k, v) in storage {
468 let k_str = format!("0x{}", hex::encode(k.as_bytes()));
469 let v_str = format!("0x{}", hex::encode(v.as_bytes()));
470 m.serialize_entry(&k_str, &v_str)?;
471 }
472 m.end()
473}
474
475/// Mnemonic string for an opcode byte, falling back to `"opcode 0xNN not defined"`
476/// for bytes outside the assigned table.
477fn opcode_name_or_fallback(byte: u8) -> String {
478 opcode_name(byte)
479 .map(str::to_owned)
480 .unwrap_or_else(|| format!("opcode 0x{byte:02x} not defined"))
481}
482
483// ─── Wire format: geth-RPC structLogger ───────────────────────────────────
484//
485// The de-facto `debug_traceTransaction` response shape, emitted by every major
486// execution client (geth, besu, reth, erigon, nethermind). Predates EIP-3155
487// and diverges from it on three per-step fields:
488//
489// - `op`: string mnemonic (`"PUSH1"`), not the numeric opcode byte.
490// - No separate `opName` field.
491// - `gas`, `gasCost`, `refund`: decimal JSON numbers, not `"0xN"` hex strings.
492//
493// `stack` is serialized as JSON `null` when capture is disabled — also a divergence
494// from EIP-3155, which mandates `[]` — but it matches geth's RPC behavior so we
495// preserve it on this code path.
496//
497// Verified against geth and besu on a kurtosis localnet via `debug_traceTransaction`:
498// byte-for-byte identical to the StructLogger output.
499
500/// Controls which always-populated per-step fields the structLogger wire format emits.
501///
502/// `mem_size`, `return_data`, and `refund` are always present in the captured
503/// [`OpcodeStep`] (the capture layer just defaults them to zero/empty when the
504/// corresponding capture config is off). geth's `debug_traceTransaction` *suppresses*
505/// these fields unless their data is actually captured. To match geth byte-for-byte
506/// we honor the caller's intent explicitly here.
507///
508/// Typical mapping at the RPC layer:
509///
510/// ```ignore
511/// let emit = StructLoggerEmit {
512/// mem_size: cfg.enable_memory, // memSize travels with memory
513/// return_data: cfg.enable_return_data,
514/// refund: false, // no equivalent geth flag; off by default
515/// };
516/// ```
517#[derive(Debug, Clone, Copy, Default)]
518pub struct StructLoggerEmit {
519 /// Emit `memSize` even when its value is meaningful at every step.
520 /// Geth ties this to memory capture; default `false` matches geth's default config.
521 pub mem_size: bool,
522 /// Emit `returnData` (as `"0x..."` hex). Default `false` matches geth.
523 pub return_data: bool,
524 /// Force-emit `refund` even when it's zero. Default `false` matches geth's
525 /// `omitempty` behavior — non-zero refund is always emitted regardless of this flag.
526 pub refund: bool,
527}
528
529/// Wraps an [`OpcodeStep`] to serialize in the geth-RPC `structLogger` shape used by
530/// `debug_traceTransaction`. See module-level docs and the comment above this type
531/// for the field-shape divergences from EIP-3155.
532pub struct StructLoggerStep<'a> {
533 pub step: &'a OpcodeStep,
534 pub emit: StructLoggerEmit,
535}
536
537impl serde::Serialize for StructLoggerStep<'_> {
538 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
539 use serde::ser::SerializeMap;
540 let step = self.step;
541 let emit = self.emit;
542
543 // pc, op, gas, gasCost, depth, stack are always emitted (6 base fields).
544 let mut field_count = 6;
545 if emit.mem_size {
546 field_count += 1;
547 }
548 if emit.return_data {
549 field_count += 1;
550 }
551 if emit.refund || step.refund != 0 {
552 field_count += 1;
553 }
554 if step.error.is_some() {
555 field_count += 1;
556 }
557 if step.memory.is_some() {
558 field_count += 1;
559 }
560 if step.storage.is_some() {
561 field_count += 1;
562 }
563
564 let mut map = serializer.serialize_map(Some(field_count))?;
565
566 map.serialize_entry("pc", &step.pc)?;
567 // op: string mnemonic, matching geth's wire output (NOT EIP-3155's numeric form).
568 map.serialize_entry("op", &opcode_name_or_fallback(step.op))?;
569 // gas/gasCost/refund: decimal JSON numbers, matching geth's wire output.
570 map.serialize_entry("gas", &step.gas)?;
571 map.serialize_entry("gasCost", &step.gas_cost)?;
572 map.serialize_entry("depth", &step.depth)?;
573
574 // stack: JSON null when disabled, array of `"0xN"` hex strings when enabled.
575 // Matches geth's RPC behavior; diverges from EIP-3155's "MUST be []" rule.
576 struct StackSerializer<'a>(&'a Option<Vec<U256>>);
577 impl serde::Serialize for StackSerializer<'_> {
578 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
579 use serde::ser::SerializeSeq;
580 match self.0 {
581 None => serializer.serialize_none(),
582 Some(vec) => {
583 let mut seq = serializer.serialize_seq(Some(vec.len()))?;
584 for v in vec {
585 seq.serialize_element(&geth_uint256_hex(v))?;
586 }
587 seq.end()
588 }
589 }
590 }
591 }
592 map.serialize_entry("stack", &StackSerializer(&step.stack))?;
593
594 if emit.mem_size {
595 map.serialize_entry("memSize", &step.mem_size)?;
596 }
597 if emit.return_data {
598 map.serialize_entry(
599 "returnData",
600 &format!("0x{}", hex::encode(&step.return_data)),
601 )?;
602 }
603 // `refund` is omitempty-for-zero in geth's wire output: always emitted when
604 // non-zero; emitted-when-zero only when the caller forces it via `emit.refund`.
605 if emit.refund || step.refund != 0 {
606 map.serialize_entry("refund", &step.refund)?;
607 }
608
609 if let Some(err) = &step.error {
610 map.serialize_entry("error", err)?;
611 }
612 if let Some(mem) = &step.memory {
613 map.serialize_entry("memory", mem)?;
614 }
615 if let Some(storage) = &step.storage {
616 struct Wrap<'a>(&'a BTreeMap<H256, H256>);
617 impl serde::Serialize for Wrap<'_> {
618 fn serialize<S: serde::Serializer>(
619 &self,
620 serializer: S,
621 ) -> Result<S::Ok, S::Error> {
622 serialize_storage_map(serializer, self.0)
623 }
624 }
625 map.serialize_entry("storage", &Wrap(storage))?;
626 }
627
628 map.end()
629 }
630}
631
632/// Wraps an [`OpcodeTraceResult`] to serialize as the geth-RPC `debug_traceTransaction`
633/// response: `{failed, gas, returnValue, structLogs: [...]}`. Each step inside
634/// `structLogs` is itself serialized via [`StructLoggerStep`] using the same `emit` flags.
635pub struct StructLoggerResult<'a> {
636 pub result: &'a OpcodeTraceResult,
637 pub emit: StructLoggerEmit,
638}
639
640impl serde::Serialize for StructLoggerResult<'_> {
641 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
642 use serde::ser::{SerializeMap, SerializeSeq};
643 let r = self.result;
644 let emit = self.emit;
645
646 // structLogs uses StructLoggerStep for each entry, with the same emit options.
647 struct Steps<'a> {
648 steps: &'a [OpcodeStep],
649 emit: StructLoggerEmit,
650 }
651 impl serde::Serialize for Steps<'_> {
652 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
653 let mut seq = serializer.serialize_seq(Some(self.steps.len()))?;
654 for s in self.steps {
655 seq.serialize_element(&StructLoggerStep {
656 step: s,
657 emit: self.emit,
658 })?;
659 }
660 seq.end()
661 }
662 }
663
664 let mut map = serializer.serialize_map(Some(4))?;
665 // `failed` is the inverse of `pass` — matches the geth wire shape.
666 map.serialize_entry("failed", &!r.pass)?;
667 map.serialize_entry("gas", &r.gas_used)?;
668 map.serialize_entry("returnValue", &format!("0x{}", hex::encode(&r.output)))?;
669 map.serialize_entry(
670 "structLogs",
671 &Steps {
672 steps: &r.steps,
673 emit,
674 },
675 )?;
676 map.end()
677 }
678}
679
680// ─── Wire format: EIP-3155 ────────────────────────────────────────────────
681//
682// The shape defined by EIP-3155 §"Required Fields":
683//
684// - `op`: numeric opcode byte (e.g. `96` for PUSH1).
685// - `opName`: separate string mnemonic, always emitted (technically optional per spec).
686// - `gas`, `gasCost`, `refund`: `"0xN"` hex strings ("Hex-Number" per spec).
687// - `stack`: always an array, never null (spec: "All array attributes MUST be
688// initialized to empty arrays NOT to null").
689//
690// Field order matches the spec's listed order. Used by streaming sinks that feed
691// EIP-3155-conformant tooling (goevmlab, fuzzers). NOT used by `debug_traceTransaction`,
692// where existing tooling expects the structLogger shape above.
693
694/// Wraps an [`OpcodeStep`] to serialize in strict EIP-3155 shape. See module-level
695/// docs and the comment above this type for the field-shape choices.
696pub struct Eip3155Step<'a>(pub &'a OpcodeStep);
697
698impl serde::Serialize for Eip3155Step<'_> {
699 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
700 use serde::ser::SerializeMap;
701 let step = self.0;
702
703 let mut field_count = 10; // required 9 + always-emitted opName
704 if step.error.is_some() {
705 field_count += 1;
706 }
707 if step.memory.is_some() {
708 field_count += 1;
709 }
710 if step.storage.is_some() {
711 field_count += 1;
712 }
713
714 let mut map = serializer.serialize_map(Some(field_count))?;
715
716 // Required fields in spec order.
717 map.serialize_entry("pc", &step.pc)?;
718 map.serialize_entry("op", &step.op)?;
719 map.serialize_entry("gas", &format!("{:#x}", step.gas))?;
720 map.serialize_entry("gasCost", &format!("{:#x}", step.gas_cost))?;
721 map.serialize_entry("memSize", &step.mem_size)?;
722
723 // stack: always an array; `None` (disabled) becomes `[]`.
724 struct StackSerializer<'a>(&'a Option<Vec<U256>>);
725 impl serde::Serialize for StackSerializer<'_> {
726 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
727 use serde::ser::SerializeSeq;
728 let vec_ref: &[U256] = self.0.as_deref().unwrap_or(&[]);
729 let mut seq = serializer.serialize_seq(Some(vec_ref.len()))?;
730 for v in vec_ref {
731 seq.serialize_element(&geth_uint256_hex(v))?;
732 }
733 seq.end()
734 }
735 }
736 map.serialize_entry("stack", &StackSerializer(&step.stack))?;
737
738 map.serialize_entry("depth", &step.depth)?;
739 map.serialize_entry(
740 "returnData",
741 &format!("0x{}", hex::encode(&step.return_data)),
742 )?;
743 map.serialize_entry("refund", &format!("{:#x}", step.refund))?;
744
745 // Optional fields in spec order: opName, error, memory, storage.
746 // opName always emitted (covers both known and unknown opcode bytes).
747 map.serialize_entry("opName", &opcode_name_or_fallback(step.op))?;
748
749 if let Some(err) = &step.error {
750 map.serialize_entry("error", err)?;
751 }
752 if let Some(mem) = &step.memory {
753 map.serialize_entry("memory", mem)?;
754 }
755 if let Some(storage) = &step.storage {
756 struct Wrap<'a>(&'a BTreeMap<H256, H256>);
757 impl serde::Serialize for Wrap<'_> {
758 fn serialize<S: serde::Serializer>(
759 &self,
760 serializer: S,
761 ) -> Result<S::Ok, S::Error> {
762 serialize_storage_map(serializer, self.0)
763 }
764 }
765 map.serialize_entry("storage", &Wrap(storage))?;
766 }
767
768 map.end()
769 }
770}