| title | Structured Gas Trace Format |
|---|---|
| description | JSON-lines trace events for individual gas charges, refunds, and dimensional transfers |
| author | Edgar Luque (@edgl) |
| discussions-to | https://ethereum-magicians.org/t/<REPLACE-with-real-thread-id> |
| status | Draft |
| type | Standards Track |
| category | Interface |
| created | 2026-05-13 |
| requires | 3155, 7778, 8037 |
A JSON-lines event format, emitted alongside an EIP-3155 trace, that records every individual gas charge, gas refund, and dimensional transfer performed during transaction execution. The format makes the internal steps of two-dimensional gas accounting (EIP-7778, EIP-8037) observable, so that two implementations producing different gasUsed values can be aligned event-by-event to find the first divergence.
EIP-3155 records one entry per opcode. The gasCost field is the total charged for that opcode and hides the individual charges that produced it. A single CALL can charge cold-account access, value transfer, memory expansion, state-gas, and forwarded subcall budget; an EIP-3155 trace shows only the sum.
Two-dimensional gas accounting introduces a second metering channel (state_gas) with its own reservoir, spill behavior, refund routing, and intrinsic-refund channels. A client that miscounts by one NEW_ACCOUNT_state_gas charge in a SELFDESTRUCT path produces the same gasUsed difference as a client that correctly charges but forgets to refund. EIP-3155 cannot distinguish these.
Today the debugging workflow is to add printf statements to both the client and the reference specification, align the two textual logs by hand, and find the first divergence. This EIP defines a stable, line-oriented event format so the same alignment can be done by a tool.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Each event is one line of JSON terminated by \n. Event lines MAY be interleaved with EIP-3155 opcode lines on the same stream. Consumers MUST distinguish events by the type field; all event types defined here are namespaced under gas..
Gas values are unsigned 64-bit integers. Addresses are 20-byte hex strings with 0x prefix. The frame field is the call-stack depth at the time of emission; the top-level transaction frame is frame: 0.
type |
Triggered by | Required fields |
|---|---|---|
gas.intrinsic |
After intrinsic-gas calculation, before execution | regular, state, floor, reservoir_initial |
gas.charge.regular |
Any decrement of regular gas | amount, frame, opcode, pc, reason, gas_remaining_after |
gas.charge.state |
Any charge against the state-gas reservoir | amount, frame, opcode, pc, reason, from_reservoir, spill_to_regular, reservoir_after, state_gas_used_after |
gas.refund.state |
Any credit to the state-gas refund channel | amount, frame, opcode, pc, reason, applied_to_reservoir, applied_to_pending |
gas.refund.regular |
Any addition to the EIP-3529 refund counter | amount, frame, opcode, pc, reason, refund_counter_after |
gas.transfer.subcall |
A child frame is entered (CALL, CREATE) | regular_to_child, state_to_child, parent_regular_after, parent_state_after |
gas.transfer.return |
A child frame returns | regular_from_child, state_from_child, pending_from_child, parent_regular_after, parent_state_after |
gas.intrinsic.refund |
Transaction-end intrinsic state-gas refunds | amount, channel, reason |
gas.summary |
Once per transaction, after all refunds applied | regular_used, state_used_net, state_refund, reservoir_final, sender_debit, coinbase_credit |
channel in gas.intrinsic.refund is one of reservoir or state_refund.
The reason field is a short string drawn from a registry colocated with this EIP. Examples: op_create.new_account, op_sstore.storage_set, op_call.value_to_empty, op_selfdestruct.beneficiary_new_account, tx_create.collision_refund, eip7702.auth_base_refill. Unknown reasons MUST be passed through unchanged; new charge sites added in future forks add new reasons without requiring an EIP per change.
Implementations MUST satisfy the following for every traced transaction:
- The sum of
gas.charge.regular.amountminus the sum ofgas.refund.regular.amount(after EIP-3529 capping) equalsgas.summary.regular_used. - The sum of
gas.charge.state.amountminus the sum ofgas.refund.state.amountminus the sum ofgas.intrinsic.refund.amount(wherechannel = state_refund) equalsgas.summary.state_used_net. - For each
gas.charge.state,from_reservoir + spill_to_regular == amount. gas.summary.sender_debit == (regular_used + Σ gas.charge.state.spill_to_regular) × effective_gas_price.- Every
gas.transfer.subcallis paired with exactly onegas.transfer.return. On error in the child, the return event has an additionalerrorfield; the pairing requirement is unchanged.
This EIP defines a debug surface; there is no consensus impact and no protocol activation. Clients SHOULD emit the format when a debug flag (for example --trace-gas) is set. Reference specifications (EELS, py-evm) SHOULD emit the format under their existing trace flag.
EIP-3155 emits one event per opcode. Multiple charges fire within a single opcode and the order matters for spill-and-clamp behavior. Adding a list of sub-charges to each EIP-3155 entry would change the shape of an established format and break existing consumers.
This is a debug surface, not a consensus surface. Existing EIP-3155 consumers already use line-oriented JSON; a compact binary encoding can be standardized later if I/O cost becomes a bottleneck.
Many charges fire in helper code where the responsible opcode is ambiguous or absent (frame entry/exit accounting, EIP-7702 authorization processing, end-of-transaction SELFDESTRUCT settlement). A stable string lets a diff tool produce actionable error messages without re-deriving the spec.
The format is additive. Consumers of EIP-3155 traces ignore lines with unknown top-level fields. There is no change to consensus, RPC, or any persisted format.
A reference event stream is provided in assets/eip-####/sd-create-sstore.jsonl. The fixture is a single transaction that:
- CREATEs an inner contract.
- The inner contract SELFDESTRUCTs to itself.
- The outer frame CALLs the inner with
value = 1. - The outer frame SSTOREs the CALL result.
Under EIP-8037 with the same-transaction SELFDESTRUCT state-gas refund removed, the stream contains exactly one gas.charge.state with reason = op_create.new_account and exactly one with reason = op_sstore.storage_set. Any additional gas.charge.state with the same reason, or any gas.refund.state with reason = tx_end.sd_refund, indicates non-conformance with the v7.1.0 spec.
The trace contains gas-accounting internals and reveals nothing not already derivable from a full EIP-3155 trace plus the post-state.
Tracing is expensive. Clients MUST NOT emit traces on production endpoints by default. Emission should be gated on a build feature and on an explicit flag or environment variable.
Copyright and related rights waived via CC0.