Geth Closes JSON RPC Panics And Adds Snap Sync Metrics


go-ethereum is the Go Ethereum execution client that indexers, ETL jobs, and RPC gateways still treat as the default node. Master this week landed 14 commits (30 files, 455 insertions, 46 deletions). None of this is a protocol fork. It is panic fixes on JSON RPC call paths, snap sync gauges that used to live only in logs, and a JUMPDEST cache that billed 17 bytes while holding about 186.

Call tracing and simulation still have sharp edges. Two of them used to take the process down instead of returning an error.

eth_simulateV1 treated a difficulty override as a no-op after the Merge, except it did not. makeHeaders zeroes difficulty on merged blocks, then internal/ethapi/override/override.go copied a nonzero blockOverrides.difficulty back onto the header. NewEVMBlockContext leaves Random nil when difficulty is not zero, so the EVM selected pre Merge rules (Shanghai and PUSH0 off) while Prague history contract code still ran via chainConfig.IsPrague. ProcessParentBlockHash then panicked on PUSH0. The fix skips the override when the header already has zero difficulty.

// Difficulty is a no-op on post-merge (zero-difficulty) headers.
if o.Difficulty != nil && (h.Difficulty == nil || h.Difficulty.Sign() != 0) {
    h.Difficulty = o.Difficulty.ToInt()
}

The other crash is a missing to. CallDefaults now rejects blob and setcode args without a recipient with ErrBlobTxCreate and ErrSetCodeTxCreate. eth_simulateV1, debug_traceCall, and eth_createAccessList used to dereference a nil args.To inside ToTransaction. transaction_args.go already guarded setDefaults for this; CallDefaults did not. eth_call and eth_estimateGas stay unchanged.

Header reads also change shape. eth_getHeaderByNumber now returns JSON null for pending and for a safe or finalized tag that cannot be resolved. Before this it returned a pending header with hash, nonce, and miner nulled, and a -32000 error for unresolvable tags. Block methods are not touched. Anyone who parsed that stub pending header will see null instead.

debug_getModifiedAccounts used to swallow trie iterator errors. Iterator.Next returns false on both exhaustion and failure, so a missing node mid traversal looked like a complete account set. eth/api_debug.go now returns iter.Err.

Sync progress used to exist only as log lines. Two commits wire the same numbers into the metrics registry, which is what you want if the node is a Prometheus target rather than a tail source.

Chain segment download got a fetch and import breakdown in eth/downloader/metrics.go and the concurrent fetchers. Bodies, receipts, and block access lists each report idle, busy, and stale peers, estimated capacity, starved and throttled assignment rounds, plus item and byte histograms. Import wait, block insert, receipt insert, and batch size sit next to them. core/blockchain.go adds ancient store write time, sync time, and bytes for the snap path that dumps receipts into freezer tables.

A follow up exposes snap sync and heal progress as gauges. eth/downloader/downloader.go writes eth/downloader/chain/progress as synced / latest. The snap protocol side in eth/protocols/snap/metrics.go and sync.go reports state bytes, the remaining estimate, account / slot / code counts, heal trie counters, and snap/2 generation plus BAL catchup.

eth/downloader/chain/progress
eth/protocols/snap/sync/progress
eth/protocols/snap/sync/bytes
eth/downloader/bodies/peers/busy
chain/ancient/write

These gauges update from the same functions that print the “Syncing: …” logs. They are not sampled on a new interval. A 15s scrape will still miss an 8s stall. A dashboard can chart heal pending and ancient write time without scraping stdout.

JUMPDEST analysis is cached as a bitmap per contract hash. The LRU is byte bounded at 8 MiB per shard. Until this week it billed value bytes only.

The cache now charges a 150 byte per entry overhead on insert and refunds it on eviction, matching the precompile cache. A 100 byte contract produced a 17 byte bitmap and was billed as 17 bytes. The map entry itself was about 186 to 188 bytes, so a budget filled with small contracts silently held about 11 times its stated size. core/jumpdest.go now uses lru.NewSizeConstrainedCacheWithKeySize and jumpDestEntrySize(key) = len(key) + 150.

RSS should sit closer to the documented cap. The cache will hold fewer bitmaps, so JUMPDEST analysis will rerun more often on small contracts. The comment says eviction churn can reach about 1.5 times, so treat 8 MiB as a soft cap. There is no new flag.

Runtime histograms had a similar “zero looks like a max” bug. runtimehistogram.go seeds max from the first occupied bucket instead of leaving it at 0. A histogram whose samples are all negative used to report Max() == 0. Min already did this. Max did not.

A few smaller changes are worth a glance if you bind contracts or consume blocks through ethclient.

WatchEvents no longer dies on a mismatched log. External nodes and sloppy RPC proxies sometimes deliver a Sync log on a Swap subscription. UnpackLog returned ErrEventSignatureMismatch and the loop exited, taking the subscription with it. accounts/abi/bind/v2/lib.go and the abigen template now continue on that error. Real unpack failures still abort. The cost is silent drops if you were using the error as a signal that the node is on the wrong topic filter.

ethclient.getBlock already checked uncle and transaction lists against header roots. It now checks withdrawals the same way. An empty root with a nonempty list, or the reverse, is a client error instead of a block you later fail to hash.

The Cloudflare bn256 copy fixes twistPoint.Neg. Neg used to zero t instead of copying it, which broke pairing against negative G2 points. twist.go now copies t.

README.md states Go 1.25 as the minimum. go.mod is already go 1.25.0. The same file drops the claim that geth can run as a light node. LES has been gone for a while. The table was stale.

Header clients that called eth_getHeaderByNumber("pending") and expected a stub object need to handle JSON null. Block methods still return a pending block, so mixed header and block reads can disagree.

Snap sync dashboards can drop log parsers for the progress line and scrape eth/protocols/snap/sync/* instead. Add chain/ancient/write if freezer flush time is how you explain disk stalls during catchup.

Source builds should be on Go 1.25. The README and go.mod now agree. The JUMPDEST overhead charge needs no config, but if you track geth RSS against the old 8 MiB story, expect the process to look smaller and the cache hit rate to look worse on small contracts.