# Integrity checks > Source: https://docs.erpc.cloud/config/failsafe/integrity > eRPC silently discards stale or structurally broken upstream responses and retries on another provider — callers always get the correct answer. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # Integrity checks Upstream providers occasionally return stale block numbers, null blocks, logs outside their available range, or structurally broken receipts. eRPC catches all of it silently — bad responses are discarded and retried against a different upstream before the caller ever sees them. **What you get:** - Highest-block enforcement keeps `eth_blockNumber` and `eth_getBlockByNumber` from going backward. - Block-range pre-screening stops `eth_getLogs` from hitting a node that doesn't have the data. - Non-null enforcement turns empty tagged-block responses into retries automatically. - Opt-in receipt and header validators catch the data anomalies indexers care about most. ## Quick taste Illustrative, not a tuned production config — the four defaults are already on; this shows adding Bloom recompute: **Config path:** `projects[].networks[].directiveDefaults` **YAML — `erpc.yaml`:** ```yaml projects: - id: main networks: - architecture: evm evm: { chainId: 1 } directiveDefaults: # opt-in: full bloom recompute — most expensive validator validateLogsBloomMatch: true ``` **TypeScript — `erpc.ts`:** ```typescript projects: [{ id: "main", networks: [{ architecture: "evm", evm: { chainId: 1 }, directiveDefaults: { // opt-in: full bloom recompute — most expensive validator validateLogsBloomMatch: 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: harden an indexer pipeline against bad upstream data** ```text I'm running an eRPC-fronted indexer that backfills historical blocks. Enable the full suite of opt-in integrity validators (header field lengths, transaction fields, transaction block info, log index contiguity, Bloom recompute) so structurally broken upstream responses are silently retried rather than silently written to the database. Work with my existing eRPC config. Read the full reference first: https://docs.erpc.cloud/config/failsafe/integrity.llms.txt ``` **Prompt Example #2: disable checks that break a non-standard chain** ```text My eRPC setup serves ZKSync Era (chainId 324). The default enforceNonNullTaggedBlocks and validateTransactionsRoot checks cause infinite retries because ZKSync legitimately returns null for some tagged blocks and uses a non-standard transactions root. Disable exactly those two checks in my eRPC config without touching any other integrity settings. Reference: https://docs.erpc.cloud/config/failsafe/integrity.llms.txt ``` **Prompt Example #3: stop retry storms on archive workloads** ```text My eRPC instance serves an archive node backfill that frequently reads recent unfinalized blocks. The default blockHead confidence triggers retry storms when upstreams legitimately return empty for not-yet-finalized blocks. Switch emptyResultConfidence to finalizedBlock for the affected network in my eRPC config and explain what changes. Reference: https://docs.erpc.cloud/config/failsafe/integrity.llms.txt ``` **Prompt Example #4: debug why eth_blockNumber enforcement won't turn off** ```text I set directiveDefaults.enforceHighestBlock: false in my network config but eth_blockNumber responses are still being replaced with a synthetic highest-known block. Why doesn't the directive control that hook, and what do I actually need to set to disable it? Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/failsafe/integrity.llms.txt ``` **Prompt Example #5: add per-request Bloom validation on a critical path** ```text I want validateLogsBloomMatch only on specific high-value eth_getBlockReceipts calls from my indexer, not globally (too CPU-heavy). Show me how to keep it off in directiveDefaults but enable it per-request via an HTTP header, and which Prometheus metrics to watch to confirm it's firing. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/failsafe/integrity.llms.txt ``` --- ### Integrity checks — full agent reference ### How it works **Two configuration planes.** Config migrated from the older `networks[].evm.integrity` (type `EvmIntegrityConfig`) to `networks[].directiveDefaults` (type `DirectiveDefaultsConfig`). During `SetDefaults`, if `evm.integrity` is populated and the corresponding `directiveDefaults` field is nil, the old value is promoted automatically. After promotion, `DirectiveDefaultsConfig.SetDefaults` fills `true` for any still-nil boolean fields. `EvmIntegrityConfig` is marked `@deprecated` in source. [`common/config.go:L2308`](https://github.com/erpc/erpc/blob/main/common/config.go#L2308) **Request directives as the runtime gate.** All checks read fields on `RequestDirectives`, populated in priority order (highest wins last): (1) `directiveDefaults` computed at startup; (2) `X-ERPC-*` HTTP request headers; (3) `?=` query parameters. Any check can be toggled per-request without redeploying. Ground-truth fields (`GroundTruthTransactions`, `GroundTruthLogs`) are only settable programmatically — they cannot be passed via HTTP. [`common/request.go:L116-215`](https://github.com/erpc/erpc/blob/main/common/request.go#L116-L215) **Hook architecture.** Checks live in EVM-specific pre/post-forward hooks registered in an `EvmHookRegistry`. Network-level hooks run before or after the entire failsafe execution; upstream-level hooks run per attempt. Key hooks: - `projectPreForward_eth_blockNumber` — replaces a stale block number with a synthetic highest-known result (reads legacy `EvmIntegrityConfig`, not directive). - `networkPostForward_eth_getBlockByNumber` — enforces highest-block then non-null after the full failsafe round (reads directive). - `upstreamPreForward_eth_getLogs` / `upstreamPreForward_trace_filter` — checks block-range availability before each upstream attempt (read legacy `EvmIntegrityConfig`). - `upstreamPostForward_eth_getLogs` — normalizes empty arrays in `eth_getLogs` upstream responses. [`architecture/evm/eth_getLogs.go:L349-362`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getLogs.go#L349-L362) - `networkPostForward_eth_getLogs` — network post-forward; handles reactive range splitting on 413-like oversized-range errors. [`architecture/evm/eth_getLogs.go:L364`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getLogs.go#L364) - `upstreamPostForward_eth_getBlockByNumber` — validates block header and transaction fields. - `upstreamPostForward_eth_getBlockReceipts` — validates receipt/log structure including Bloom. **Highest-block enforcement — `eth_blockNumber`.** Reads `ncfg.Evm.Integrity.EnforceHighestBlock` directly (not the directive). If the response block is below the network-known highest, eRPC replaces the response with a synthetic JSON-RPC result containing the highest hex block number — no re-request is made. Cache responses bypass enforcement entirely. [`architecture/evm/eth_blockNumber.go:L15-121`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_blockNumber.go#L15-L121) **Highest-block enforcement — `eth_getBlockByNumber[latest/finalized]`.** Reads `dirs.EnforceHighestBlock`. If the returned block is behind the network-known highest, eRPC constructs a new request for that block, sets `SkipCacheRead=true`, excludes the lagging upstream (`UseUpstream=!`), and forwards again. If the re-request also returns a lower block, `pickHighestBlock` picks whichever response has the higher block number — protecting against a corrupted state pointer. Cache responses skip enforcement. [`architecture/evm/eth_getBlockByNumber.go:L111-277`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L111-L277) **Block-range availability.** Before forwarding `eth_getLogs`, `trace_filter`, or `arbtrace_filter` to a specific upstream, `CheckBlockRangeAvailability` is called: `toBlock` first with `forceFreshIfStale=true`, `fromBlock` second with `forceFreshIfStale=false`. If either block is outside the upstream's available range, `ErrUpstreamBlockUnavailable` (retryable) is returned so the network-level retry routes to a different upstream. Block tags are resolved to hex numbers via the state poller; unresolvable tags (`pending`, `safe`, `earliest`) skip the check entirely. [`architecture/evm/block_range.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/block_range.go) **Future-block empty-result guard.** Both `enforceNonNullBlock` and `upstreamPostForward_markUnexpectedEmpty` call `emptyResultBeyondConfidence`. If the concrete block number is beyond the confidence head (latest or finalized, per `emptyResultConfidence`), the empty result is returned truthfully rather than being retried — preventing infinite retry storms on not-yet-produced blocks. Fail-open: if the network's head is unknown (≤ 0) or the request uses a block tag, the empty is always promoted to `ErrEndpointMissingData` and retried. [`architecture/evm/common.go:L55-89`](https://github.com/erpc/erpc/blob/main/architecture/evm/common.go#L55-L89) **Validation error semantics.** Structural failures produce `ErrEndpointContentValidation` — retryable at network scope (try another upstream), not retryable at the same upstream. It feeds the `validation` bucket in `ErrUpstreamsExhausted` exhaustion accounting. On the wire, clients receive HTTP 200 with a JSON-RPC error body (`code: -32603`). `ErrorStatusCode()` returns 502 but is dead code with zero call sites. [`common/errors.go:L2719-2747`](https://github.com/erpc/erpc/blob/main/common/errors.go#L2719-L2747) ### Config schema #### `networks[].directiveDefaults` — active integrity directives All boolean fields under this path set the default `RequestDirectives` value for every request on the network. Per-request headers/query-params can override. | Field | Type | Default | Behavior / footguns | |---|---|---|---| | `enforceHighestBlock` | `*bool` | `true` | Re-fetches `eth_getBlockByNumber[latest/finalized]` against a different upstream when the result is behind the known tip. **Does NOT disable `eth_blockNumber` enforcement** — that hook reads the deprecated `evm.integrity.enforceHighestBlock` field. Header: `X-ERPC-Enforce-Highest-Block`. [`common/defaults.go:L1458-1460`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1458-L1460) | | `enforceGetLogsBlockRange` | `*bool` | `true` | Before forwarding `eth_getLogs`/`trace_filter`/`arbtrace_filter`, checks both `fromBlock` and `toBlock` are within the upstream's available range. **Does NOT control the actual hooks** — those read `evm.integrity.enforceGetLogsBlockRange` directly. Header: `X-ERPC-Enforce-GetLogs-Range`. [`common/defaults.go:L1461-1463`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1461-L1463) | | `enforceNonNullTaggedBlocks` | `*bool` | `true` | Converts null `eth_getBlockByNumber` responses for tag-based requests into `ErrEndpointMissingData`. Numeric block requests always error on null regardless. Disable for chains that return null for some tags (e.g. ZKSync Era). Header: `X-ERPC-Enforce-Non-Null-Tagged-Blocks`. [`common/defaults.go:L1464-1466`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1464-L1466) | | `validateTransactionsRoot` | `*bool` | `true` | Checks `transactionsRoot` is consistent with the transaction count. Has a phantom-transaction special case for Polygon PoS and BSC. Header: `X-ERPC-Validate-Transactions-Root`. [`common/defaults.go:L1467-1469`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1467-L1469) | | `validateHeaderFieldLengths` | `*bool` | `false` | Validates hex-decoded byte lengths: `hash`, `parentHash`, `stateRoot`, `transactionsRoot`, `receiptsRoot` must each be 32 bytes; `logsBloom` must be 256 bytes. Absent fields (empty string) are skipped — additive, not exhaustive. Header: `X-ERPC-Validate-Header-Field-Lengths`. [`architecture/evm/eth_getBlockByNumber.go:L615-682`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L615-L682) | | `validateTransactionFields` | `*bool` | `false` | `eth_getBlockByNumber` hydrated only — silently skips hash-only responses. Checks tx hash is 32 bytes and no duplicate tx hashes in the block. Header: `X-ERPC-Validate-Transaction-Fields`. [`common/config.go:L2126`](https://github.com/erpc/erpc/blob/main/common/config.go#L2126) | | `validateTransactionBlockInfo` | `*bool` | `false` | `eth_getBlockByNumber` hydrated only. Verifies `tx.blockHash`, `tx.blockNumber`, and `tx.transactionIndex` match block-level values. Header: `X-ERPC-Validate-Transaction-Block-Info`. [`architecture/evm/eth_getBlockByNumber.go:L710-753`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L710-L753) | | `enforceLogIndexStrictIncrements` | `*bool` | `false` | `eth_getBlockReceipts` only. Global log index counter across ALL receipts in the block starting at 0, must increment by exactly 1 per log. Header: `X-ERPC-Enforce-Log-Index-Strict-Increments`. [`architecture/evm/eth_getBlockReceipts.go:L399-416`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L399-L416) | | `validateTxHashUniqueness` | `*bool` | `false` | Stricter than always-on duplicate check: also rejects empty `transactionHash` in receipts. Header: `X-ERPC-Validate-Tx-Hash-Uniqueness`. [`common/config.go:L2131`](https://github.com/erpc/erpc/blob/main/common/config.go#L2131) | | `validateTransactionIndex` | `*bool` | `false` | `eth_getBlockReceipts` only. Validates `transactionIndex` in each receipt equals its array position. Header: `X-ERPC-Validate-Transaction-Index`. [`common/config.go:L2132`](https://github.com/erpc/erpc/blob/main/common/config.go#L2132) | | `validateLogFields` | `*bool` | `false` | `eth_getBlockReceipts` only. Validates log address length (20 bytes), topic count (≤4), topic lengths (32 bytes each), and log/receipt cross-field consistency. Header: `X-ERPC-Validate-Log-Fields`. [`common/config.go:L2133`](https://github.com/erpc/erpc/blob/main/common/config.go#L2133) | | `validateLogsBloomEmptiness` | `*bool` | `false` | `eth_getBlockReceipts` only. Non-zero bloom → logs must exist; logs present → bloom must not be zero. Header: `X-ERPC-Validate-Logs-Bloom-Emptiness`. [`common/config.go:L2137`](https://github.com/erpc/erpc/blob/main/common/config.go#L2137) | | `validateLogsBloomMatch` | `*bool` | `false` | `eth_getBlockReceipts` only. Re-derives bloom from log addresses/topics using keccak256 and compares byte-for-byte. Most expensive validation. Header: `X-ERPC-Validate-Logs-Bloom-Match`. [`common/config.go:L2139`](https://github.com/erpc/erpc/blob/main/common/config.go#L2139) | | `validateReceiptTransactionMatch` | `*bool` | `false` | Library-mode only. Cross-validates `receipt[i].transactionHash == GroundTruthTransactions[i].hash`. Requires `GroundTruthTransactions` set programmatically. [`common/config.go:L2142`](https://github.com/erpc/erpc/blob/main/common/config.go#L2142) | | `validateContractCreation` | `*bool` | `false` | Library-mode only. Cross-validates contract creation consistency with `GroundTruthTransactions`. Requires `validateReceiptTransactionMatch`. [`common/config.go:L2143`](https://github.com/erpc/erpc/blob/main/common/config.go#L2143) | | `receiptsCountExact` | `*int64` | `nil` | Exact receipt count assertion for `eth_getBlockReceipts`. nil means no check. Header: `X-ERPC-Receipts-Count-Exact`. [`common/config.go:L2146`](https://github.com/erpc/erpc/blob/main/common/config.go#L2146) | | `receiptsCountAtLeast` | `*int64` | `nil` | Minimum receipt count assertion. Header: `X-ERPC-Receipts-Count-At-Least`. [`common/config.go:L2147`](https://github.com/erpc/erpc/blob/main/common/config.go#L2147) | | `validationExpectedBlockHash` | `*string` | `nil` | `eth_getBlockReceipts` only. Compares `receipt.blockHash` (lowercased, `0x` stripped) against this value. Receipts with empty `blockHash` are skipped. Header: `X-ERPC-Validation-Block-Hash`. [`architecture/evm/eth_getBlockReceipts.go:L168-179`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L168-L179) | | `validationExpectedBlockNumber` | `*int64` | `nil` | `eth_getBlockReceipts` only. Validates every `receipt.blockNumber` equals this value. Receipts with empty `blockNumber` are skipped. Header: `X-ERPC-Validation-Block-Number`. [`architecture/evm/eth_getBlockReceipts.go:L181-199`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L181-L199) | #### Per-request override: HTTP headers and query parameters All directives can be overridden per-request without redeploying. Booleans accept `"true"` (case-insensitive) to enable; any other value disables. Integer directives (`receiptsCountExact`, `receiptsCountAtLeast`, `validationExpectedBlockNumber`) accept a decimal integer string. `validationExpectedBlockHash` accepts an optional `0x` prefix. [`common/request.go:L45-114`](https://github.com/erpc/erpc/blob/main/common/request.go#L45-L114) | Directive | HTTP Header | Query Parameter | |---|---|---| | `enforceHighestBlock` | `X-ERPC-Enforce-Highest-Block` | `enforce-highest-block` | | `enforceGetLogsBlockRange` | `X-ERPC-Enforce-GetLogs-Range` | `enforce-getlogs-range` | | `enforceNonNullTaggedBlocks` | `X-ERPC-Enforce-Non-Null-Tagged-Blocks` | `enforce-non-null-tagged-blocks` | | `enforceLogIndexStrictIncrements` | `X-ERPC-Enforce-Log-Index-Strict-Increments` | `enforce-log-index-strict-increments` | | `validateLogsBloomEmptiness` | `X-ERPC-Validate-Logs-Bloom-Emptiness` | `validate-logs-bloom-emptiness` | | `validateLogsBloomMatch` | `X-ERPC-Validate-Logs-Bloom-Match` | `validate-logs-bloom-match` | | `validateTxHashUniqueness` | `X-ERPC-Validate-Tx-Hash-Uniqueness` | `validate-tx-hash-uniqueness` | | `validateTransactionIndex` | `X-ERPC-Validate-Transaction-Index` | `validate-transaction-index` | | `validateTransactionsRoot` | `X-ERPC-Validate-Transactions-Root` | `validate-transactions-root` | | `validateHeaderFieldLengths` | `X-ERPC-Validate-Header-Field-Lengths` | `validate-header-field-lengths` | | `validateTransactionFields` | `X-ERPC-Validate-Transaction-Fields` | `validate-transaction-fields` | | `validateTransactionBlockInfo` | `X-ERPC-Validate-Transaction-Block-Info` | `validate-transaction-block-info` | | `validateLogFields` | `X-ERPC-Validate-Log-Fields` | `validate-log-fields` | | `receiptsCountExact` | `X-ERPC-Receipts-Count-Exact` | `receipts-count-exact` | | `receiptsCountAtLeast` | `X-ERPC-Receipts-Count-At-Least` | `receipts-count-at-least` | | `validationExpectedBlockHash` | `X-ERPC-Validation-Block-Hash` | `validation-block-hash` | | `validationExpectedBlockNumber` | `X-ERPC-Validation-Block-Number` | `validation-block-number` | `GroundTruthTransactions` and `GroundTruthLogs` are **not exposable via HTTP** — they can only be set programmatically on `NormalizedRequest`. [`common/request.go:L210-215`](https://github.com/erpc/erpc/blob/main/common/request.go#L210-L215) #### `networks[].evm.integrity` — deprecated (`EvmIntegrityConfig`) | Field | Type | Default | Notes | |---|---|---|---| | `enforceHighestBlock` | `*bool` | `nil` → `true` via `EvmIntegrityConfig.SetDefaults` | **@deprecated** — use `directiveDefaults.enforceHighestBlock`. Still read directly by `projectPreForward_eth_blockNumber` at runtime. Setting `directiveDefaults.enforceHighestBlock: false` without also setting this to `false` leaves `eth_blockNumber` enforcement active. [`common/config.go:L2310`](https://github.com/erpc/erpc/blob/main/common/config.go#L2310) [`common/defaults.go:L2117-2120`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2117-L2120) | | `enforceGetLogsBlockRange` | `*bool` | `nil` → `true` via `EvmIntegrityConfig.SetDefaults` | **@deprecated** — use `directiveDefaults.enforceGetLogsBlockRange`. Still read by `eth_getLogs` and `trace_filter` hooks at runtime. [`common/config.go:L2312`](https://github.com/erpc/erpc/blob/main/common/config.go#L2312) [`common/defaults.go:L2121-2123`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2121-L2123) | | `enforceNonNullTaggedBlocks` | `*bool` | `nil` → `true` via `EvmIntegrityConfig.SetDefaults` | **@deprecated** — use `directiveDefaults.enforceNonNullTaggedBlocks`. Only acts as migration source; not read at runtime. [`common/config.go:L2314`](https://github.com/erpc/erpc/blob/main/common/config.go#L2314) [`common/defaults.go:L2124-2126`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2124-L2126) | Migration: `NetworkConfig.SetDefaults` performs the migration in this order: (1) if `DirectiveDefaults == nil`, initialize to an empty struct; (2) if `evm.Integrity != nil` (user populated the deprecated block) AND `directiveDefaults. == nil` (user has NOT set the new field), copy the value; (3) call `DirectiveDefaultsConfig.SetDefaults()` which fills `true` for any still-nil boolean fields. The migration is best-effort copy that preserves explicit `false` values from the old config. No warning is emitted. [`common/defaults.go:L1952-1964`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1952-L1964) #### `networks[].evm.emptyResultConfidence` | Field | Type | Default | Notes | |---|---|---|---| | `emptyResultConfidence` | `AvailabilityConfidence` | `"blockHead"` | `"blockHead"` (default): retries empty results for blocks at/below latest tip; blocks above it return null truthfully. `"finalizedBlock"`: retries only for blocks at/below the finalized tip — use when upstreams legitimately return empty for recent unfinalized blocks. Fail-open: if head is unknown or request uses a tag, always retries. [`common/defaults.go:L2078`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L2078) | #### `upstreams[].integrity` — non-functional placeholder | Field | Type | Default | Notes | |---|---|---|---| | `integrity.eth_getBlockReceipts.enabled` | `bool` | `false` | Not read by any validation code. Has no effect. Use `directiveDefaults` instead. [`common/config.go:L1006-1010`](https://github.com/erpc/erpc/blob/main/common/config.go#L1006-L1010) | | `integrity.eth_getBlockReceipts.checkLogIndexStrictIncrements` | `*bool` | `nil` | Reserved / future. Not read by any code. Active control: `directiveDefaults.enforceLogIndexStrictIncrements`. [`common/config.go:L1008`](https://github.com/erpc/erpc/blob/main/common/config.go#L1008) | | `integrity.eth_getBlockReceipts.checkLogsBloom` | `*bool` | `nil` | Reserved / future. Not read by any code. Active control: `directiveDefaults.validateLogsBloomMatch`. [`common/config.go:L1009`](https://github.com/erpc/erpc/blob/main/common/config.go#L1009) | ### Worked examples All patterns below are distilled from real production fleets; comments explain the non-obvious choices. **1. Mainnet Ethereum — baseline defaults with `evm.integrity` for full coverage.** Production uses `evm.integrity` in networkDefaults so both the `eth_blockNumber` hook (which reads the deprecated field directly) and `eth_getBlockByNumber` enforcement (which reads the directive) are both active. Setting only `directiveDefaults` would leave `eth_blockNumber` enforcement silently active via the legacy path — explicitly setting both closes the gap: **Config path:** `projects[].networks[].evm` **YAML — `erpc.yaml`:** ```yaml evm: chainId: 1 # enforceHighestBlock here feeds the eth_blockNumber pre-forward hook, # which reads the deprecated EvmIntegrityConfig field directly at runtime. # directiveDefaults.enforceHighestBlock controls eth_getBlockByNumber only. integrity: enforceHighestBlock: true enforceGetLogsBlockRange: true ``` **TypeScript — `erpc.ts`:** ```typescript evm: { chainId: 1, // enforceHighestBlock here feeds the eth_blockNumber pre-forward hook, // which reads the deprecated EvmIntegrityConfig field directly at runtime. // directiveDefaults.enforceHighestBlock controls eth_getBlockByNumber only. integrity: { enforceHighestBlock: true, enforceGetLogsBlockRange: true, }, } ``` **2. ZKSync Era / Sophon / Abstract / Kaia / Filecoin / IOTA / Etherlink — non-standard chains.** A large class of ZK-rollups and non-EVM-standard chains legitimately returns null for certain tagged blocks and uses a non-standard `transactionsRoot` encoding. Both checks are disabled; all other defaults remain active: **Config path:** `projects[].networks[]` **YAML — `erpc.yaml`:** ```yaml networks: - evm: chainId: 324 # ZKSync Era returns null for 'finalized'/'safe' tags intermittently — # enforcing non-null would cause infinite retries on a correct response. evm: integrity: enforceNonNullTaggedBlocks: false directiveDefaults: # ZKSync Era uses a non-standard txRoot (ZK proof root, not trie root) — # the default validateTransactionsRoot check would reject every block. validateTransactionsRoot: false ``` **TypeScript — `erpc.ts`:** ```typescript networks: [{ evm: { chainId: 324 }, // ZKSync Era returns null for 'finalized'/'safe' tags intermittently — // enforcing non-null would cause infinite retries on a correct response. evm: { integrity: { enforceNonNullTaggedBlocks: false }, }, directiveDefaults: { // ZKSync Era uses a non-standard txRoot (ZK proof root, not trie root) — // the default validateTransactionsRoot check would reject every block. validateTransactionsRoot: false, }, }] ``` **3. Archive / ETL indexer backfill — full opt-in suite.** An indexer backfilling historical blocks needs maximum structural confidence. The full opt-in set adds CPU cost on the eRPC side, not extra RPC calls — bad responses are silently retried against a different upstream before the indexer ever sees them: **Config path:** `projects[].networks[].directiveDefaults` **YAML — `erpc.yaml`:** ```yaml directiveDefaults: # defaults (already on — listed explicitly for reviewability): enforceHighestBlock: true enforceGetLogsBlockRange: true enforceNonNullTaggedBlocks: true validateTransactionsRoot: true # opt-in for indexer hardening — each adds CPU cost, not extra RPC calls: validateHeaderFieldLengths: true # catch truncated/corrupted hash fields validateTransactionFields: true # tx hash length + uniqueness (hydrated only) validateTransactionBlockInfo: true # tx.blockHash/Number/Index vs block header validateLogsBloomMatch: true # full keccak256 Bloom recompute (most expensive) enforceLogIndexStrictIncrements: true # global log index 0,1,2,…N across all receipts ``` **TypeScript — `erpc.ts`:** ```typescript directiveDefaults: { // defaults (already on — listed explicitly for reviewability): enforceHighestBlock: true, enforceGetLogsBlockRange: true, enforceNonNullTaggedBlocks: true, validateTransactionsRoot: true, // opt-in for indexer hardening — each adds CPU cost, not extra RPC calls: validateHeaderFieldLengths: true, validateTransactionFields: true, validateTransactionBlockInfo: true, // full keccak256 Bloom recompute — most expensive validator validateLogsBloomMatch: true, enforceLogIndexStrictIncrements: true, } ``` **4. Archive node backfill — stop retry storms on unfinalized blocks.** The default `blockHead` confidence retries empty results for any block at/below the latest tip. Archive nodes legitimately return empty for recent unfinalized blocks; switching to `finalizedBlock` prevents the storm: **Config path:** `projects[].networks[].evm` **YAML — `erpc.yaml`:** ```yaml evm: chainId: 1 # Default 'blockHead' retries empty results for blocks at/below the latest tip. # Archive backfills return empty for unfinalized blocks normally — use 'finalizedBlock' # so only truly confirmed blocks trigger a retry; unfinalized blocks return empty truthfully. emptyResultConfidence: finalizedBlock ``` **TypeScript — `erpc.ts`:** ```typescript evm: { chainId: 1, // Default 'blockHead' retries empty for blocks at/below the latest tip. // Archive backfills return empty for unfinalized blocks normally — use 'finalizedBlock' // so only truly confirmed blocks trigger a retry; unfinalized blocks return empty truthfully. emptyResultConfidence: "finalizedBlock", } ``` **5. Per-request Bloom check for a critical path.** Rather than enabling `validateLogsBloomMatch` globally (full keccak256 recompute per receipt), send the header only from the caller path where it matters — e.g. an indexer's block-completion verification step: ``` X-ERPC-Validate-Logs-Bloom-Match: true ``` All directives have a corresponding `X-ERPC-*` header — see the table in "Config schema" for the full mapping. This applies to any of the opt-in validators; use it to surgically enable expensive checks without paying the CPU cost on every request. ### Request/response behavior - Integrity violations produce `ErrEndpointContentValidation` — HTTP 200 with JSON-RPC error body `{"code": -32603, "message": "..."}`. `ErrorStatusCode()` returns 502 but is dead code with zero call sites. [`erpc/http_server.go:L1473-1491`](https://github.com/erpc/erpc/blob/main/erpc/http_server.go#L1473-L1491) - `ErrEndpointContentValidation` is retryable at network scope (try another upstream) but NOT retryable at the same upstream. [`common/errors.go:L2719-2747`](https://github.com/erpc/erpc/blob/main/common/errors.go#L2719-L2747) - `ErrUpstreamBlockUnavailable` (block-range failure) is retryable — the network-level retry routes to a different upstream. - Cache responses bypass all highest-block enforcement — a cached `eth_getBlockByNumber[latest]` will not be re-fetched even if a newer block is known. - The `eth_blockNumber` enforcement response is synthetic: eRPC returns the highest-known hex block number directly; no upstream re-request is made. - Always-on checks for `eth_getBlockReceipts` (no directive required): duplicate `transactionHash` across receipts; all receipts sharing the same `blockHash` if present. [`architecture/evm/eth_getBlockReceipts.go:L55-460`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L55-L460) ### Best practices - **Do not try to disable `eth_blockNumber` enforcement via `directiveDefaults` alone.** That hook reads the deprecated `evm.integrity.enforceHighestBlock` field. To disable it you must also set `evm.integrity.enforceHighestBlock: false`. - **Same split for `eth_getLogs` and `trace_filter`.** Setting `directiveDefaults.enforceGetLogsBlockRange: false` has no effect on those hooks. Set `evm.integrity.enforceGetLogsBlockRange: false` to actually disable range checks. - **Set `emptyResultConfidence: finalizedBlock` for archive workloads.** The default `blockHead` retries empty results for any block at/below the latest tip — this causes retry storms when archive nodes legitimately return empty for unfinalized blocks. - **Prefer per-request headers for expensive validators.** `validateLogsBloomMatch` (full Bloom recompute) and `validateHeaderFieldLengths` add CPU cost per response. Enable globally only on networks where upstream data quality warrants it; use `X-ERPC-Validate-Logs-Bloom-Match: true` headers for critical-path requests only. - **`validateTransactionsRoot` is on by default** but can be disabled per-request via `X-ERPC-Validate-Transactions-Root: false`. Useful for Polygon PoS / BSC if phantom-transaction detection isn't firing correctly. - **`upstreams[].integrity` has no effect** — all fields are non-functional placeholders. All active receipt/log validation is controlled exclusively through `networks[].directiveDefaults.*`. - **Monitor `erpc_upstream_stale_latest_block_total`** by upstream — a consistently high count for one upstream signals it is perpetually behind and should be down-weighted or removed. ### Edge cases & gotchas 1. **`eth_blockNumber` dual-path.** `projectPreForward_eth_blockNumber` reads `ncfg.Evm.Integrity.EnforceHighestBlock` (deprecated field), while `eth_getBlockByNumber` reads `dirs.EnforceHighestBlock` (directive). Setting `directiveDefaults.enforceHighestBlock: false` disables the `eth_getBlockByNumber` path but leaves `eth_blockNumber` enforcement active. To disable both, also set `evm.integrity.enforceHighestBlock: false`. [`architecture/evm/eth_blockNumber.go:L23-29`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_blockNumber.go#L23-L29) 2. **`eth_getLogs` / `trace_filter` dual-path.** Both `upstreamPreForward_eth_getLogs` and `upstreamPreForward_trace_filter` read `ncfg.Evm.Integrity.EnforceGetLogsBlockRange` directly. Setting only `directiveDefaults.enforceGetLogsBlockRange: false` has no effect on those hooks. [`architecture/evm/eth_getLogs.go:L283-285`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getLogs.go#L283-L285) 3. **Cache responses bypass enforcement.** Both `eth_blockNumber` and `eth_getBlockByNumber` hooks skip when `resp.FromCache()` is true. A cached `eth_getBlockByNumber[latest]` will not be upgraded even if a newer block is known. Tune realtime cache TTLs to limit the staleness window. 4. **`emptyResultBeyondConfidence` fail-open.** If the network's head is unknown (≤ 0) or the request uses a block tag (`latest`, `finalized`, etc.) rather than a concrete number, the guard always treats the empty as missing data and retries — regardless of `emptyResultConfidence`. 5. **Unresolvable block tags in `eth_getLogs` skip range check.** Tags like `pending`, `safe`, or `earliest` that cannot be resolved by the state poller cause the entire range check to be skipped; the request is forwarded to the upstream unchecked. [`architecture/evm/eth_getLogs.go:L327-333`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getLogs.go#L327-L333) 6. **`trace_filter` requires hex-prefixed block values.** Unlike `eth_getLogs`, the `trace_filter` hook does not resolve block tags. If `fromBlock`/`toBlock` are not `0x`-prefixed, the hook returns immediately without checking. [`architecture/evm/trace_filter.go:L323-341`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L323-L341) 7. **`enforceLogIndexStrictIncrements` uses a global counter, not per-receipt.** Log indices must be globally contiguous across ALL receipts in the block starting from 0. A pattern of `[0,1], [1]` (two receipts) fails because the second receipt's first log (1) conflicts with the global counter expecting 2. [`architecture/evm/eth_getBlockReceipts.go:L399-416`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L399-L416) 8. **`validationExpectedBlockHash` and `validationExpectedBlockNumber` only apply to `eth_getBlockReceipts`.** Using them for `eth_getBlockByNumber` validation has no effect — those fields are only checked inside `validateGetBlockReceipts`. 9. **Phantom transactions on Polygon PoS, BSC.** `validateTransactionsRoot` has a special case for system transactions with `from=0x0, gas=0x0`. If all transactions are hash-only (not full objects), they are conservatively treated as non-phantom. [`architecture/evm/eth_getBlockByNumber.go:L574-613`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L574-L613) 10. **`upstreams[].integrity` block is entirely non-functional.** All three fields (`enabled`, `checkLogIndexStrictIncrements`, `checkLogsBloom`) are defined in the struct but read by no validation code. Use `networks[].directiveDefaults.*` instead. [`common/config.go:L1006-1031`](https://github.com/erpc/erpc/blob/main/common/config.go#L1006-L1031) 11. **`validateTransactionFields` and `validateTransactionBlockInfo` silently skip hash-only transactions.** Both checks are inside `if len(fullTxs) > 0` — if all transactions are hash strings (hydrated=false), `fullTxs` is empty and neither directive fires. No error, no warning. 12. **`pickHighestBlock` protects against a corrupted state pointer.** After re-fetching against the highest-known block, if the re-fetch returns a lower block than the original response, `pickHighestBlock` returns the original. [`architecture/evm/eth_getBlockByNumber.go:L335-415`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L335-L415) 13. **`validateLogsBloomMatch` uses a 2048-bit bloom filter with big-endian bit-setting.** The `bloomAdd` function follows the standard EIP bloom algorithm: 3 byte-positions set per value, derived from keccak256 of the input; bit position index computed as `byteIndex = 255 - int(bitpos>>3)` (big-endian). The recomputed bloom is compared byte-for-byte with `receipt.logsBloom`. [`architecture/evm/eth_getBlockReceipts.go:L468-482`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L468-L482) 14. **Numeric block requests for `eth_getBlockByNumber` ALWAYS error on null — no directive gate.** `enforceNonNullBlock` treats numeric (non-tag) block requests as unconditionally erroring when the response is null, unless `emptyResultBeyondConfidence` exempts the block (i.e., the block is beyond the confidence head). The `enforceNonNullTaggedBlocks` directive only gates the tagged-block path. [`architecture/evm/eth_getBlockByNumber.go:L279-333`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L279-L333) ### Observability | Metric | Type | Labels | When it fires | |---|---|---|---| | `erpc_upstream_stale_latest_block_total` | counter | project, vendor, network, upstream, category | Upstream returned a block number less than the network-known latest head. Fired by both `eth_blockNumber` and `eth_getBlockByNumber[latest]` enforcement. `category` = method name. | | `erpc_upstream_stale_finalized_block_total` | counter | project, vendor, network, upstream | Upstream returned a finalized block less than the network-known finalized head. Fired by `eth_getBlockByNumber[finalized]` enforcement. | | `erpc_upstream_stale_upper_bound_total` | counter | project, vendor, network, upstream, category, confidence | Request skipped: upstream's latest block < requested `toBlock`. Fired during upstream selection. | | `erpc_upstream_stale_lower_bound_total` | counter | project, vendor, network, upstream, category, confidence | Request skipped: requested `fromBlock` is below upstream's available lower bound. | | `erpc_upstream_attempt_outcome_total` | counter | project, network, upstream, category, outcome, … | `outcome=block_unavailable` when range check fails; `outcome=missing_data` for null responses. | | `erpc_network_retry_attempt_total` | counter | project, network, category, reason, finality | `reason=block_unavailable` on block-range retry; `reason=missing_data` on null-response retry. | **OTel trace spans:** - `Project.PreForwardHook.eth_blockNumber` — spans `eth_blockNumber` highest-block enforcement; attributes include `block.number_lag` in detailed tracing mode. - `Network.PostForward.eth_getBlockByNumber` — spans block enforcement and null check. - `Upstream.PreForwardHook.eth_getLogs` / `Upstream.PreForwardHook.trace_filter` — spans block-range availability checks. - `Upstream.PostForwardHook.eth_getBlockByNumber` — spans upstream block structure validation. - `Upstream.PostForwardHook.eth_getBlockReceipts` — spans upstream receipt/log validation. - `Evm.PickHighestBlock` — spans the `pickHighestBlock` comparison. **Notable log messages:** - `DEBUG "upstream returned older block than we known, falling back to highest known block"` — `eth_blockNumber` stale detection. - `DEBUG "enforcing highest latest block"` / `"enforcing highest finalized block"` — `eth_getBlockByNumber` stale detection. - `TRACE "skipping enforcement of highest block number as response is from cache"` — cache bypass. - `WARN "passed upstream is not a common.EvmUpstream"` — emitted in `eth_getLogs` and `trace_filter` pre-forward hooks when the upstream type assertion fails; the range check is skipped. ### Source code entry points - [`architecture/evm/eth_blockNumber.go:L15-L121`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_blockNumber.go#L15-L121) — `projectPreForward_eth_blockNumber`: reads legacy `EvmIntegrityConfig.EnforceHighestBlock`; replaces stale responses with synthetic highest-block result. - [`architecture/evm/eth_getBlockByNumber.go:L43-L109`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L43-L109) — `networkPostForward_eth_getBlockByNumber`: entry point for highest-block + non-null network-level enforcement. - [`architecture/evm/eth_getBlockByNumber.go:L489-L756`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockByNumber.go#L489-L756) — `validateBlock`: upstream-level block structure validation (`validateHeaderFieldLengths`, `validateBlockTransactions`). - [`architecture/evm/eth_getLogs.go:L273-L347`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getLogs.go#L273-L347) — `upstreamPreForward_eth_getLogs`: block-range availability guard; tag resolution. - [`architecture/evm/eth_getBlockReceipts.go:L55-L460`](https://github.com/erpc/erpc/blob/main/architecture/evm/eth_getBlockReceipts.go#L55-L460) — `validateGetBlockReceipts`: full receipt/log structural validation including Bloom recompute. - [`architecture/evm/trace_filter.go:L280-L356`](https://github.com/erpc/erpc/blob/main/architecture/evm/trace_filter.go#L280-L356) — `upstreamPreForward_trace_filter`: block-range availability for `trace_filter` / `arbtrace_filter`. - [`architecture/evm/block_range.go`](https://github.com/erpc/erpc/blob/main/architecture/evm/block_range.go) — `CheckBlockRangeAvailability`: shared availability check using `EvmAssertBlockAvailability`. - [`architecture/evm/common.go:L55-L89`](https://github.com/erpc/erpc/blob/main/architecture/evm/common.go#L55-L89) — `emptyResultBeyondConfidence`: confidence-head guard for empty/null results. - [`common/defaults.go:L1454-L1471`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1454-L1471) — `DirectiveDefaultsConfig.SetDefaults`: sets the four always-on defaults. - [`common/defaults.go:L1952-L1964`](https://github.com/erpc/erpc/blob/main/common/defaults.go#L1952-L1964) — migration from `EvmIntegrityConfig` to `DirectiveDefaultsConfig`. - [`common/request.go:L116-L215`](https://github.com/erpc/erpc/blob/main/common/request.go#L116-L215) — `RequestDirectives` struct: the runtime directive surface. - [`common/request.go:L45-L114`](https://github.com/erpc/erpc/blob/main/common/request.go#L45-L114) — HTTP header and query-parameter names for per-request override. ### Related pages - [Retry](/config/failsafe/retry.llms.txt) — integrity violations are retryable; retry routes them to a fresh upstream. - [Timeout](/config/failsafe/timeout.llms.txt) — bounds the full re-fetch triggered by highest-block enforcement. - [Selection policies](/config/projects/selection-policies.llms.txt) — upstream scoring uses stale-block metrics produced by integrity enforcement. - [Rate limiters](/config/rate-limiters.llms.txt) — keep re-fetch bursts from blowing a provider budget. - [Survive provider outages](/use-cases/survive-provider-outages.llms.txt) — the broader scenario this feature serves. --- ## Navigation (machine-readable surface) - Up: [Failsafe](https://docs.erpc.cloud/config/failsafe.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 - [Circuit breaker](https://docs.erpc.cloud/config/failsafe/circuit-breaker.llms.txt) — When an upstream starts failing, eRPC stops sending it traffic automatically — and quietly brings it back once it recovers. - [Consensus](https://docs.erpc.cloud/config/failsafe/consensus.llms.txt) — Fan out every request to multiple providers simultaneously, agree on a single canonical answer, and automatically flag — or silence — the ones that lie. - [Hedge](https://docs.erpc.cloud/config/failsafe/hedge.llms.txt) — When a provider is having a slow moment, eRPC quietly races a backup request — your slowest responses simply disappear. - [Retry](https://docs.erpc.cloud/config/failsafe/retry.llms.txt) — When a provider misbehaves, eRPC automatically rotates to the next one — and paces retries for missing data to match the chain's own block time. - [Timeout](https://docs.erpc.cloud/config/failsafe/timeout.llms.txt) — Give every request a hard latency budget — three nested layers keep stalled upstreams from tying up your connections indefinitely.