Integrity & Empty Data
AIOpen as plain markdown for AIRPC nodes may return stale, empty, or structurally invalid data — especially near the tip of the chain. eRPC's integrity layer enforces block tracking, validates response structure, and handles empty or unavailable results by retrying across upstreams automatically. All controls live in directiveDefaults on the network (or networkDefaults) and in failsafe[].retry for empty-result tuning.
You can configure:
- Block tracking —
enforceHighestBlock,enforceGetLogsBlockRange,enforceNonNullTaggedBlocks - Transaction and header validation —
validateTransactionsRoot,validateTransactionFields,validateTransactionBlockInfo,validateHeaderFieldLengths - Log and receipt validation —
validateLogFields,validateLogsBloomEmptiness,validateLogsBloomMatch,enforceLogIndexStrictIncrements,validateTxHashUniqueness,validateTransactionIndex - Receipt cross-checks —
validateReceiptTransactionMatch,validateContractCreation,receiptsCountExact,receiptsCountAtLeast,validationExpectedBlockHash,validationExpectedBlockNumber - Empty/missing result handling —
retry.emptyResultAccept,emptyResultConfidence,emptyResultMaxAttempts,emptyResultDelay,blockUnavailableDelay - Per-network empty-as-error promotion —
evm.markEmptyAsErrorMethods
Minimum useful config
Enable the three most common block-tracking directives and configure empty-result retry behavior:
projects: - id: main networks: - architecture: evm evm: chainId: 1 directiveDefaults: enforceHighestBlock: true # serve the freshest latest/finalized across all upstreams enforceGetLogsBlockRange: true # skip upstreams that don't have the requested range enforceNonNullTaggedBlocks: true # treat null for tagged blocks as an error retryEmpty: true # retry empty responses on other upstreamsResponse validation for indexers
Validation directives are disabled by default (they add JSON-parsing overhead). Enable them for high-integrity workloads like indexing, where structurally corrupt responses must be rejected and retried:
projects: - id: main networks: - architecture: evm evm: chainId: 1 directiveDefaults: validateLogsBloomEmptiness: true # logs exist iff bloom is non-zero validateLogsBloomMatch: true # recompute bloom from logs and verify enforceLogIndexStrictIncrements: true validateTxHashUniqueness: true validateTransactionIndex: true # Recommended for indexers: hedge + consensus + retry # Invalid responses are rejected, valid ones are compared, retries if all fail failsafe: - matchMethod: eth_getBlockReceipts hedge: maxCount: 3 delay: 100ms consensus: maxParticipants: 3 agreementThreshold: 2 retry: maxAttempts: 5Empty-result retry tuning (chain-specific)
This example tunes empty-result handling for Polygon (2.3 s blocks). blockUnavailableDelay and emptyResultDelay only fire when relevant — for finalized blocks the delay is skipped automatically, so one retry policy works across all finalities.
projects: - id: main networks: - architecture: evm evm: chainId: 137 directiveDefaults: retryEmpty: true failsafe: # Unfinalized logs/receipts: consensus guards against reorg splits - matchMethod: eth_getLogs|eth_getBlockReceipts matchFinality: [unfinalized, unknown] retry: &retry-polygon maxAttempts: 6 delay: 0ms blockUnavailableDelay: 1s emptyResultDelay: 500ms emptyResultMaxAttempts: 3 emptyResultConfidence: blockHead emptyResultAccept: [eth_getLogs] consensus: maxParticipants: 3 agreementThreshold: 2 # All other logs/receipts - matchMethod: eth_getLogs|eth_getBlockReceipts retry: *retry-polygon # eth_call: empty is a valid contract return - matchMethod: eth_call retry: <<: *retry-polygon emptyResultAccept: [eth_call] # Everything else - matchMethod: '*' retry: <<: *retry-polygon emptyResultMaxAttempts: 2Copy for your AI assistant — full integrity & empty-data referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
Block tracking directives (directiveDefaults)
| Directive | Default | Notes |
|---|---|---|
enforceHighestBlock | true | Track the highest block seen across all upstreams. For eth_blockNumber, replaces stale values with the known highest. For eth_getBlockByNumber("latest"|"finalized"), retries on other upstreams when the response is behind the known tip. Metrics: erpc_upstream_stale_latest_block_total, erpc_upstream_stale_finalized_block_total. |
enforceGetLogsBlockRange | true | Before sending eth_getLogs, trace_filter, or arbtrace_filter, verify the upstream has the requested block range. Skips upstreams whose toBlock exceeds their latest, or whose fromBlock is below their available window (maxAvailableRecentBlocks). Forces a fresh poll if the upstream's latest is stale before deciding. Metrics: erpc_upstream_evm_get_logs_stale_upper_bound_total, erpc_upstream_evm_get_logs_stale_lower_bound_total. |
enforceNonNullTaggedBlocks | true | Convert null responses to errors for eth_getBlockByNumber when called with "latest", "pending", "safe", "finalized", or "earliest". Numeric block requests are always errors when null regardless of this setting. Set false for chains that legitimately return null for some tags (e.g. zkSync). |
Transaction and header validation directives
All disabled by default. Enable for high-integrity use-cases where the overhead of JSON parsing is acceptable.
| Directive | Header | Notes |
|---|---|---|
validateTransactionsRoot | X-ERPC-Validate-Transactions-Root | Recompute the transactions root from the block's transaction list and verify it matches the header. Catches truncated or reordered tx lists. |
validateTransactionFields | X-ERPC-Validate-Transaction-Fields | Validate field formats (hex lengths, required keys) for each transaction in a block. Rejects responses with malformed fields. |
validateTransactionBlockInfo | X-ERPC-Validate-Transaction-Block-Info | Verify that each transaction's embedded blockHash and blockNumber match the containing block. Catches upstreams returning cross-contaminated responses. |
validateHeaderFieldLengths | X-ERPC-Validate-Header-Field-Lengths | Check byte lengths of block header fields (hash, parentHash, etc.). Rejects headers with truncated or padded values. |
Log and receipt validation directives
| Directive | Header | Notes |
|---|---|---|
validateLogFields | X-ERPC-Validate-Log-Fields | Validate log address and topic lengths. Rejects logs with malformed addresses (not 20 bytes) or topics (not 32 bytes). |
validateLogsBloomEmptiness | X-ERPC-Validate-Logs-Bloom-Emptiness | Consistency check: if logs are present the bloom must be non-zero; if logs are absent the bloom must be zero. Catches upstreams that zero out the bloom without zeroing the logs. |
validateLogsBloomMatch | X-ERPC-Validate-Logs-Bloom-Match | Recompute the bloom filter from the actual logs and compare to the header. Most expensive validation — only enable when you need to guarantee bloom correctness (e.g. indexers that rely on bloom-based filtering). |
enforceLogIndexStrictIncrements | X-ERPC-Enforce-Log-Index-Strict-Increments | Log indices must increment by exactly 1 across all receipts in a block response. Catches upstreams that return receipts with gaps or duplicated log indices. |
validateTxHashUniqueness | X-ERPC-Validate-Tx-Hash-Uniqueness | No transaction hash may appear more than once in a block's receipt set. Catches duplicate-receipt bugs in some upstreams. |
validateTransactionIndex | X-ERPC-Validate-Transaction-Index | Receipt transaction indices must be sequential starting from 0. Rejects responses with out-of-order or missing indices. |
Receipt cross-check directives
| Directive | Header | Notes |
|---|---|---|
validateReceiptTransactionMatch | X-ERPC-Validate-Receipt-Transaction-Match | Cross-validate that each receipt's fields match the corresponding transaction (e.g. to, from, contractAddress). Requires the ground-truth transactions to be available (library/indexer mode). |
validateContractCreation | X-ERPC-Validate-Contract-Creation | Verify that contractAddress is populated when to is null (contract creation tx) and absent otherwise. |
receiptsCountExact | X-ERPC-Receipts-Count-Exact | Integer. The eth_getBlockReceipts response must contain exactly this many receipts. Useful when the caller knows the transaction count from the block header. |
receiptsCountAtLeast | X-ERPC-Receipts-Count-At-Least | Integer. The response must contain at least this many receipts. Softer than receiptsCountExact — allows appended receipts (e.g. from internal transactions on some chains). |
validationExpectedBlockHash | X-ERPC-Validation-Expected-Block-Hash | Hex string. All receipts in the response must carry this block hash. Rejects responses that mix receipts from different blocks. |
validationExpectedBlockNumber | X-ERPC-Validation-Expected-Block-Number | Hex or decimal. All receipts must carry this block number. Same purpose as validationExpectedBlockHash but keyed by number. |
Empty/missing data handling — failsafe[].retry fields
| Field | Default | Notes |
|---|---|---|
emptyResultAccept | ["eth_getLogs", "eth_call"] | Methods where an empty response is valid data — never retry on empty for these. eth_getLogs returns [] for blocks with no matching logs. eth_call returns 0x for calls that return nothing. |
emptyResultConfidence | blockHead | When to trust an empty result: blockHead (block ≤ upstream's latest) or finalizedBlock (block ≤ finalized). finalizedBlock is more conservative: empties below the finalized horizon are trusted; empties in unfinalized range are retried. |
emptyResultMaxAttempts | = maxAttempts | Separate attempt cap for empty-result retries. Set lower than maxAttempts when you want aggressive retries for errors but lighter retries for empties. |
emptyResultDelay | = delay | Fixed delay between empty-result retries. Overrides the normal delay for this case. Set to roughly one block time divided by your emptyResultMaxAttempts. |
blockUnavailableDelay | dynamic | Fallback delay when ALL upstreams lack the requested block. When the network's dynamic block-time estimate is available, the delay is derived automatically as blockTime × blockUnavailableDelayMultiplier (default 0.8). This static value is only used during warmup (first few seconds). Tune the multiplier via evm.blockUnavailableDelayMultiplier on the network. |
Choosing emptyResultDelay by chain:
| Chain | Block time | emptyResultDelay | emptyResultMaxAttempts |
|---|---|---|---|
| Ethereum | 12 s | 2000ms | 3 |
| Polygon | 2.3 s | 500ms | 5-6 |
| Base / Optimism | 2 s | 500ms | 4-5 |
| Arbitrum | 250 ms | 200ms | 5 |
| Monad | 500 ms | 200ms | 3 |
directiveDefaults.retryEmpty
| Field | Default | Notes |
|---|---|---|
retryEmpty | true | When true, eRPC treats empty responses (null, [], 0x) from non-emptyResultAccept methods as retryable — the request is replayed on other upstreams. When false, the first empty response is returned as-is. Disable only if your application explicitly handles empty responses itself. |
evm.markEmptyAsErrorMethods (per-network)
Promote empty responses to hard errors (not just retryable empties) for specific methods. Retried AND counted as upstream errors (affects scoring and circuit breaker):
networks:
- architecture: evm
evm:
chainId: 1
markEmptyAsErrorMethods:
- eth_getTransactionReceipt # empty means tx missing, not pending on this chaineRPC ships with sensible defaults that cover common point-lookup methods (eth_getBlockByNumber, eth_getBlockReceipts, eth_getTransactionByHash, traces, etc.). Only add entries when an empty response on that method specifically means the upstream lacks the data rather than "not found."
eth_getBlockByHash and eth_getTransactionReceipt are intentionally excluded from defaults: subgraph upstreams commonly return null for eth_getBlockByHash, and eth_getTransactionReceipt returns null for pending transactions.
Per-upstream integrity — eth_getBlockReceipts checks
Two per-upstream flags gate which checks run locally on that upstream's responses (in addition to whatever directiveDefaults enable network-wide):
| Field | Path | Notes |
|---|---|---|
checkLogIndexStrictIncrements | upstreams[].evm | Upstream-local equivalent of enforceLogIndexStrictIncrements. Marks the upstream unhealthy when its receipts fail this check, without requiring the network-wide directive. |
checkLogsBloom | upstreams[].evm.integrity.eth_getBlockReceipts | Upstream-local bloom check for eth_getBlockReceipts responses: recomputes the bloom union of all receipt logs and verifies it matches the block header's logsBloom. Catches missing logs in the receipts response. Marks the upstream unhealthy on mismatch, without requiring the network-wide directive. |
checkLogsBloom vs validateLogsBloomMatch — two different checks at two different layers:
evm.integrity.eth_getBlockReceipts.checkLogsBloom(upstream level) — checks that the bloom union computed from the receipts' logs equals the block header'slogsBloom. Fires oneth_getBlockReceiptsresponses only. Catches upstreams that return receipts with missing or truncated log entries.directiveDefaults.validateLogsBloomMatch(network level) — checks that the addresses and topics ineth_getLogsresults are consistent with the block headers' bloom filters. Fires oneth_getLogsresponses. Catches upstreams that drop log entries from a getLogs response without reflecting that in the bloom.
Deprecated: evm.integrity block
The old network.evm.integrity block is deprecated. It is still accepted but emits a deprecation warning. Migrate to directiveDefaults:
# Old (deprecated — still works):
networks:
- architecture: evm
evm:
chainId: 1
integrity:
enforceHighestBlock: true
enforceGetLogsBlockRange: true
enforceNonNullTaggedBlocks: true
# New (use this):
networks:
- architecture: evm
evm: { chainId: 1 }
directiveDefaults:
enforceHighestBlock: true
enforceGetLogsBlockRange: true
enforceNonNullTaggedBlocks: trueThe directiveDefaults form is a superset — it exposes all transaction, log, and receipt validation fields that the old integrity block never covered. See Networks for the full directiveDefaults reference.
Common pitfalls
- Validation directives are off by default — they add JSON-parsing overhead on every response. Enable only what you actually need. For most non-indexer use-cases, the block-tracking directives (
enforceHighestBlock,enforceGetLogsBlockRange) are sufficient. validateLogsBloomMatchis the most expensive — it recomputes the bloom filter from every log in the response. Enable only oneth_getBlockReceiptsor similar receipt-heavy methods, not network-wide.emptyResultAcceptvsmarkEmptyAsErrorMethods—emptyResultAcceptsays "empty is valid, don't retry."markEmptyAsErrorMethodssays "empty is an error, retry AND penalize the upstream." They are opposites. Adding a method to both has undefined behavior; pick one.emptyResultConfidence: finalizedBlockis more conservative — empties on unfinalized blocks are still retried even for methods inemptyResultAccept. UseblockHead(default) unless you specifically need finalized-only trust.blockUnavailableDelaystatic value is rarely needed — the dynamic block-time estimate takes over within the first few seconds. Only set it if your use-case requires a specific floor during the warmup window.enforceNonNullTaggedBlocks: falsefor zkSync-style chains — some L2s legitimately return null for certain block tags (e.g."pending"on chains without a mempool). Disable only for those specific networks.- Using deprecated
evm.integritywith newdirectiveDefaults— if both are present,directiveDefaultstakes precedence and theintegrityblock is ignored. Migrate fully.
Append .llms.txt to this URL (or use the AI link above) to fetch the entire expanded reference as plain markdown for an AI assistant.