Skip to content

debug: specify callTracer output and add debug_traceCall - #855

Open
MysticRyuujin wants to merge 10 commits into
ethereum:mainfrom
MysticRyuujin:calltracer-spec-fixtures
Open

MysticRyuujin wants to merge 10 commits into
ethereum:mainfrom
MysticRyuujin:calltracer-spec-fixtures

Conversation

@MysticRyuujin

@MysticRyuujin MysticRyuujin commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Specifies the callTracer output (undefined until now) for debug_traceTransaction, debug_traceBlockByNumber/ByHash, and a new debug_traceCall. Same approach as the opcode tracer spec (#762); other named tracers keep the unconstrained branch.

Stacked on the Amsterdam chain bump (#867); the first three commits are that PR. Review the last four commits here.

What's in it

  • CallFrame / CallLog / CallTracerConfig / CallTracerBlockEntry (src/schemas/call-tracer.yaml) with per-field presence and omission rules; recursion via an $id self-ref.
  • debug_traceCall: GenericTransaction, optional block param (default latest), and TraceCallConfig (reuses the eth_simulateV1 overrides). A revert is a traced result, not a JSON-RPC error.
  • Tooling: specgen passes absolute-URI $refs through so the recursive schema survives, and speccheck selects the anyOf branch for the tracer each fixture requests. Without that selection, trace-result validation was vacuous, including for the existing opcode branch. Negative tests prove both.
  • 21 fixtures exercising nested call trees: inner reverts, STATICCALL write protection, DELEGATECALL logs, CALLCODE, precompile calls, and CREATE followed by SELFDESTRUCT. The scenarios come from the calltree contract in hivechain (cmd/hivechain: add calltree contract and tx mods for callTracer testing hive#1587, merged), so the fixtures regenerate from upstream. The set includes the omitted-block-param form of debug_traceCall, which fills now that eth/tracers: make debug_traceCall block parameter optional go-ethereum#35583 is merged.

Decisions already baked into the spec (raise it if you object, but the data settled these):

  • error is exactly "execution reverted" for REVERT, free-form otherwise. That is the only string all clients agree on.
  • root gasUsed equals receipt gasUsed (4/5 clients; besu to align).
  • reverted-frame logs are cleared (receipt consistency; reth to align).

Conformance, original 18 fixtures (hive rpc-compat, glamsterdam-devnet-8 images, 2026-08-25). The three fixtures added later have their own table below. Every other debug_* failure these clients show also occurs on main's fixtures, so the rows below are the whole delta.

client result changes needed
go-ethereum 18/18 none; ethereum/go-ethereum#35583 (merged 2026-08-28) made the debug_traceCall block param optional
erigon 18/18 none
reth 17/18 return -32000 instead of -32001 for an out-of-range block in debug_traceCall
besu 15/18 honor onlyTopCall (still returns nested calls); accept a block hash as the block param
nethermind 13/18 stop base-fee validation of debug_traceCall calls that carry no fee fields; all five failures are max fee per gas less than block base fee errors where the reference traces
ethrex 1/18 debug_trace* not usable yet; tracked as not-yet-implemented, not a conformance gap

Conformance, fixtures added after that run (hive rpc-compat, ethpandaops master/main images, 2026-09-21). trace-call-omitted-block is speconly; the other two are exact-match. Side finding from the client builds: reth main and nethermind master add the EIP-8037 gas fields (regularGasUsed/executionGasUsed, stateGasUsed, gasRefund) to callTracer frames when tracing at the Amsterdam head, so the exact-match debug_traceCall/calltracer-only-top-call fixture fails on both before and after the index fixes. That is the #852 follow-up surfacing early, not an index issue.

client trace-call-omitted-block calltracer-with-log-index calltracer-only-top-call-with-log changes needed
go-ethereum pass pass pass none
erigon pass pass pass none since erigontech/erigon#23797 (merged 2026-09-09); the Sep 7 main image still returns transaction-local 0x0, current main built from source passes both
reth pass fail fail number logs[].index block-globally in debug_traceTransaction (0x0 for 0x3); report position 0 under onlyTopCall (returns 0x8). Draft fixes: paradigmxyz/revm-inspectors#512 + paradigmxyz/reth#27356, both fixtures pass on the patched build
nethermind pass fail fail emit logs[].index; the rest of both frames matches byte for byte. Draft fix: NethermindEth/nethermind#13636, both fixtures pass on the patched build
besu pass fail fail honor withLog (the logs array is missing entirely). Draft fix: besu-eth/besu#11342 implements withLog with position and index, both fixtures pass on the patched build
ethrex not run not run not run cannot import the test chain (lambdaclass/ethrex#6954), so hive cannot measure it. Its callTracer already numbers index block-globally; draft lambdaclass/ethrex#7294 fixes onlyTopCall log filtering and makes debug_traceCall start at zero, unit-tested

logs[].index is now defined (2026-09-21): required, block-global, equal to the receipt logIndex, as proposed by @bomanaps and agreed on the client call. This is geth's current behavior. erigon and reth number transaction-locally in debug_traceTransaction, and nethermind omits the field; the existing withLog fixtures did not catch this because they trace the first log-emitting transaction of a block or trace a whole block, where accumulation gives the same numbers. Two new exact-match fixtures enforce this and pair each trace with its receipt: calltracer-with-log-index traces the second log-emitting transaction of a block, so transaction-local numbering fails it, and calltracer-only-top-call-with-log traces the calltree root with onlyTopCall, where the root log keeps index 2 behind two unreported nested logs, so renumbering under onlyTopCall fails it. The second table above has the measured results. That run also exposed an ambiguity in position under onlyTopCall (geth 0, reth 8, nethermind 0): the spec now says position is 0 when onlyTopCall is set, since no subcall frames are collected.

EIP-8037 / #852: this PR specs the fork-independent frame shape only. CallFrame keeps additionalProperties open, so the regularGasUsed/stateGasUsed/gasRefund fields proposed in #852 drop in as optional fields on top, and the Amsterdam chain underneath already produces post-fork traces to generate fixtures against. Thanks @qu0b for the EIP-8037 groundwork and the client survey.

Known issue: hive replays the exact-match tracer fixtures correctly either way, but validates the speconly subset against the whole result schema, where the unconstrained named-tracer branch accepts anything. Until ethereum/hive#1588 adds the same tracer-aware branch selection speccheck uses here, that hive-side validation stays vacuous for trace methods. speccheck already enforces it when fixtures are generated, so this is a hive reporting gap, not a spec gap, and not a blocker.

Update 2026-08-28: rebased on the current #867, which is itself rebased on main and pins go-ethereum master. Added the omitted-block-param fixture that was waiting on ethereum/go-ethereum#35583. Fill, speccheck, lint, and the tools tests all pass.

@MysticRyuujin
MysticRyuujin force-pushed the calltracer-spec-fixtures branch 2 times, most recently from e4872f4 to 4861c60 Compare August 11, 2026 19:48
@MysticRyuujin
MysticRyuujin force-pushed the calltracer-spec-fixtures branch from 4861c60 to 7c9b696 Compare August 25, 2026 14:15
@MysticRyuujin MysticRyuujin changed the title debug: standardize callTracer output and add debug_traceCall debug: specify callTracer output and add debug_traceCall Aug 25, 2026
@MysticRyuujin
MysticRyuujin marked this pull request as ready for review August 25, 2026 14:16
rjl493456442 pushed a commit to ethereum/go-ethereum that referenced this pull request Aug 28, 2026
`debug_traceCall` requires the block number or hash parameter today.
`TraceCall` takes `rpc.BlockNumberOrHash` by value, so an omitted
parameter returns "missing value for required argument 1".

The pending callTracer spec (ethereum/execution-apis#855) marks the
parameter optional with a default of `latest`, consistent with the
state-method defaults from #35100 (execution-apis #812/#814 lineage).

This changes the argument to `*rpc.BlockNumberOrHash` and defaults to
latest when nil. A regression test calls `debug_traceCall` through the
RPC server with the parameter omitted and compares against an explicit
`latest`. No behavior change when the parameter is supplied.
Pin go-ethereum to glamsterdam-devnet-8 (matching hive master) and move
mkchain to -lastfork amsterdam. Adapt the affected generators: EIP-7708
transfer logs in eth_simulateV1 log counts, EIP-2780 intrinsic repricing
in eth_estimateGas, EIP-7843 slotNumber in testing_buildBlockV1, and fund
the complete-eth-sends recipient so account-creation state gas does not
push the 21000-gas send out of gas.
Chain generated from upstream hivechain (hive master). Amsterdam
activates at block 57 of 60. All fixtures refilled; block hashes
change, so the whole tree drifts.
Picks up the post-glam8 RPC fixes: estimateGas returns used gas for
plain transfers (#35592), debug_traceCall block param optional (#35583),
eth_config skips unconfigured forks (#35553). The testing_commitBlockV1
generators from ethereum#801 now send slotNumber post-Amsterdam, and the plain
transfer estimate expectation is 15000 per EIP-2780.
Specify the callTracer output for debug_traceTransaction,
debug_traceBlockByNumber/ByHash, and a new debug_traceCall. Add the
recursive CallFrame/CallLog/CallTracerConfig/CallTracerBlockEntry schemas,
specgen absolute-URI $ref passthrough, and speccheck tracer-aware anyOf
branch selection.

Fixtures regenerated separately against the osaka+calltree chain.
…fs stubbed

The docs plugin fetches every $ref URL over HTTP. The CallFrame
recursion $id is deliberately unpublished, so the build failed.
build-docs-spec.mjs writes openrpc-docs.json with in-document $id
refs stubbed, and docusaurus reads that copy.
go-ethereum #35583 made the block parameter optional, so the omitted
form now fills against the pinned geth.
@MysticRyuujin
MysticRyuujin force-pushed the calltracer-spec-fixtures branch from 7c9b696 to 9590ccc Compare August 28, 2026 13:44
taratorio pushed a commit to nerolation/erigon that referenced this pull request Aug 31, 2026
…ntech#23683)

`debug_traceCall` took the block selector by value
(`rpc.BlockNumberOrHash`), so omitting it — or passing `null` — was
rejected by the positional argument parser with `missing value for
required argument 1`. It was the last state-reading method still
requiring the parameter: `eth_call`, `eth_estimateGas`,
`eth_getBalance`, `eth_getCode`, `eth_getProof`, `eth_createAccessList`,
`trace_call` and friends already take `*rpc.BlockNumberOrHash` and
default to `latest`.

The parameter is now a pointer and defaults to `latest`, matching geth
(`ethereum/go-ethereum#35583`) and the pending spec in
`ethereum/execution-apis#855`, which marks it optional with default
`latest`. `pending` is still rejected, as before.

Also renames the shared helper `orLatest` to `blockOrLatest`
(mechanical, 9 call sites) and adds unit tests for it.

## Testing

- New `TestDebugTraceCallBlockParamDefaultsToLatest` drives the change
through the JSON-RPC dispatch layer (where the failure was): an omitted
and a `null` block selector must produce the same trace as an explicit
`"latest"`. Red before the change with `missing value for required
argument 1` on both cases.
- New `TestBlockOrLatest` covers the helper: nil defaults to latest, an
explicit number is kept, a hash keeps `requireCanonical`, `pending` is
passed through for the caller to reject.
- `go test -short ./rpc/jsonrpc/ ./rpc/` and `golangci-lint` on
`rpc/jsonrpc/...` are clean.
LukaszRozmej added a commit to NethermindEth/nethermind that referenced this pull request Sep 1, 2026
…13078)

* fix(rpc): omit `to` on failed top-level CREATE frames in callTracer

The execution-apis `CallFrame` schema (ethereum/execution-apis#855) requires
`to` to be omitted on CREATE/CREATE2 frames that failed, since no contract was
deployed. NativeCallTracer applied that only to nested frames; the root frame
of a failed deployment kept the would-be contract address, unlike geth, which
runs the same processOutput at depth 0.

Also pins the spec's `debug_traceCall` revert contract: a revert is a traced
result carrying error/revertReason/output, not a JSON-RPC error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: address review on the callTracer failed-CREATE fix

- Cover a non-reverting halt (INVALID) as well as REVERT: the `to`-omission
  fires on any EVM halt, not just REVERT. Replaces the two-bool parameters
  with a CreateOutcome enum so the fourth case stays readable.
- Assert no JSON-RPC error before dereferencing `result` in the traceCall
  create test, so a pre-flight rejection reports the error object instead of
  a NullReferenceException.
- Build the state override from a dictionary instead of a raw-string JSON
  round-trip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bomanaps

bomanaps commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The index should be block-global so to match receipt logIndex since that is the only semantics where it has real cross-reference value, and for debug_traceTransaction implementations would need to replay prior transactions to produce the correct value but debug_traceBlock gets it for free from accumulated state. I prefer we define it as block-global rather than leave it undefined and differ per client.

Follows the client-call decision to adopt go-ethereum's block-global
numbering. Adds an exact-match fixture that traces the second log-emitting
transaction of a block, so transaction-local numbering fails it.
The root log keeps its receipt logIndex although the nested logs before it
are not reported, so a client that renumbers under onlyTopCall fails it.
@MysticRyuujin

Copy link
Copy Markdown
Contributor Author

I have opened draft PRs against the other clients to unify the index log behavior and hopefully put at least a couple more eyes on the decision.

jimmyc256 pushed a commit to Lambda256/go-ethereum-ralim that referenced this pull request Sep 23, 2026
…35583)

`debug_traceCall` requires the block number or hash parameter today.
`TraceCall` takes `rpc.BlockNumberOrHash` by value, so an omitted
parameter returns "missing value for required argument 1".

The pending callTracer spec (ethereum/execution-apis#855) marks the
parameter optional with a default of `latest`, consistent with the
state-method defaults from ethereum#35100 (execution-apis ethereum#812/ethereum#814 lineage).

This changes the argument to `*rpc.BlockNumberOrHash` and defaults to
latest when nil. A regression test calls `debug_traceCall` through the
RPC server with the parameter omitted and compares against an explicit
`latest`. No behavior change when the parameter is supplied.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants