fix(rpc): omit to on failed top-level CREATE frames in callTracer - #13078
Conversation
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>
|
Claude finished @LukaszRozmej's task in 5m 30s —— View job Review: omit
|
| null, | ||
| new { tracer = "callTracer" }); | ||
|
|
||
| JToken frame = JToken.Parse(response)["result"]!; |
There was a problem hiding this comment.
Low — unlike the sibling test above, this one dereferences ["result"]! without first asserting the response isn't a JSON-RPC error. If debug_traceCall ever starts rejecting this payload (e.g. an intrinsic-gas or base-fee pre-flight change), the test fails with a bare NullReferenceException on line 428 instead of showing the actual error object. Cheap to make the failure legible:
| JToken frame = JToken.Parse(response)["result"]!; | |
| JToken parsed = JToken.Parse(response); | |
| Assert.That(parsed["error"], Is.Null, "the trace must come back as a result, not a JSON-RPC error"); | |
| JToken frame = parsed["result"]!; |
| stateOverrides = JsonSerializer.Deserialize<object>( | ||
| $$$"""{"{{{RevertingContractAddress}}}":{"code":"{{{RevertingContractCode}}}"}}""") |
There was a problem hiding this comment.
Low (style) — RpcTest.BuildJsonRequest serializes each param by runtime type, so a plain dictionary works here and avoids the raw-string/triple-brace interpolation and the Deserialize<object> round-trip:
| stateOverrides = JsonSerializer.Deserialize<object>( | |
| $$$"""{"{{{RevertingContractAddress}}}":{"code":"{{{RevertingContractCode}}}"}}""") | |
| stateOverrides = new Dictionary<string, object> | |
| { | |
| [RevertingContractAddress] = new { code = RevertingContractCode } | |
| } |
System.Collections.Generic is already imported; System.Text.Json then becomes unused in this partial only if no other test needs it (it is used elsewhere in the file — leave the using).
| Assert.That(frame!.Type, Is.EqualTo(Instruction.CREATE)); | ||
| if (revert) | ||
| { | ||
| Assert.That(frame.Error, Is.Not.Null, "expected error description on reverted CREATE"); |
There was a problem hiding this comment.
Low (coverage) — the new to-omission rule fires on any halt, not just REVERT (MarkFrameFailed is keyed on error is not null, not on EvmExceptionType.Revert). Both new tests only exercise the revert flavour. A third [TestCase] here with init code that halts non-revertingly — e.g. Prepare.EvmCode.Op(Instruction.INVALID) — would pin the OutOfGas/BadInstruction path through MarkAsFailed, which is the one where output is empty and RevertReason stays null.
- 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>
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 3m 10s —— View job Re-review: omit
|
Changes
toon the root call frame when a top-levelCREATE/CREATE2fails — no contract was deployed.NativeCallTracerapplied this rule only to nested frames; geth runs the sameprocessOutputat depth 0.MarkFrameFailed) so the root and nested frames cannot drift apart again.debug_traceCallregression coverage for the callTracer revert contract: a revert is a traced result carryingerror/revertReason/output, not a JSON-RPC error.Conformance with the newly specified
callTraceroutput anddebug_traceCallin ethereum/execution-apis#855, whoseCallFrameschema statesto"MUST be present on all frames except CREATE and CREATE2 frames that failed, where it MUST be omitted since no contract was created".The base-fee item that PR's conformance table lists for Nethermind ("stop base-fee validation of
debug_traceCallcalls that carry no fee fields") was already fixed in #12804, after the 2026-08-25 run it reports. This was the remainingCallFramedivergence found when auditing the tracer anddebug_traceCallagainst all of the spec's normative rules and its 19 fixtures.Two divergences were found and deliberately left alone:
revertReasonis also populated forPanic(uint256)reverts, where the spec text saysError(string)only (geth omits it). Changing it would make our output strictly less useful and touches revert decoding shared witheth_call.onlyTopCall+withLog, geth attaches nested frames' logs to the root frame while we drop them. The spec is silent on the interaction and no fixture covers it.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Covered at both the tracer level (
GethLikeCallTracerTests.TopLevelCreate_Revert) and the RPC level (DebugRpcModuleTests.Debug_traceCall_with_callTracer_omits_to_on_failed_top_level_create,..._reports_revert_in_the_frame). Verified the CREATE test fails without the fix — the frame comes back with"to":"0x0ffd3e46…".Documentation
Requires documentation update
Requires explanation in Release Notes
🤖 Generated with Claude Code