`trace_call`: trace_call initializes the trace field as a non-null empty array even when trace was not requested, instead of returning null for every unrequested trace type
type:bug
## Summary
trace_call initializes the trace field as a non-null empty array even when trace was not requested, instead of returning null for every unrequested trace type.
The behavior was reproduced against **erigon/3.7.0/linux-arm64/go1.26.5**. The implementation cited below is pinned to commit [`993149925e45`](https://github.com/erigontech/erigon/commit/993149925e454e7f15113a11e78f48af4f23a7a9).
## Current documentation
The following wording was still present in the official documentation on **2026-08-16**:
**`trace_call`** — [official documentation](https://docs.erigon.tech/interacting-with-erigon/trace)
> `Object` containing `output`, `stateDiff`, `trace[]`, `vmTrace`. Each requested trace type is populated; the others are `null`. See [Response Fields Reference](#response-fields-reference) for full field semantics.
## What the endpoint does instead
| Method | Documented behavior | Runtime-confirmed behavior |
|---|---|---|
| `trace_call` | Trace types in the returned object that are not requested are `null`. | Judge C1 and the runtime plan align on a stateDiff-only request exposing an unrequested trace as [] rather than null. |
## Relevant implementation
**1. [`rpc/jsonrpc/trace_adhoc.go:1147-1164`](https://github.com/erigontech/erigon/blob/993149925e454e7f15113a11e78f48af4f23a7a9/rpc/jsonrpc/trace_adhoc.go#L1147-L1164) — `TraceAPIImpl.Call`**
```go
defer cleanup()
traceResult := &TraceCallResult{Trace: []*ParityTrace{}}
var traceTypeTrace, traceTypeStateDiff, traceTypeVmTrace bool
for _, traceType := range traceTypes {
switch traceType {
case TraceTypeTrace:
traceTypeTrace = true
case TraceTypeStateDiff:
traceTypeStateDiff = true
case TraceTypeVmTrace:
traceTypeVmTrace = true
default:
return nil, fmt.Errorf("unrecognized trace type: %s", traceType)
}
}
if traceTypeVmTrace {
traceResult.VmTrace = &VmTrace{Ops: []*VmTraceOp{}}
```
**2. [`rpc/jsonrpc/trace_adhoc.go:269-283`](https://github.com/erigontech/erigon/blob/993149925e454e7f15113a11e78f48af4f23a7a9/rpc/jsonrpc/trace_adhoc.go#L269-L283) — `parseOeTracerConfig`**
```go
func parseOeTracerConfig(traceConfig *config.TraceConfig) (OeTracerConfig, error) {
if traceConfig != nil && traceConfig.Tracer != nil && *traceConfig.Tracer != "" {
return OeTracerConfig{}, errors.New("trace_* does not support custom tracers; use debug_* (e.g. debug_traceTransaction) for named or JS tracers")
}
if traceConfig == nil || traceConfig.TracerConfig == nil || *traceConfig.TracerConfig == nil {
return OeTracerConfig{}, nil
}
var config OeTracerConfig
if err := json.Unmarshal(*traceConfig.TracerConfig, &config); err != nil {
return OeTracerConfig{}, err
}
return config, nil
}
```
## Reproduction
A live trace_call can request only stateDiff and expose the unrequested trace field. The documentation predicts null, while the implementation initializes Trace as a non-nil slice and therefore predicts an array.
Prepare the disposable dev node as follows:
1. From the funded dev account, deploy creation bytecode `0x6001600c60003960016000f300` (runtime executes a single STOP and touches no storage); refer to the deployed address as `{{stop_target.address}}`.
Bash-compatible JSON-RPC probes are shown below. Replace template values such as `{{tx_hash}}` with values produced by the setup before running them:
```bash
#!/usr/bin/env bash
set -euo pipefail
RPC_URL="${RPC_URL:-http://127.0.0.1:8545}"
echo '=== subject: trace_call ==='
curl -sS -H 'Content-Type: application/json' --data @- "$RPC_URL" <<'JSON'
{
"jsonrpc": "2.0",
"id": 1,
"method": "trace_call",
"params": [
{
"from": "{{dev}}",
"to": "{{stop_target.address}}",
"data": "0x"
},
[
"stateDiff"
]
]
}
JSON
echo
echo '=== control: eth_getCode ==='
curl -sS -H 'Content-Type: application/json' --data @- "$RPC_URL" <<'JSON'
{
"jsonrpc": "2.0",
"id": 2,
"method": "eth_getCode",
"params": [
"{{stop_target.address}}",
"latest"
]
}
JSON
echo
echo '=== control: trace_call ==='
curl -sS -H 'Content-Type: application/json' --data @- "$RPC_URL" <<'JSON'
{
"jsonrpc": "2.0",
"id": 3,
"method": "trace_call",
"params": [
{
"from": "{{dev}}",
"to": "{{stop_target.address}}",
"data": "0x"
},
[
"trace"
]
]
}
JSON
echo
```
The reproduction tests this documented clause:
> Each requested trace type is populated; the others are `null`.
## Observed behavior
The observed response matched the implementation-side prediction:
- `response`: `no_error`
- `$.result.stateDiff`: `jsonpath_exists`
- `$.result.stateDiff`: `jsonpath_type` — `{"type": "object"}`
- `$.result.trace`: `jsonpath_exists`
- `$.result.trace`: `jsonpath_type` — `{"type": "array"}`
The documentation-side prediction differed as follows:
- `response`: `no_error`
- `$.result.stateDiff`: `jsonpath_exists`
- `$.result.stateDiff`: `jsonpath_type` — `{"type": "object"}`
- `$.result.trace`: `jsonpath_exists`
- `$.result.trace`: `jsonpath_type` — `{"type": "null"}`
Representative subject response:
```json
{
"result": {
"output": "0x",
"stateDiff": {
"0x71562b71999873db5b286df957af199ec94617f7": {
"balance": "=",
"code": "=",
"nonce": {
"*": {
"from": "0x1",
"to": "0x2"
}
},
"storage": {}
}
},
"trace": [
{
"action": {
"from": "0x71562b71999873db5b286df957af199ec94617f7",
"callType": "call",
"gas": "0x2fa9e78",
"input": "0x",
"to": "0x3a220f351252089d385b29beca14e27f204c296a",
"value": "0x0"
},
"result": {
"gasUsed": "0x0",
"output": "0x"
},
"subtraces": 0,
"traceAddress": [],
"type": "call"
}
],
"vmTrace": null
},
"transport": "http"
}
```
## Expected behavior
The public contract and endpoint behavior should agree. In particular:
- `trace_call` should satisfy the documented behavior: Trace types in the returned object that are not requested are `null`.
## Impact
Tracing and debugging clients that follow the documentation can reject valid responses, read the wrong field, or draw an incorrect conclusion about the executed operation.
0 条评论