# Method Handlers > Source: https://docs.erpc.cloud/reference/evm/method-handlers > eRPC's per-method EVM hooks eliminate entire classes of provider inconsistency — stale blocks, duplicate broadcasts, range-exceeded traces — before your application ever sees them. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # Method Handlers Every EVM node lies a little differently: one returns a block behind on `eth_blockNumber`, another rejects `eth_sendRawTransaction` as "already known" after a successful broadcast, a third chokes on a wide `trace_filter`. eRPC intercepts those inconsistencies before they reach your application — normalizing block tags for cache correctness, making transaction broadcast idempotent across retries, and auto-splitting trace ranges that upstreams can't handle. ## Quick taste Illustrative, not a tuned production config — idempotent broadcast and reactive trace splitting enabled: **Config path:** `projects[].networks[].evm` **YAML — `erpc.yaml`:** ```yaml projects: - id: main networks: - architecture: evm evm: chainId: 1 # "already known" / "nonce too low" replies become synthetic success idempotentTransactionBroadcast: true # reactive bisection when an upstream rejects a trace_filter range traceFilterSplitOnError: true ``` **TypeScript — `erpc.ts`:** ```typescript projects: [{ id: "main", networks: [{ architecture: "evm", evm: { chainId: 1, // "already known" / "nonce too low" replies become synthetic success idempotentTransactionBroadcast: true, // reactive bisection when an upstream rejects a trace_filter range traceFilterSplitOnError: true, }, }], }] ``` ## Agent reference Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly: **Prompt Example #1: make eth_sendRawTransaction retries safe and idempotent** ```text My wallet app retries eth_sendRawTransaction on timeout but sometimes gets "already known" or "nonce too low" errors that confuse users. Configure eRPC in my eRPC config to make broadcast idempotent — "already known" should become a synthetic success, and "nonce too low" should probe whether the same tx is already in-chain before deciding. Read the full reference: https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt ``` **Prompt Example #2: stop stale block numbers from reaching my app** ```text Some of my upstreams occasionally return a block number that is one block behind the network head. Configure eRPC in my eRPC config so that eth_blockNumber and eth_getBlockByNumber always return the highest confirmed block across all upstreams, and explain which integrity flags control this behavior. Reference: https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt ``` **Prompt Example #3: enable trace_filter auto-splitting for wide block ranges** ```text My trace pipeline sends trace_filter requests spanning thousands of blocks and some upstreams reject them with range-too-large errors. Set up proactive splitting per upstream and reactive bisection as a fallback in my eRPC config. Also tell me which metric to watch for split failures so I can tune concurrency. Reference: https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt ``` **Prompt Example #4: debug why my markEmptyAsErrorMethods customization is wrong** ```text I added a custom markEmptyAsErrorMethods list in my eRPC config but now some methods that returned empty arrays correctly are getting retried in a loop. Explain the footgun (custom list fully replaces defaults, not merges), show me the default list, and help me rebuild a correct combined list. Reference: https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt ``` --- ### Method handlers — full agent reference ### How it works **Hook topology.** Five hook layers fire at specific points in the request lifecycle, ordered around the cache read and the failsafe retry loop: 1. **Project.PreForward** (`HandleProjectPreForward`, `architecture/evm/hooks.go:L10`) — fires before cache read. Answers the [local-answer methods](#local-answers), handles `eth_blockNumber` (returns highest known, replaces real upstream response), `eth_call` and `eth_estimateGas` (inject missing block param), and records `trace_filter`/`arbtrace_filter` range histogram. 2. **Network.PreForward** (`HandleNetworkPreForward`, `architecture/evm/hooks.go:L40`) — fires after upstream selection. Handles proactive `trace_filter`/`arbtrace_filter` auto-splitting when the request range exceeds the per-upstream threshold. 3. **Upstream.PreForward** (`HandleUpstreamPreForward`, `architecture/evm/hooks.go:L86`) — fires once per upstream attempt. Handles `eth_getLogs`/`trace_filter` block-range availability. 4. **Upstream.PostForward** (`HandleUpstreamPostForward`, `architecture/evm/hooks.go:L109`) — fires after each upstream response. Handles `eth_getBlockByNumber`/`eth_getBlockByHash` block validation, `eth_getBlockReceipts` integrity checks, and `eth_sendRawTransaction` nonce-exception interception. Also applies the configurable `markEmptyAsErrorMethods` gate to convert null/empty results to retryable `ErrEndpointMissingData`. Conversion only fires when the `RetryEmpty` directive is `true` on the request; when `RetryEmpty=false`, null/empty passes through unchanged even for methods in the list. 5. **Network.PostForward** (`HandleNetworkPostForward`, `architecture/evm/hooks.go:L62`) — fires once after the failsafe loop. Handles `eth_getBlockByNumber` highest-block enforcement, `eth_sendRawTransaction` exhausted-broadcast recovery, and reactive `trace_filter` splitting. **Block-tag interpolation.** `NormalizeHttpJsonRpc` (`architecture/evm/json_rpc.go:L92`) runs on every request before upstream selection. It resolves `"latest"` to the highest known block number and `"finalized"` to the highest known finalized block number (when available), normalizes numeric hex to canonical form, caches the result in the request for downstream cache-key generation, and sets `EvmBlockRef` metadata (`"latest"`, `"finalized"`, or `"*"` when both appear) for routing and observability. `"safe"` and `"pending"` pass through unchanged. When a `ReqRefs` path points to a map/object param (rather than a scalar block tag), the function skips replacing that param — this prevents accidentally replacing a complex call-data object with a hex string. Resolution is lock-efficient: param values are extracted under a read lock, resolved outside the lock, then deep-copy + write-commit only occur when a change is actually needed. The `SkipInterpolation` directive and per-method `translateLatestTag`/`translateFinalizedTag` flags can disable interpolation on a per-request or per-method basis. **Future/missing block guard (`emptyResultBeyondConfidence`).** When an upstream returns null/empty for a numeric block request, `emptyResultBeyondConfidence` (`architecture/evm/common.go:L62`) compares the request's cached block number against the network's confidence head (`EvmHighestLatestBlockNumber` by default; `EvmHighestFinalizedBlockNumber` when `emptyResultConfidence: finalizedBlock`). If the requested block is strictly above the head, the result is truthful — no retry is triggered. If the head is unknown (0) the guard fails open: any block is treated as retryable. This prevents retry storms for not-yet-produced future blocks while preserving normal retry behavior for stale nodes. **eth_call / eth_estimateGas missing-block-param injection.** If the request arrives with exactly one parameter (the call object, no block tag), the project pre-forward hook appends `"latest"` as the second param before forwarding; interpolation then pins it to the network's latest block number, so no upstream applies its own default block. `eth_call` is intentionally excluded from `markEmptyAsErrorMethods` and from structured error wrapping — callers expect raw ABI revert data (a hex string), not a structured error object. #### Local answers eRPC answers these methods itself, before cache and upstream selection, because the true answer does not depend on which upstream serves it, or is about eRPC, the node the client talks to. Routing them made one upstream a single point of failure for a constant (upstreams often reject `net_*` methods) or returned one node's view where eRPC fronts several. | Method | eRPC's answer | Routed instead when | |---|---|---| | `eth_chainId` | `networks[].evm.chainId` as hex | chain id not configured | | `net_version` | `networks[].evm.chainId` as a decimal string | chain id not configured | | `net_listening` | `true` | never | | `net_peerCount` | the number of upstreams eRPC can route the request to now (the selection policy's eligible set, narrowed by `use-upstream`), as hex | never | | `web3_clientVersion` | `erpc//-/` | never | | `eth_syncing` | `false` while one routable upstream is synced | no routable upstream is known synced | Every one is on by default. `networks[].evm.localAnswers` turns one off per network: `{net_peerCount: false}` routes `net_peerCount` to the upstreams as any other method, with no eRPC answer. `networkDefaults.evm.localAnswers` applies to each network per method, unless the network sets that method itself. A key that is not in the table fails config validation. A request with `x-erpc-skip-cache-read: true` is routed too, so a force-refresh can read an upstream's own answer. Source: [`common/evm_local_answers.go`](https://github.com/erpc/erpc/blob/main/common/evm_local_answers.go), [`architecture/evm/local_answer.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/local_answer.go) **eth_blockNumber highest-block enforcement.** With the `enforceHighestBlock` directive on (`directiveDefaults.enforceHighestBlock`, default `true`, overridable per request), the network pre-forward hook answers from the served tip without dispatching and the post-forward hook pins every response to it; refresh, freshness and pin rules: [Block tracking](/reference/evm/block-tracking.llms.txt#eth_blocknumber). **eth_getBlockByNumber and eth_getBlockByHash.** Network.PostForward runs two checks: - **`enforceHighestBlock`:** For `"latest"` or `"finalized"` tagged requests, if the response block number is behind the network's known highest, eRPC re-issues the request for that block number, fetches the newer block, and returns whichever of the two is higher via `pickHighestBlock`. The upstream that answered stale stays eligible for the re-fetch: on fast chains it is often the one whose own feed just delivered the tip. Increments `erpc_upstream_stale_latest_block_total` or `erpc_upstream_stale_finalized_block_total`. - **`enforceNonNullBlock`:** Converts null/empty results to `ErrEndpointMissingData`. For numeric block params this always applies. For tag params it only applies when `enforceNonNullTaggedBlocks` is true. Both branches are guarded by `emptyResultBeyondConfidence`. Upstream.PostForward runs opt-in block validation when directives are set: `ValidateTransactionsRoot`, `ValidateHeaderFieldLengths`, `ValidateTransactionFields`, and `ValidateTransactionBlockInfo`. Phantom/system transactions are handled specially — their blocks legitimately have `transactionsRoot = emptyTrieRoot` despite a non-empty transactions array. Recognized conventions: Polygon PoS / BSC (`from=0x0`, `gas=0x0`) and HyperEVM native/L1 system txs (synthetic signature `r=0x1` and `gasPrice=0`). **eth_getBlockReceipts.** The upstream post-forward hook runs the opt-in [data-integrity module](/config/failsafe/integrity.llms.txt) — when enabled via `networks[].integrity`, its receipt checks (schema, same-block-hash, tx-hash uniqueness, transaction-index consistency, log field shapes/metadata, bloom emptiness/match, log-index contiguity, and — at the authoritative level — receipts-root recompute and receipt-vs-canonical-block corroboration). With no `integrity` config nothing runs. `eth_getBlockReceipts` is intentionally NOT in the default `markEmptyAsErrorMethods` list — empty arrays are the correct response for zero-transaction blocks. **eth_sendRawTransaction idempotency.** eRPC makes `eth_sendRawTransaction` idempotent across retries and failsafe exhaustion. Upstream-level (nonce exception handling): when an upstream returns `ErrCodeEndpointNonceException`: - `already_known` (`NonceExceptionReasonAlreadyKnown`, `common/errors.go:L2754`) → synthesize success with the locally-derived tx hash immediately (no probe). Matched message patterns: `"already known"`, `"known transaction"`, `"already imported"`, `"transaction already in mempool"`, `"tx already in mempool"`, `"already in the mempool"`, `"transaction already exists"`, `"already have transaction"`, `"already exists in mempool"`. Source: [`architecture/evm/error_normalizer.go:L304-L323`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L304-L323). - `nonce_too_low` (`NonceExceptionReasonNonceTooLow`, `common/errors.go:L2756`) → probe `eth_getTransactionByHash` on the same upstream. If the same tx is found (hash matches), return synthetic success. If a different tx with that nonce is found, re-raise as `-32003`. If probe itself errors, the original error is returned unchanged. Matched message patterns: `"nonce too low"`, `"nonce is too low"`, `"nonce has already been used"`. Source: [`architecture/evm/error_normalizer.go:L326-L340`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L326-L340). `ErrEndpointNonceException` always carries `retryableTowardNetwork=false` (`common/errors.go:L2773`). This means N:no: if the upstream-level idempotency hook does not intercept (e.g., `idempotentTransactionBroadcast: false`), the error will NOT be retried on another upstream. This is intentional — once "already known" is established, retrying on another upstream would either echo the same result or erroneously succeed. Network-level (exhausted broadcast recovery): after the failsafe loop exhausts all attempts (`ErrCodeUpstreamsExhausted`, `ErrCodeFailsafeRetryExceeded`, or `ErrCodeFailsafeTimeoutExceeded`), eRPC probes `eth_getTransactionByHash` across the network using `context.WithoutCancel` and a 3-second independent timeout — so the probe runs even when the parent context is already expired. The returned tx hash is cross-checked against the locally-derived hash before granting synthetic success; a mismatch or missing hash field returns the original error. Tx hash extraction supports legacy (type-0 RLP) and EIP-2718 typed transactions (EIP-1559, EIP-2930) via `ethtypes.Transaction.UnmarshalBinary`. Error normalizer N:yes override for `eth_sendRawTransaction`: revert/VM-execution errors and out-of-gas/-32003 errors are flipped to `retryableTowardNetwork=true` (different providers may accept a tx that another rejects). Insufficient-funds errors remain `N:no` — they are deterministic. The nonce detection rules in the error normalizer MUST appear before the generic `-32003` handler. Some vendors use `-32003` for "already known" or "nonce too low" messages; if the generic rule fired first, these would bypass the idempotency machinery entirely. Source: [`architecture/evm/error_normalizer.go:L297-L301`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L297-L301). **trace_filter and arbtrace_filter auto-splitting.** Two splitting modes coexist: Proactive splitting (Network.PreForward): if the request's block range exceeds `min(TraceFilterAutoSplittingRangeThreshold)` across selected upstreams, the request is immediately split into contiguous chunks and dispatched concurrently. Re-entrancy is blocked via `ParentRequestId`. Reactive splitting (Network.PostForward): if an upstream returns a range-too-large error (`ErrCodeEndpointRequestTooLarge` or `JsonRpcErrorEvmLargeRange`) and `traceFilterSplitOnError: true`, eRPC bisects the request — halving block range first (splitting at midpoint `fromBlock + (range/2)`), then `fromAddress` list, then `toAddress` list. Returns an error if a single-block + single-address request still triggers the error. Both proactive and reactive splitting also reject `fromBlock > toBlock` immediately with `-32600` (InvalidRequest) before dispatching sub-requests. Sub-results are concatenated in index order with no deduplication. Any single sub-failure fails the entire merged response. The upstream post-forward hook normalizes `null`/`""`/`{}` results to `[]` — some older Parity/OpenEthereum nodes return `null` for empty ranges. The effective proactive-split threshold is `min` across all upstreams with a positive `TraceFilterAutoSplittingRangeThreshold`. Upstreams with threshold=0 are ignored. If ALL upstreams have threshold=0, no proactive splitting occurs. Concurrency is capped by `traceFilterSplitConcurrency` (default 10). Source: [`architecture/evm/trace_filter.go:L175-L187`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L175-L187). **eth_query\* (Monad MIP-16).** Five methods — `eth_queryBlocks`, `eth_queryTransactions`, `eth_queryLogs`, `eth_queryTraces`, `eth_queryTransfers` — follow [MIP-16](https://github.com/monad-crypto/MIPs/pull/89). One executor (`erpc/query_executor.go`) answers them for JSON-RPC and for the gRPC `bds.evm.QueryService`. The request and response codec is the manifesto's: `evm.Query*RequestFromJsonRpc` parses `params` strictly (one request object; unknown keys, unknown fields, empty `fields` arrays, `target: "0x0"`, bad filter values → `-32602`), and `evm.Query*ResponseToJsonRpc` renders exactly the selected fields. Monad chains (143, 10143) use the MIP-16 Appendix 1 schema; other chains accept every field of the proto selections. JSON-RPC entry: `Network.Forward` hands an `eth_query*` request to the executor right after static responses and safe-block routing, before multiplexing, cache and per-upstream dispatch. The HTTP server's auth, project rate limit and project `allowMethods`/`ignoreMethods` run first like for any request; the network rate limit is taken in `forwardQuery`; `HandleNetworkPostForward` and the project metrics run on the result. One JSON call returns one page. Execution order: 1. **Range.** Omitted `fromBlock` is `earliest` (asc) or `latest` (desc); omitted `toBlock` is `latest` (asc) or `earliest` (desc). Each tag resolves once per request (`latest` → `EvmHighestLatestBlockNumber`, `finalized` → `EvmHighestFinalizedBlockNumber`, `safe` → finalized else latest, `earliest` → `0`), so two equal tags name the same block. In `desc` order `fromBlock` is the upper bound. An inverted range fails with `-32602` (`evm.ValidateQueryRange`); bounds are never swapped. 2. **Method served.** No native `QueryService` upstream that allows the method and no upstream whose `evm.queryShim` allows it → `-32004`. 3. **Availability.** A range end above the network's highest known block, or a block outside the union of the availability windows (`blockAvailability` bounds) of the upstreams that can serve the method natively or through the shim sub-requests → `-32001`, before any scan. For the shim, an upstream counts only when its method allow/ignore config accepts every sub-request the request will issue (`ShouldHandleMethod` returns true without an error): `eth_getBlockByNumber` always, plus `eth_getLogs` for `eth_queryLogs`, `trace_block` or `debug_traceBlockByNumber` for traces and transfers, and `eth_getBlockReceipts` when the selection reads receipt fields; none counting → `-32004`. 4. **Native pipe-through.** Native upstreams are tried in selection-policy order with the MIP-16 proto request unchanged. An upstream that fails before its first page (for example `UNIMPLEMENTED` for `eth_queryTraces` on a reader without traces) falls through to the next one, then to the shim. After a page went out, a stream error ends the request: no other upstream continues it. A JSON call takes the first page and cancels the stream; that page's `cursorBlock` is the page end. 5. **Shim.** The shim builds pages from `eth_getBlockByNumber`, `eth_getBlockReceipts`, `eth_getLogs` and `trace_block` / `debug_traceBlockByNumber` sub-requests, each sent through `Network.Forward` (cache, failsafe, upstream selection) with the request id as parent id. The sub-requests of one page prefer the upstream that served the first one (`UseUpstream` set to its id), for methods that upstream accepts (`ShouldHandleMethod` true without an error); others go unpinned. A pinned sub-request that fails is sent once more unpinned, and one answered with `null` (a lagging upstream that does not have the block yet) is asked once more of every other upstream that takes the method (`!`), so a lagging upstream never turns an available block into `-32001`; that answer stands, an error included, except an error saying the block is missing (`ErrEndpointMissingData`, `ErrUpstreamBlockUnavailable`), which keeps the `null` and so `-32001`. The upstream that answers with data becomes the page's pin; a pin that answered `null` where no other upstream had data is dropped, so later sub-requests of the page go unpinned. A sub-request no upstream serves (`eth_getBlockReceipts`, or both trace methods) fails with `-32004`. **Shim pages.** A page scans blocks from its `fromBlock` in traversal order, in chunks fetched concurrently (4, doubling to 64; 16 sub-requests in flight), and ends at the first of: `toBlock`; the block that brings the primary count to `target` or more (the whole block is in the page); the budget; a break in chain consistency. A block is complete once its header, its primary objects and the transactions they join (with receipts when selected) are fetched, all of it inside the page's time budget, so the budget bounds every sub-request of the page. The budget is `queryShim.maxBlocksPerPage` blocks (default 1000) and `queryShim.maxPageDuration` (default `10s`), the smallest across the upstreams that enable the shim for the method. A page that hits the time budget ends at the last complete block; if `fromBlock` itself did not complete, the request fails with `-32005`. Only the page's own deadline ends a page early: a sub-request that times out or fails on its own fails the request. `fromBlock` and `cursorBlock` are the verified headers of the page's first and last block; `toBlock` is the header of the resolved `toBlock`, fetched once more only when the page ends before it. Related objects (`transactions`, `blocks`) appear only when selected, once each, only for blocks and transactions a primary object references; they do not count toward `target`. They come from the block already fetched during the scan (`eth_getBlockByNumber` with full transactions when transactions are selected), never from a second fetch; logs without `blockTimestamp` take the scanned header's timestamp. Transactions carry receipt fields merged from `eth_getBlockReceipts` (`evm.MergeReceipt`) whenever the selection reads one of `status`, `gasUsed`, `cumulativeGasUsed`, `effectiveGasPrice`, `contractAddress`, `logsBloom`. Primary and related arrays are sorted by the MIP-16 ordering keys and reversed as a whole in `desc` order, including objects inside one block. **Shim chain consistency.** One response never mixes blocks from before and after a reorg (MIP-16 Chain consistency). Each scanned block is checked against its own header: every log, receipt, transaction and `trace_block` frame must carry the header's `blockHash`, each `trace_block` frame with a `transactionHash` must name one of the block's transactions, and each `callTracer` result's `txHash` must be the block's transaction at its index (callTracer results carry no block hash). Consecutive headers must link by `parentHash` in traversal order (in `desc`, the earlier block's `parentHash` is the next block's `hash`). At the first break, the scan stops; the blocks from the one before the break are scanned once more with cache reads skipped (`skipCacheRead`) and no pin, and the longer consistent run wins. The page ends at the last consistent block, so the next page starts on whichever chain the upstreams then serve; clients detect the switch by comparing `fromBlock.parentHash` with the previous `cursorBlock.hash`. If `fromBlock` itself is still inconsistent after the re-scan, the request fails as transient: erpc's retryable server error `-32603` on JSON-RPC, `UNAVAILABLE` on gRPC; retry. **Shim filters.** `null` and `[]` place no constraint; an array matches any element; fields combine with AND. `selector` matches only inputs of 4 bytes or more. `eth_queryLogs` passes `address` and `topics` to `eth_getLogs` (one call per chunk), so topic semantics are exactly `eth_getLogs`'. `eth_queryBlocks` filters on `miner`. **Shim traces and transfers.** `trace_block` frames convert with `evm.TraceFromParity` and `evm.PropagateParityReverted` (reward frames are dropped); when `trace_block` is unsupported, `debug_traceBlockByNumber` with `callTracer` converts per transaction with `evm.TraceFromGethDebug`. The network remembers which method answered and asks it directly afterwards. Transfers are `evm.NativeTransfersFromTraces`. Frames with `reverted: true` are dropped unless `filter.includeReverted` is `true`. System transactions and precompile calls are kept. **Errors.** JSON-RPC errors carry the MIP-16 code in `error.code`: `-32602` invalid params, `-32001` range outside availability, `-32004` method not served, `-32005` budget exceeded before `fromBlock` completed. gRPC returns `INVALID_ARGUMENT`, `OUT_OF_RANGE`, `UNIMPLEMENTED` and `RESOURCE_EXHAUSTED` for the same conditions. `fromBlock` not consistent across sub-requests (reorg in progress, lagging upstream) is transient and no MIP-16 condition: `-32603` on JSON-RPC, `UNAVAILABLE` on gRPC. A sub-request failure that has no MIP-16 meaning (upstream error, timeout) passes through erpc's usual error normalization on JSON-RPC, and erpc's gRPC status mapping on gRPC (`DEADLINE_EXCEEDED`, `RESOURCE_EXHAUSTED`, `UNAVAILABLE`, ...; see [gRPC & BDS](/reference/grpc-bds.llms.txt)). A native upstream's own error is unwrapped from the stream wrapper, so its gRPC status keeps its MIP-16 code on both transports (`OUT_OF_RANGE` → `-32001`, and so on) when no shim takes over. ### Config schema #### Network-level fields (`networks[].evm.*`) | Field | Type | Default | Behavior / footguns | |---|---|---|---| | `integrity.enforceHighestBlock` | `*bool` | `true` | Enables highest-block enforcement for `eth_blockNumber` and `eth_getBlockByNumber`. Also propagated to `DirectiveDefaults.EnforceHighestBlock`. **Deprecated path** — new canonical path is `networks[].directiveDefaults`. Source: [`common/config.go:L2114`](https://github.com/erpc/erpc/blob/main/common/config.go#L2114), [`common/defaults.go:L1458-1459`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1458-L1459) | | `integrity.enforceNonNullTaggedBlocks` | `*bool` | `true` | When true, null responses for tagged block queries (`"latest"`, `"finalized"`) trigger `ErrEndpointMissingData`. Propagated to `DirectiveDefaults.EnforceNonNullTaggedBlocks`. Source: [`common/config.go:L2116`](https://github.com/erpc/erpc/blob/main/common/config.go#L2116), [`common/defaults.go:L1464-1465`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1464-L1465) | | `integrity.enforceGetLogsBlockRange` | `*bool` | `true` | Enables upstream-level block range availability checking for `trace_filter`/`arbtrace_filter` and `eth_getLogs`. Source: [`common/config.go:L2115`](https://github.com/erpc/erpc/blob/main/common/config.go#L2115), [`common/defaults.go:L2121-2122`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2121-L2122) | | `emptyResultConfidence` | `AvailabilityConfidence` | `blockHead` | Confidence head for `emptyResultBeyondConfidence`: `blockHead` uses `EvmHighestLatestBlockNumber`; `finalizedBlock` uses `EvmHighestFinalizedBlockNumber`. Source: [`common/config.go:L2250`](https://github.com/erpc/erpc/blob/main/common/config.go#L2250), [`common/defaults.go:L2075-2078`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2075-L2078) | | `markEmptyAsErrorMethods` | `[]string` | see default list | Methods for which null/empty upstream responses become `ErrEndpointMissingData`. `nil` = use default list. `[]` (empty slice) = disable entirely. **Footgun:** setting a custom list does NOT merge with defaults — the default list is fully replaced. Source: [`common/config.go:L2218`](https://github.com/erpc/erpc/blob/main/common/config.go#L2218), [`common/defaults.go:L2111-2113`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2111-L2113) | | `idempotentTransactionBroadcast` | `*bool` | `true` (nil = enabled) | Enables idempotency handling for `eth_sendRawTransaction`. Set `false` to disable. Source: [`common/config.go:L2239`](https://github.com/erpc/erpc/blob/main/common/config.go#L2239) | | `traceFilterSplitOnError` | `*bool` | `nil` (off) | Enables reactive bisection of `trace_filter` on too-large errors. Intentionally off by default. Source: [`common/config.go:L2197`](https://github.com/erpc/erpc/blob/main/common/config.go#L2197), [`common/defaults.go:L2103-2104`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2103-L2104) | | `traceFilterSplitConcurrency` | `int` | `10` | Max in-flight sub-requests during trace_filter splitting. Source: [`common/config.go:L2200`](https://github.com/erpc/erpc/blob/main/common/config.go#L2200), [`common/defaults.go:L2105-2106`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2105-L2106) | **Default `markEmptyAsErrorMethods`** ([`common/defaults.go:L2044-2064`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2044-L2064)): `eth_blockNumber`, `eth_getBlockByNumber`, `eth_getTransactionByHash`, `eth_getTransactionByBlockHashAndIndex`, `eth_getTransactionByBlockNumberAndIndex`, `eth_getUncleByBlockHashAndIndex`, `eth_getUncleByBlockNumberAndIndex`, `debug_traceTransaction`, `trace_transaction`, `trace_block`, `trace_get`. Excluded by design: `eth_getBlockByHash` (subgraphs return empty legitimately), `eth_getTransactionReceipt` (null for pending is valid), `eth_getBlockReceipts` (empty array for 0-tx blocks is valid). #### Per-method fields (`networks[].methods.definitions..*`) | Field | Type | Default | Behavior / footguns | |---|---|---|---| | `translateLatestTag` | `*bool` | `nil` (enabled) | When nil or true, `"latest"` in this method's block param is replaced with the concrete hex from `EvmHighestLatestBlockNumber`. **Footgun:** disabling causes the cache key to omit a concrete block number, breaking cache deduplication with concrete-block requests for the same block. Source: [`common/config.go:L307-309`](https://github.com/erpc/erpc/blob/main/common/config.go#L307-L309), [`architecture/evm/json_rpc.go:L130`](https://github.com/erpc/erpc/blob/main/architecture/evm/json_rpc.go#L130) | | `translateFinalizedTag` | `*bool` | `nil` (enabled) | Same as above for `"finalized"` and `EvmHighestFinalizedBlockNumber`. Both flags are evaluated independently. Even when disabled, `seenFinalized` is still recorded so `EvmBlockRef` metadata is still written. Source: [`common/config.go:L310-312`](https://github.com/erpc/erpc/blob/main/common/config.go#L310-L312), [`architecture/evm/json_rpc.go:L131`](https://github.com/erpc/erpc/blob/main/architecture/evm/json_rpc.go#L131) | #### Upstream-level fields (`upstreams[].evm.*`) | Field | Type | Default | Behavior / footguns | |---|---|---|---| | `traceFilterAutoSplittingRangeThreshold` | `int64` | `0` (disabled) | Max block range per chunk for proactive trace_filter splitting at this upstream. Effective threshold is `min` across all selected upstreams with positive values. Zero = this upstream doesn't constrain. Source: [`common/config.go:L1134`](https://github.com/erpc/erpc/blob/main/common/config.go#L1134), [`common/defaults.go:L1546-1547`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1546-L1547) | | `queryShim.enabled` | `*bool` | `nil` (disabled) | This upstream may serve the shim sub-requests of `eth_query*`. At least one upstream must enable it (or serve `QueryService` natively) for a method, else the method returns `-32004`. Source: [`common/config.go:L1373-1383`](https://github.com/erpc/erpc/blob/main/common/config.go#L1373-L1383) | | `queryShim.allowedMethods` | `[]string` | `[]` (all allowed) | Wildcard-matched `eth_query*` methods the shim serves through this upstream. Empty = all five. Source: [`common/config.go:L1404-1419`](https://github.com/erpc/erpc/blob/main/common/config.go#L1404-L1419) | | `queryShim.maxBlocksPerPage` | `int64` | `1000` | Budget: blocks one shim page may scan. The smallest value across shim upstreams wins. Source: [`erpc/query_executor.go:L48-53`](https://github.com/erpc/erpc/blob/main/erpc/query_executor.go#L48-L53) | | `queryShim.maxPageDuration` | `Duration` | `10s` | Budget: time one shim page may scan. The page ends at the last complete block; `-32005` when `fromBlock` did not complete. The smallest value across shim upstreams wins. Source: [`erpc/query_executor.go:L48-53`](https://github.com/erpc/erpc/blob/main/erpc/query_executor.go#L48-L53) | ### Worked examples **1. Indexer with wide trace_filter ranges.** Proactive splitting per upstream keeps trace requests within each node's limit. The min-threshold is 200 blocks across upstreams, but different providers can declare different constraints and eRPC uses the most restrictive: **Config path:** `projects[].networks[].evm + upstreams[].evm` **YAML — `erpc.yaml`:** ```yaml # Network level — reactive fallback if proactive misses networks: - architecture: evm evm: traceFilterSplitOnError: true traceFilterSplitConcurrency: 20 # Upstream level — proactive split threshold per provider upstreams: - id: quicknode evm: traceFilterAutoSplittingRangeThreshold: 200 - id: alchemy evm: traceFilterAutoSplittingRangeThreshold: 500 ``` **TypeScript — `erpc.ts`:** ```typescript networks: [{ architecture: "evm", evm: { traceFilterSplitOnError: true, traceFilterSplitConcurrency: 20, }, }], upstreams: [ { id: "quicknode", evm: { traceFilterAutoSplittingRangeThreshold: 200 } }, { id: "alchemy", evm: { traceFilterAutoSplittingRangeThreshold: 500 } }, ] ``` **2. Broadcast-heavy app (wallets, bots).** With idempotent broadcast enabled, retried sends that arrive with "already known" or "nonce too low" from one provider are converted to synthetic success rather than surfacing confusing errors. No config change needed — it is on by default. Disable only if your app has its own nonce tracking and does not want eRPC to probe on its behalf: **Config path:** `projects[].networks[].evm` **YAML — `erpc.yaml`:** ```yaml networks: - architecture: evm evm: idempotentTransactionBroadcast: false # disable if app handles nonces ``` **TypeScript — `erpc.ts`:** ```typescript networks: [{ architecture: "evm", evm: { idempotentTransactionBroadcast: false }, }] ``` **3. Finality-sensitive data pipeline.** When your application must never read data above the finalized head (e.g. a settlement system), set `emptyResultConfidence: finalizedBlock`. Blocks between finalized and latest become "beyond confidence" — empty results from that range are returned truthfully rather than retried: **Config path:** `projects[].networks[].evm` **YAML — `erpc.yaml`:** ```yaml networks: - architecture: evm evm: emptyResultConfidence: finalizedBlock integrity: enforceHighestBlock: true enforceNonNullTaggedBlocks: true ``` **TypeScript — `erpc.ts`:** ```typescript networks: [{ architecture: "evm", evm: { emptyResultConfidence: "finalizedBlock", integrity: { enforceHighestBlock: true, enforceNonNullTaggedBlocks: true, }, }, }] ``` **4. eth_query\* (MIP-16) from an archive node.** Enable the shim on an archive upstream that serves blocks, receipts, logs and traces to expose the five MIP-16 methods. Leave `allowedMethods` empty to allow all five; lower the budget to keep each page short: **Config path:** `upstreams[].evm.queryShim` **YAML — `erpc.yaml`:** ```yaml upstreams: - id: archive-node evm: queryShim: enabled: true # at most 500 blocks or 5s of scanning per page maxBlocksPerPage: 500 maxPageDuration: 5s ``` **TypeScript — `erpc.ts`:** ```typescript upstreams: [{ id: "archive-node", evm: { queryShim: { enabled: true, // at most 500 blocks or 5s of scanning per page maxBlocksPerPage: 500, maxPageDuration: "5s", }, }, }] ``` ### Request/response behavior - `eth_call` and `eth_estimateGas` with one param always have `"latest"` appended before forwarding; callers never see this normalization. [[`architecture/evm/eth_call.go:L12-34`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_call.go#L12-L34)] - Local answers are skipped when `ShouldSkipCacheRead("")` is true, allowing force-refresh requests to reach real upstreams. [[`architecture/evm/local_answer.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/local_answer.go)] - `eth_blockNumber` is answered from the served head while it is known; any forwarded or cached response is pinned exactly to it. [[`architecture/evm/eth_blockNumber.go:L13-171`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_blockNumber.go#L13-L171)] - `eth_getBlockByNumber` `enforceHighestBlock` re-forward keeps every upstream eligible, including the one that answered stale, and skips the cache; a re-fetched block that no upstream has comes back empty and `pickHighestBlock` keeps the original block. [[`architecture/evm/eth_getBlockByNumber.go:L276-303`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L276-L303)] - `emptyResultBeyondConfidence` with head=0 (unknown) fails open — any block is treated as potentially retryable. [[`architecture/evm/common.go:L62-89`](https://github.com/erpc/erpc/blob/main/architecture/evm/common.go#L62-L89)] - `eth_sendRawTransaction` network-level probe uses `context.WithoutCancel` + 3-second fixed timeout; probe runs even when the parent context is expired. [[`architecture/evm/eth_sendRawTransaction.go:L24-26`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_sendRawTransaction.go#L24-L26)] - `eth_sendRawTransaction` synthetic success requires hash cross-check: returned `hash` field must match the locally-derived hash; mismatch or missing field returns the original error. [[`architecture/evm/eth_sendRawTransaction.go:L375-385`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_sendRawTransaction.go#L375-L385)] - `trace_filter`/`arbtrace_filter` upstream post-forward always normalizes `null`/`""`/`{}` to `[]`; errors pass through unchanged. [[`architecture/evm/trace_filter.go:L361-374`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L361-L374)] - trace_filter split sub-requests set `ParentRequestId`; both proactive and reactive splitting skip requests with a non-nil `ParentRequestId` to prevent re-entrancy. [[`architecture/evm/trace_filter.go:L122-125`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L122-L125)] - eth_query\* shim sub-requests carry the client request id as `ParentRequestId` and go through `Network.Forward` (cache, failsafe, selection); the sub-requests of one page prefer the upstream that served its first one (a `null` from it is asked of the other upstreams), and the consistency re-scan skips cache reads. [[`erpc/query_shim.go`](https://github.com/erpc/erpc/blob/main/erpc/query_shim.go)] - Error normalizer nonce rules (rules 11/12) appear before the generic `-32003` rule; this ordering is a hard correctness requirement. [[`architecture/evm/error_normalizer.go:L297-301`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L297-L301)] - Insufficient-funds errors remain `N:no` for `eth_sendRawTransaction`; only revert, out-of-gas, and `-32003` codes are flipped to `N:yes`. [[`architecture/evm/error_normalizer.go:L350-361`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L350-L361)] ### Best practices - Enable `traceFilterSplitOnError: true` in production even if you also set `traceFilterAutoSplittingRangeThreshold` per upstream — reactive splitting is your safety net when a new upstream joins that doesn't advertise its limit. - Never add `eth_getBlockReceipts` to `markEmptyAsErrorMethods`: empty arrays are the correct response for 0-transaction blocks and inclusion will cause retry storms against the outer network timeout. - If you set a custom `markEmptyAsErrorMethods` list, the default list is fully replaced — copy the defaults from `common/defaults.go:L2044-L2064` and then add your additions. - Keep `idempotentTransactionBroadcast: true` (the default). The only reason to disable it is if your application manages nonces explicitly and treats "already known" / "nonce too low" as signals for its own retry logic. - Use `emptyResultConfidence: finalizedBlock` for settlement or audit workloads where serving un-finalized data would be a correctness violation. The default `blockHead` is appropriate for all other use cases. - Avoid setting `translateLatestTag: false` or `translateFinalizedTag: false` per-method unless you have a specific reason — disabling tag translation breaks cache deduplication: `"latest"`-tagged and concrete-block requests for the same block will generate different cache keys. - Set `traceFilterSplitConcurrency` conservatively (start at 10, the default) and watch `erpc_network_evm_trace_filter_split_failure_total` before raising it — each concurrent sub-request is a separate upstream call. ### Edge cases & gotchas 1. **eth_call / eth_estimateGas missing block param auto-injection** — a client sending only the call object (no block tag) gets `"latest"` silently appended. Source: [`architecture/evm/eth_call.go:L18-30`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_call.go#L18-L30). 2. **Skip-cache-read routes local answers** — `x-erpc-skip-cache-read: true` sends `eth_chainId`, `net_*`, `web3_clientVersion` and `eth_syncing` to real upstreams. Source: [`architecture/evm/local_answer.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/local_answer.go). 3. **eth_blockNumber never reaches an upstream while the served head is known** (with `enforceHighestBlock` on) — the network pre-forward hook answers it; a forwarded or cached response is pinned exactly to the served head, never above or below it. Source: [`architecture/evm/eth_blockNumber.go:L13-171`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_blockNumber.go#L13-L171). 4. **enforceHighestBlock never excludes the stale upstream** — the re-forward is a fresh request for the higher block number, eligible on every upstream; the stale-block counters increment only when the original response contained a real block number (non-zero). Source: [`architecture/evm/eth_getBlockByNumber.go:L173-188`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L173-L188), [`architecture/evm/eth_getBlockByNumber.go:L276-303`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L276-L303). 5. **pickHighestBlock safety** — always compares old vs new response rather than blindly returning the re-fetched block, guarding against corrupt poller state. If one response is null/empty, returns the other; if both are null/empty, returns the error from the re-forward rather than a null block. Source: [`architecture/evm/eth_getBlockByNumber.go:L335-415`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L335-L415). 6. **eth_getBlockReceipts excluded from default markEmptyAsErrorMethods** — empty arrays are correct for 0-tx blocks; inclusion causes retry storms. Source: [`common/defaults.go:L2054-2064`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2054-L2064). 7. **Phantom transactions (Polygon PoS / BSC / HyperEVM)** — blocks whose transactions are all system/phantom txs legitimately have `transactionsRoot = emptyTrieRoot` on a non-empty array; the integrity module's `transactionsRootConsistency` check skips the inconsistency error for these. Polygon PoS / BSC system txs use `from=0x0, gas=0x0`; HyperEVM native/L1 system txs instead carry a real-looking sender and non-zero gas and are marked by a synthetic signature `r=0x1` with zero `gasPrice` (which also covers the HYPE bridge case where `s=1` ≠ `from`). Source: [`architecture/evm/integrity/checks_block.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/integrity/checks_block.go). 8. **trace_filter null-to-empty normalization** — older Parity/OpenEthereum nodes return `null` for empty ranges; `["result": null]` is converted to `[]`. Source: [`architecture/evm/trace_filter.go:L361-374`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L361-L374). 9. **trace_filter proactive split min-threshold across upstreams** — all-zero thresholds means no proactive splitting occurs. Source: [`architecture/evm/trace_filter.go:L175-187`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L175-L187). 10. **eth_sendRawTransaction independent probe context** — the parent context is always expired when the network-level hook fires; `context.WithoutCancel` is required for the probe to run. Source: [`architecture/evm/eth_sendRawTransaction.go:L346-347`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_sendRawTransaction.go#L346-L347). 11. **eth_sendRawTransaction hash mismatch refuses synthetic success** — prevents a byzantine upstream from faking a successful broadcast. Source: [`architecture/evm/eth_sendRawTransaction.go:L375-385`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_sendRawTransaction.go#L375-L385). 12. **queryShim allowedMethods narrows the shim, not the network** — a method that no shim upstream allows and no native upstream serves returns `-32004`; it is never forwarded raw. Source: [`erpc/query_executor.go`](https://github.com/erpc/erpc/blob/main/erpc/query_executor.go). 13. **eth_queryTraces trace_block → debug fallback** — `-32601`/unsupported `trace_block` switches the network to `debug_traceBlockByNumber` (callTracer); if both are unsupported the request returns `-32004`. Source: [`erpc/query_shim.go`](https://github.com/erpc/erpc/blob/main/erpc/query_shim.go). 14. **eth_queryLogs reads one eth_getLogs per scan chunk** — chunks start at 4 blocks and double to 64, so a node's `eth_getLogs` range limit above 64 blocks never bites; `eth_getLogs` also applies the `address`/`topics` filter. Source: [`erpc/query_shim.go`](https://github.com/erpc/erpc/blob/main/erpc/query_shim.go). 15. **200-OK ABI-selector revert detection** — responses whose `result` starts with `0x08c379a0` (`Error(string)` selector) are converted to `ErrEndpointExecutionException`. Applies to all methods that can trigger EVM execution, not just `eth_call`. Source: [`architecture/evm/error_normalizer.go:L609-624`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L609-L624). 16. **trace/debug timeout detection via raw-byte scan** — responses from `trace_*`/`debug_*`/`eth_trace` methods can exceed 50 MB; the normalizer uses `strings.Contains(dt, "execution timeout")` instead of JSON parsing to detect server-side timeouts. Source: [`architecture/evm/error_normalizer.go:L626-652`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L626-L652). 17. **validateBlock requires non-nil directives** — all upstream-level block validation for `eth_getBlockByNumber` is skipped when `rq.Directives()` is nil. Source: [`architecture/evm/eth_getBlockByNumber.go:L449-454`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L449-L454). 18. **translateLatestTag=false breaks cache deduplication** — disabling tag translation causes cache keys to omit a concrete block number; `"latest"`-tagged and concrete-block requests for the same block produce different cache keys. Source: [`architecture/evm/json_rpc.go:L147-153`](https://github.com/erpc/erpc/blob/main/architecture/evm/json_rpc.go#L147-L153). 19. **integrity fields are deprecated** — `networks[].evm.integrity.*` is deprecated; values are migrated to `networks[].directiveDefaults` during `SetDefaults`. Source: [`common/defaults.go:L1957-1964`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1957-L1964). 20. **ErrEndpointClientSideException returns HTTP 200 for revert-class codes** — `JsonRpcErrorEvmReverted` (3), `JsonRpcErrorCallException` (-32000), and `JsonRpcErrorTransactionRejected` (-32003) return HTTP 200, not 400; callers must check the JSON-RPC error code to distinguish execution failures from malformed requests. Source: [`common/errors.go:L1894-1905`](https://github.com/erpc/erpc/blob/main/common/errors.go#L1894-L1905). 21. **markEmptyAsErrorMethods requires RetryEmpty=true to fire** — methods in the list only have their null/empty results converted to `ErrEndpointMissingData` when the `RetryEmpty` directive is `true` on the request. When `RetryEmpty=false`, null/empty passes through unchanged regardless of the list. Methods NOT in the list are never converted. Source: [`architecture/evm/common.go:L62-89`](https://github.com/erpc/erpc/blob/main/architecture/evm/common.go#L62-L89). 22. **trace_filter fromBlock > toBlock returns -32600 immediately** — both the proactive network pre-forward hook and the upstream pre-forward block-range check reject requests where `fromBlock > toBlock` with a `-32600` (InvalidRequest) error before any sub-requests are dispatched. Source: [`architecture/evm/trace_filter.go:L122-200`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L122-L200). 23. **eth_sendRawTransaction upstream probe errors return the original error** — if the `eth_getTransactionByHash` probe issued during `nonce_too_low` handling itself fails (network error, timeout, etc.), the original nonce error is returned unchanged. The probe is a best-effort check, not a retry gate. Source: [`architecture/evm/eth_sendRawTransaction.go:L51-270`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_sendRawTransaction.go#L51-L270). 24. **NormalizeHttpJsonRpc skips top-level object params** — when a `ReqRefs` path points to a map/object param (e.g., the call object in `eth_call`), the function does not attempt to replace it with a hex block number even if the path resolves to a numeric value. Only scalar leaf-level block references within nested objects are replaced. Source: [`architecture/evm/json_rpc.go:L168-194`](https://github.com/erpc/erpc/blob/main/architecture/evm/json_rpc.go#L168-L194). 25. **eth_query\* `"pending"` is invalid** — only `latest`, `earliest`, `safe`, `finalized` and QUANTITY are accepted (`-32602`). `"safe"` resolves to the finalized block when known, else latest; `"earliest"` is block `0`, which the availability check requires the union of the serving upstreams' windows to cover. Source: [`erpc/query_executor.go`](https://github.com/erpc/erpc/blob/main/erpc/query_executor.go). 26. **eth_query\* tries native QueryService upstreams first** — gRPC BDS upstreams that serve `bds.evm.QueryService` and allow the method are tried before the shim, in selection-policy order. A native upstream needs `allowMethods` to admit the method (or `eth_query*`). On Monad, Goldsky's endpoint serves `eth_queryLogs` and `eth_queryTransactions` natively; see [Goldsky cache](/config/database/goldsky-cache.llms.txt). Source: [`erpc/query_executor.go`](https://github.com/erpc/erpc/blob/main/erpc/query_executor.go). 27. **eth_query\* JSON calls skip the cache and multiplexer** — each JSON call runs the executor; only the shim's sub-requests are cached. Source: [`erpc/networks.go`](https://github.com/erpc/erpc/blob/main/erpc/networks.go). 28. **eth_query\* shim pages stop at a reorg** — a page whose next block does not link to the previous one by `parentHash`, or whose objects carry another block hash than their header, ends at the last consistent block after one cache-skipping re-scan; a short page near the tip is normal, and `-32603` (gRPC `UNAVAILABLE`) means even `fromBlock` was inconsistent. Retry, or page from `"finalized"`. Source: [`erpc/query_shim.go`](https://github.com/erpc/erpc/blob/main/erpc/query_shim.go). ### Observability | Metric | Type | Labels | When it fires | |---|---|---|---| | `erpc_upstream_stale_latest_block_total` | Counter | `project`, `vendor`, `network`, `upstream`, `category` | Upstream returned an older block than the network's known highest; `category` = `"eth_blockNumber"` or `"eth_getBlockByNumber"` | | `erpc_upstream_stale_finalized_block_total` | Counter | `project`, `vendor`, `network`, `upstream` | Upstream returned an older finalized block in `eth_getBlockByNumber` | | `erpc_network_latest_block_timestamp_distance_seconds` | Gauge | `project`, `network`, `origin` | `eth_getBlockByNumber(latest)` response received with `origin="network_response"` | | `erpc_network_evm_trace_filter_range_requested` | Histogram | `project`, `network`, `method`, `user`, `finality` | Any `trace_filter`/`arbtrace_filter` request with resolvable block bounds | | `erpc_network_evm_trace_filter_forced_splits_total` | Counter | `project`, `network`, `method`, `dimension`, `user`, `agent_name` | Each split by dimension (`block_range`, `from_address`, `to_address`) | | `erpc_network_evm_trace_filter_split_success_total` | Counter | `project`, `network`, `method`, `user`, `agent_name` | Each successful sub-request in a split execution | | `erpc_network_evm_trace_filter_split_failure_total` | Counter | `project`, `network`, `method`, `user`, `agent_name` | Each failed sub-request in a split execution | **Trace span names** (for distributed tracing / APM): - `Project.PreForwardHook` — parent span for all project-level hooks - `Network.PreForwardHook` - `Network.PostForward.eth_blockNumber`, `Network.PostForward.eth_getBlockByNumber`, `Network.PostForward.eth_sendRawTransaction` - `Upstream.PreForwardHook`, `Upstream.PreForwardHook.trace_filter` - `Upstream.PostForwardHook.eth_getBlockByNumber`, `Upstream.PostForwardHook.eth_getBlockReceipts`, `Upstream.PostForwardHook.eth_sendRawTransaction` - `Evm.PickHighestBlock`, `extractTxHashFromSendRawTransaction`, `createSyntheticSuccessResponse`, `verifyAndHandleNonceTooLow` **Notable log messages** (for debugging): - `"interpolated block tag to concrete block number"` (debug) — `NormalizeHttpJsonRpc` resolved `"latest"` or `"finalized"` to a concrete hex. - `"passed through block tag"` (trace) — `"safe"`, `"pending"`, etc. left unchanged. - `"upstream returned older block than we know, falling back to highest known block"` (debug) — `eth_blockNumber` response below the served head, raised to it. - `"response is ahead of the served tip, pinning to the served tip"` (debug) — `eth_blockNumber` response above the served head, lowered to it. - `"converting 'already known' error to idempotent success"` (info) — `eth_sendRawTransaction` upstream hook. - `"tx FOUND on-chain - converting 'nonce too low' to idempotent success"` (info) — `eth_sendRawTransaction` nonce-too-low probe success. - `"exhausted error overridden: tx found in network, returning synthetic success"` (info) — `eth_sendRawTransaction` network-level hook. - `"verification response carries a different tx hash than submitted — refusing synthetic success"` (warn) — hash mismatch guard. ### Source code entry points - [`architecture/evm/hooks.go:L10-L160`](https://github.com/erpc/erpc/blob/main/architecture/evm/hooks.go#L10-L160) — hook dispatch table; all five entry points routing by method name - [`architecture/evm/json_rpc.go:L92-L321`](https://github.com/erpc/erpc/blob/main/architecture/evm/json_rpc.go#L92-L321) — `NormalizeHttpJsonRpc`; block-tag resolution, param deep-copy, path rewriting - [`architecture/evm/common.go:L62-L89`](https://github.com/erpc/erpc/blob/main/architecture/evm/common.go#L62-L89) — `emptyResultBeyondConfidence`; `upstreamPostForward_markUnexpectedEmpty` - [`architecture/evm/eth_sendRawTransaction.go:L51-L400`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_sendRawTransaction.go#L51-L400) — upstream/network idempotency; `extractTxHashFromSendRawTransaction`; `verifyAndHandleNonceTooLow` - [`architecture/evm/trace_filter.go:L22-L600`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L22-L600) — `TraceFilterMethods`; proactive/reactive splitting; `splitTraceFilterRequest`; `executeTraceFilterSubRequests` - [`architecture/evm/eth_getBlockByNumber.go:L43-L600`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L43-L600) — `enforceHighestBlock`, `enforceNonNullBlock`, `pickHighestBlock`; block header/tx validation - [`architecture/evm/error_normalizer.go:L246-L652`](https://github.com/erpc/erpc/blob/main/architecture/evm/error_normalizer.go#L246-L652) — all normalizer rules: revert, nonce ordering, insufficient-funds, out-of-gas, ABI-selector 200-OK, trace timeout scan - [`erpc/query_executor.go`](https://github.com/erpc/erpc/blob/main/erpc/query_executor.go) — `EvmQueryExecutor`: range resolution, availability, native-first routing, MIP-16 error codes, JSON entry `forwardQuery` - [`erpc/query_shim.go`](https://github.com/erpc/erpc/blob/main/erpc/query_shim.go) — shim pages: block scanner, budget, chain consistency (hash checks, cache-skipping re-scan), filters, relations, receipts, trace_block→debug fallback - [`common/defaults.go:L2044-L2113`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2044-L2113) — `DefaultMarkEmptyAsErrorMethods`; `EvmNetworkConfig.SetDefaults`; directive default migration ### Related pages - [EVM network config](/config/projects/evm-networks.llms.txt) — where `networks[].evm.*` fields live alongside chain ID, polling, and block tracker settings. - [Upstream config](/config/projects/upstreams.llms.txt) — `upstreams[].evm.*` fields including `queryShim` and `traceFilterAutoSplittingRangeThreshold`. - [Failsafe](/config/failsafe/retry.llms.txt) — the retry/timeout/hedge loop that wraps the hooks; method handlers fire inside this loop. - [Cache](/config/database/evm-json-rpc-cache.llms.txt) — block-tag interpolation (handled here) directly controls cache key stability. - [Matcher](/config/matcher.llms.txt) — controls which requests reach which upstreams before hook logic runs. - [Survive provider outages](/use-cases/survive-provider-outages.llms.txt) — how idempotent broadcast and block enforcement combine during failover. --- ## Navigation (machine-readable surface) - Up: [All pages index](https://docs.erpc.cloud/llms.txt) - Root index of every page: [llms.txt](https://docs.erpc.cloud/llms.txt) · everything in one file: [llms-full.txt](https://docs.erpc.cloud/llms-full.txt) ### Sibling pages - [Block tracking & served tip](https://docs.erpc.cloud/reference/evm/block-tracking.llms.txt) — eRPC keeps a live pulse on every upstream's chain head and finality, then distills a single honest block number your clients can trust — with no "block not found" surprises. - [getLogs auto-splitting](https://docs.erpc.cloud/reference/evm/getlogs-splitting.llms.txt) — Large eth_getLogs queries silently split into parallel chunks and merge back — no more range-limit errors from any provider, no client changes required.