erigontech / erigontech/erigon

rpc: JSON-RPC divergences from geth and reth

Open
#23,733 4 comments 0 reactions 1 assignee Claimed by @lupin012 View on GitHub
RPC
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

Compared Erigon's full JSON-RPC surface (`eth`, `debug`, `net`, `web3`, `txpool`, `admin`, `engine`) against **go-ethereum v1.17.5** and **reth `66a7229`** (2026-09-02). Confirmed output divergences plus missing methods, tracers and namespaces. Tracking issue — each box can go in its own PR.

The geth comparison is below; the reth comparison follows after the separator.

# Geth (go-ethereum v1.17.5)

## Output divergences

- [x] **1. `callTracer` emits `to` on a failed CREATE** — `execution/tracing/tracers/native/call.go:88`
`callFrame.To` is a value `common.Address` with `json:"to,omitempty"`, and `omitempty` is a no-op on `[20]byte`, so `f.To = common.Address{}` still serializes `"to":"0x0000…0000"`. Geth uses `*common.Address` + `nil`, so the key is omitted. Our own fixtures pin the wrong output:
`tracers/internal/tracetest/testdata/call_tracer/inner_create_oog_outer_throw.json` and `inner_revert_reason.json` both have `"to": "0x000…0"` where geth's fixtures for the same txs have no `to`.
Fix: `To *common.Address` + regenerate `gen_callframe_json.go` + update the two fixtures.

- [x] **2. `callTracer` `withLog`: `logs[].index` is per-transaction** — `execution/tracing/tracers/native/call.go:278`
We reset `t.logIndex = 0` in `OnTxStart`. Geth reports `log.Index`, i.e. `StateDB.logSize`, which is not reset between txs in a block. On `debug_traceBlockByNumber`, if tx0 emits 3 logs, geth's first log of tx1 is `0x3` and ours is `0x0`.

- [x] **3. `callTracer` reports `error` where geth clears it** — `execution/tracing/tracers/native/call.go:76,80`
Geth's `processOutput(output, err, reverted)` nils `err` when `!reverted` (pre-Homestead contract-storage OOG), and `failed()` is `len(Error) > 0 && revertedSnapshot`. Our `processOutput` has no `reverted` parameter. Affects mainnet below block 1,150,000, and via `clearFailedLogs` it also changes which logs survive in `withLog` mode.

- [x] **4. `debug_traceTransaction` `limit` uses a different unit** — `execution/tracing/tracers/logger/json_stream.go:161`
Ours is a max opcode-step count; geth's is a max output size in bytes (`l.resultSize > l.cfg.Limit`). Same JSON key, very different result for `{"limit": 100}`.

- [ ] **5. `eth_syncing.startingBlock` is hardcoded to `0x0`** — `rpc/jsonrpc/eth_system.go:277`
Clients that compute progress as `(current - starting) / (highest - starting)` get wrong numbers.

- [ ] **6. `debug_storageRangeAt` iterates in a different order by default**
`--rpc.gethcompat` defaults to `false`, so entries come back in raw storage-key order instead of keccak order. `nextKey` differs too, so a geth-written paginating client walks a different sequence.

- [ ] **7. Pending dynamic-fee transactions report a different `gasPrice` than geth** — `computeGasPrice` in `rpc/ethapi/api.go`
Geth guards on `baseFee != nil && blockHash != (common.Hash{})`; we drop the blockHash half (the parameter is `_ common.Hash`). So for a tx still in the pool we return `min(tip + projectedNextBaseFee, feeCap)` where geth returns `maxFeePerGas`. Affects `eth_getTransactionByHash` and `txpool_content` for tx types 2/3/4.

- [ ] **8. `gasPrice` is missing entirely from `eth_subscribe("newPendingTransactions", true)`** — `rpc/jsonrpc/eth_filters.go:235`
That path passes `current = nil`, so `baseFee` is nil, `computeGasPrice` returns nil, and `RPCTransaction.GasPrice` carries `json:"gasPrice,omitempty"` — geth's has no `omitempty` and always emits the key. The same path also reports `blockTimestamp: "0x0"`.
Corroborated in-tree: `SignTransactionResult.MarshalJSON` (`rpc/ethapi/api.go:475`) already patches this up for `eth_fillTransaction`/`eth_signTransaction` by re-inserting `gasPrice`/`maxFeePerGas`/`maxPriorityFeePerGas` as `null` when the key is missing. Every other `RPCTransaction` consumer still drops it. Dropping `omitempty` from `RPCTransaction.GasPrice` fixes all of them and lets that workaround go — it currently costs a Marshal -> Unmarshal-to-map -> Marshal -> Marshal round trip.

## Missing methods

- [ ] `eth_getHeaderByNumber`, `eth_getHeaderByHash` — only exist as `erigon_*`, and return raw `types.Header` JSON (no `size`, `null`s instead of omitted fields) rather than geth's `RPCMarshalHeader` shape
- [ ] `eth_pendingTransactions`, `eth_resend`
- [ ] `txpool_inspect` — implementation exists but is commented out at `rpc/jsonrpc/txpool_api.go:195-230`
- [ ] `debug_traceBlock`, `debug_traceBlockFromFile`, `debug_traceChain`, `debug_traceBadBlock`, `debug_intermediateRoots`, `debug_standardTraceBlockToFile`, `debug_dumpBlock`, `debug_preimage`, `debug_chaindbProperty`, `debug_chaindbCompact`, `debug_printBlock`, `debug_getAccessibleState`, `debug_stateSize`

- [ ] `eth_accounts`, `eth_sign`, `eth_signTransaction`, `eth_sendTransaction` are registered but always error (`rpc/jsonrpc/eth_deprecated.go:30-43`, `rpc/jsonrpc/send_transaction.go:136`). Geth serves all four with an unlocked account or an external signer, so clients get an error rather than method-not-found and cannot tell the two cases apart.
- [ ] `eth_fillTransaction` ignores blob sidecars — `CallArgs` has no `blobs`/`commitments`/`proofs`; geth's `TransactionArgs` has them and builds the sidecar from them.

## Missing tracers

- [ ] `erc7562Tracer` (ERC-4337 bundler validation)
- [ ] `keccak256PreimageTracer`

## Other namespaces

### engine

- [ ] **Six non-spec methods are exposed on the engine endpoint.** `execution/engineapi/engine_server.go:168` registers `Service: EngineAPI(e)`, but `rpc/service.go:133` reflects over the *concrete* `*EngineServer`, so every exported method is registered. That publishes `engine_setBeaconChainConfig`, `engine_setConsuming`, `engine_start`, `engine_handleNewPayload`, `engine_handleForkChoice` and `engine_sSZRESTHandler`. Two of them mutate node state. Geth's `engine` namespace carries only spec methods.
- [ ] `engine_exchangeTransitionConfigurationV1` — deprecated in the spec, but geth still serves it for older CLs
- [ ] `engine_getBlobsV4` — geth has it (`eth/catalyst/api.go:722`), we stop at V3

### admin

- [ ] `admin_peerEvents`, `admin_startHTTP`/`admin_stopHTTP`, `admin_startWS`/`admin_stopWS`, `admin_startRPC`/`admin_stopRPC`, `admin_datadir`

### miner — namespace absent entirely

- [ ] `miner_setExtra`, `miner_setGasPrice`, `miner_setGasLimit`

### net

- [ ] `net_listening` returns `false` when the p2p backend is unreachable (`rpc/jsonrpc/net_api.go:49`); geth hardcodes `true`, so it never reports `false`.

## Checked, no divergence

`web3_*`; `txpool_content`/`contentFrom`/`status`; `net_version`/`net_peerCount`; `rpc_modules`; `eth_subscribe` `newHeads` and `logs` payloads (`types.Header` and `types.Log` JSON match geth field for field, except `slotNumber`, which we omit when nil and geth emits as `null`). Revert errors (code 3 + hex `data`), the `-38xxx` code table, `feeHistoryResult`, `RPCMarshalHeader`/`RPCMarshalBlock`, `MarshalReceipt`, `CallArgs`/`StateOverrides`/`BlockOverrides`, struct-log field set and hex formats, storage-key decoding in `eth_getStorageAt`.

Worth a second look, lower confidence: `prestateTracer.exists()` uses `CodeHash != nil` where geth uses `len(*Code) > 0` — should diverge for EIP-7702 deauth in `includeEmpty`/diff mode.

---

# Reth (`paradigmxyz/reth@66a7229`, 2026-09-02)

Method surface taken from the `#[method(name = ...)]` attributes in `crates/rpc/rpc-api`.

## Reth confirms the geth findings

- Item 1 (`to` emitted on a failed CREATE) diverges from reth as well: alloy's `CallFrame.to` is `Option

` with `skip_serializing_if = "Option::is_none"`, so reth omits the key exactly like geth. We are the only one of the three that emits `0x0000…0000`.
- `eth_getHeaderByNumber`/`eth_getHeaderByHash`, `eth_pendingTransactions`, `txpool_inspect`, `miner_setExtra`/`setGasPrice`/`setGasLimit` and most of the missing `debug_*` list are all present in reth too — so those are gaps against both clients, not geth quirks.

## eth — in reth, missing here (beyond the geth list)

- [ ] `eth_getAccount`, `eth_getAccountInfo`
- [ ] `eth_getMultiProof`
- [ ] `eth_getTransactionBySenderAndNonce` — we expose it only as `ots_getTransactionBySenderAndNonce`
- [ ] `eth_sendRawTransactionConditional`
- [ ] `eth_signTypedData`
- [ ] **BAL method naming diverges.** We expose `eth_getBlockAccessList(numberOrHash)` plus `debug_getRawBlockAccessList`; reth exposes `eth_getBlockAccessListByBlockHash`, `eth_getBlockAccessListByBlockNumber` and `eth_getBlockAccessListRaw`. EIP-7928 is still moving, so this needs a spec check before either side is called wrong.

## debug — in reth, missing here (beyond the geth list)

- [ ] `debug_codeByHash`, `debug_getRawTransactions`, `debug_accountInfoAt`, `debug_chainConfig`, `debug_seedHash`, `debug_stateRootWithUpdates`
- [ ] `debug_executionWitnessByBlockHash` — we have `debug_executionWitnesses` (plural, by range) instead

## engine — reth is ahead of us

- [ ] `engine_forkchoiceUpdatedV5`, `engine_newPayloadV6`, `engine_getBlobsV4`, `engine_getInclusionListV1`

## Other namespaces

- [ ] `ots_getHeaderByNumber` — reth has it, we do not, even though the Otterscan API originated here
- [ ] `trace_blockOpcodeGas`, `trace_transactionOpcodeGas` — reth extensions, not part of the OpenEthereum API
- [ ] `admin_banPeer`, `admin_unbanPeer`, `admin_clearTxpool` — reth-only; geth has none of these either, so lowest priority here

## Checked, no divergence

`net_*`, `web3_*`, `rpc_modules`, `txpool_content`/`contentFrom`/`status`.

Reth-only namespaces deliberately not chased: `anvil_`, `hardhat_`, `mev_`, `flashbots_`, `reth_`, `testing_`, and the bundle/private-transaction `eth_*` methods — dev and builder tooling rather than client compatibility.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.