Skip to content

rpcdaemon: add eth_getHeaderByHash and eth_getHeaderByNumber - #23717

Merged
yperbasis merged 8 commits into
erigontech:mainfrom
MysticRyuujin:eth-getheader-methods
Sep 16, 2026
Merged

yperbasis merged 8 commits into
erigontech:mainfrom
MysticRyuujin:eth-getheader-methods

Conversation

@MysticRyuujin

Copy link
Copy Markdown
Contributor

Adds eth_getHeaderByHash and eth_getHeaderByNumber, implementing the spec proposed in ethereum/execution-apis#877 (ethereum/execution-apis#874). geth, Nethermind, and reth already serve these methods; Erigon has them only in the erigon_ namespace.

The new methods do not reuse the erigon_getHeaderBy* path because its types.Header JSON marshaling emits pre-fork fields as literal null and returns errors for unknown blocks. They marshal through ethapi.RPCMarshalHeader, which fork-gates optional fields, and return null for an unknown block, for the pending tag, and for an unresolvable safe or finalized tag.

RPCMarshalHeader no longer sets size. Its only caller overwrites the value with the block size, and the header methods must not emit it: the spec excludes size because no client computes a header size and the block getters already report the block size.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolvable safe and finalized tags lack regression coverage, and a method comment is misplaced.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds standard Ethereum header lookup RPC methods with specification-aligned null and field behavior.

Changes:

  • Adds eth_getHeaderByNumber and eth_getHeaderByHash.
  • Marshals fork-gated header fields without block-only size.
  • Adds endpoint tests, documentation, and slow-log exclusions.
File summaries
File Description
rpc/rpccfg/rpccfg.go Adds header methods to the slow-log blacklist.
rpc/jsonrpc/eth_block.go Implements both header endpoints.
rpc/jsonrpc/eth_block_test.go Tests header lookup and response fields.
rpc/jsonrpc/eth_api.go Exposes methods through EthAPI.
rpc/ethapi/api.go Removes header size output.
cmd/rpcdaemon/README.md Documents endpoint availability.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread rpc/jsonrpc/eth_block_test.go
Comment thread rpc/jsonrpc/eth_block.go
eth_getHeaderByNumber and eth_getHeaderByHash opened a raw BeginTemporalRo
and let headerBy* select the overlay afterwards. A publish/commit/unpublish
cycle landing between the two steps leaves the head block in neither layer,
and the unknown-block branch then reports it as null.

Use BeginTemporalRoWithOverlay like every sibling endpoint, and add both
methods to the overlay-race and prune-gating tables.
GetHeaderByNumber turns an unresolvable fork-choice marker into a null
result through rpchelper.UnknownBlockCode. The test module configures both
markers, so only the resolvable path was exercised.
@yperbasis yperbasis added this to the 3.7.0 milestone Sep 14, 2026
@AskAlexSharov
AskAlexSharov added this pull request to the merge queue Sep 14, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 14, 2026
Sahil-4555 pushed a commit to Sahil-4555/erigon that referenced this pull request Sep 14, 2026
…ch#23943)

`RPCMarshalHeader` and `RPCMarshalBlock` built a `map[string]any`. That
boxes every value into an interface, hashes the map 19 times per header,
and then makes `encoding/json` sort the keys and walk the reflect path
per entry. `RPCMarshalBlock` now returns `*RPCBlock`. The header builder
became the internal `rpcMarshalHeader`, which returns `*RPCHeader` and
takes the hash; nothing exported returns `*RPCHeader`, so erigontech#23717 needs
to export it when it rebases onto this.

`RPCMarshalBlock` and `RPCMarshalBlockEx` also took an `additional
map[string]any` that was merged into the result. Every caller passed
`nil` or a freshly made map that nothing ever wrote to, so the parameter
and the two wrappers are gone; `rpc/ethapi/internal.go` is deleted.

## The JSON is unchanged

`TestRPCMarshalHeaderMatchesLegacyJSON` and
`TestRPCMarshalBlockMatchesLegacyJSON` keep the old map builders as an
oracle and compare the encoded objects field by field. The header sweep
covers all 256 combinations of the optional fields, times the three
shapes of `extraData` and of the Gnosis AuRa seal (nil, empty, set),
times zero and non-zero values — 4608 headers. The block sweep covers
withdrawals nil / empty / set, uncles, and each `inclTx`/`fullTx` pair.

The empty-but-not-nil cases are the ones that matter. A plain
`omitempty` on `withdrawals` drops `"withdrawals":[]`, which is what
most post-Shanghai blocks carry, and the same trap applies to
`auraSeal`. Both fields use a pointer so the omission tracks nil exactly
as the map did. Both traps were confirmed by making them and watching
the oracle fail.

Key order does change: a map encodes alphabetically, a struct in
declaration order. JSON objects are unordered, so no client should care.

## Numbers

n5, go1.27.1, `-cpu=1 -count=15`, benchstat over the oracle vs the new
type in the same binary:

| case | map | struct | delta | map allocs | struct allocs |
|---|---|---|---|---|---|
| header, london | 13.6µs | 4.4µs | -67.9% | 74 | 4 |
| header, prague | 16.6µs | 5.3µs | -68.3% | 89 | 4 |
| block, 0 txs | 22.4µs | 9.5µs | -57.5% | 114 | 17 |
| block, 1 tx | 26.6µs | 13.4µs | -49.7% | 123 | 26 |
| block, 200 txs | 605µs | 585µs | -3.3% | 1715 | 1618 |

A 200-tx block is dominated by the transactions, so the header saving is
most of a percent there; the header endpoints erigontech#23717 adds get the full
win.

`transactions` is typed `any` rather than `[]any` on purpose. Under
go1.27 a `[]any` struct field costs about two allocations per element
where the same slice reached through an `any` costs one, so the typed
field would have made large blocks worse. A properly typed transaction
slice is worth a follow-up: the same microbenchmark drops from 204
allocations to 2.
@MysticRyuujin

Copy link
Copy Markdown
Contributor Author

Do ya'll need me to fix the conflicts?

main now returns typed structs from the block and header marshalers.
Export RPCMarshalHeader and return *ethapi.RPCHeader from
eth_getHeaderByNumber and eth_getHeaderByHash. RPCHeader has no size,
totalDifficulty or transactions fields, so the tests no longer check
for those keys.
@AskAlexSharov

Copy link
Copy Markdown
Collaborator

No need, I merged main and resolved the conflicts. main now returns typed structs from the block and header marshalers (#23943), so RPCMarshalHeader is exported again and both header endpoints return *ethapi.RPCHeader.

@yperbasis
yperbasis added this pull request to the merge queue Sep 16, 2026
Merged via the queue into erigontech:main with commit a033581 Sep 16, 2026
137 checks passed
Sahil-4555 pushed a commit to Sahil-4555/erigon that referenced this pull request Sep 18, 2026
…ontech#24094)

QA integration tests for `eth_getHeaderByNumber` and
`eth_getHeaderByHash`, the two APIs added in erigontech#23717.

The fixtures live in erigontech/rpc-tests#603; this PR only pins
`RPC_VERSION` to that branch so the QA workflows pick them up.

### `eth_getHeaderByNumber`

| test | param | case |
|---|---|---|
| 01 | `0x41b57c` | pre-London: no `baseFeePerGas`, no post-Merge fields
|
| 02 | `0xC65D40` | London: `baseFeePerGas` set, `difficulty` still
non-zero |
| 03 | `0x1036640` | post-Merge pre-Shanghai: `difficulty` `0x0`, no
`withdrawalsRoot` |
| 04 | `0x112A880` | Shanghai: `withdrawalsRoot` set, no blob fields |
| 05 | `0x12C135B` | Cancun: `blobGasUsed`, `excessBlobGas`,
`parentBeaconBlockRoot` |
| 06 | `0x1600000` | Prague: `requestsHash` |
| 07 | `earliest` | resolves to the genesis header |
| 08 | `latest` | head header (`metadata.latest`, runs only with `-L`) |
| 09 | `pending` | `null`, the pending block is not exposed |
| 10 | `0x7FFFFFFFFFFFFF` | unknown number: `null`, not an error |
| 11 | `abc` | invalid number: hex without `0x` prefix, error |

### `eth_getHeaderByHash`

| test | case |
|---|---|
| 01 | pre-London: no `baseFeePerGas`, no post-Merge fields |
| 02 | London: `baseFeePerGas` set, `difficulty` still non-zero |
| 03 | post-Merge pre-Shanghai: `difficulty` `0x0`, no `withdrawalsRoot`
|
| 04 | Shanghai: `withdrawalsRoot` set, no blob fields |
| 05 | Cancun: `blobGasUsed`, `excessBlobGas`, `parentBeaconBlockRoot` |
| 06 | Prague: `requestsHash` |
| 07 | genesis block hash |
| 08 | unknown hash: `null`, not an error |
| 09 | invalid hash: wrong length, error |
| 10 | invalid hash: not a string, error |

The same blocks are used by number and by hash, so the two APIs are
compared on identical headers.

###  `Integration-Tests `
Compared eth_getHeaderByNumber() and eth_getHeaderByHash() for
historical block automatically on QA - RPC Integration Tests /
mainnet-rpc-integ-tests
Compared eth_getHeaderByNumber() output with geth on the latest block —
results match. Dispatched manually: CI run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants