Integrity

When requesting latest block or logs data from RPC nodes, there's no guarantee that the responding node has the most recent data. eRPC has a built-in mechanism to track highest known block number across all upstreams and enforce additional checks on certain methods such as:

  • eth_getBlockByNumber
  • eth_blockNumber
  • eth_getLogs

How does eRPC handle data integrity?

There are various mechanisms that aim to keep the data integrity as high as possible:

  1. Retry directives such as "retry-empty" or "retry-pending" where request will be sent to another node if one returns an empty or pending response.

  2. Integrity module utilizes various components (e.g. EVM state poller) running for every Upstream, to track latest/finalized blocks and intercept certain methods (e.g. eth_blockNumber or eth_getBlockByNumber(latest)) to ensure the highest known block number is used. This behavior is essential for use-cases like "indexers", to always receive the highest known block number.

  3. Selection policy is used to exclude nodes that have too much block-head lag.

Config

In this page we focus on the (2.) integrity module and its configuration options:

projects:
  - id: main
 
    # Enable for all networks:
    networkDefaults:
      evm:
        integrity:
          # eth_blockNumber and eth_getBlockByNumber if the chosen upstream returns a block older than the highest known block,
          # it will return the highest known block instead.
          # This _might_ cause an extra upstream request for eth_getBlockByNumber(latest) calls.
          enforceHighestBlock: true
 
          # When eth_getLogs end-range is a specific blockNumber ensure only upstreams whose
          # latest block is equal or more handle it, otherwise return an error (i.e. MissingData) for client to retry.
          # This might cause more requests if evm state poller has lagging latest block vs eth_getLogs end-range;
          # it _might_ send an additional eth_blockNumber to other upstreams to see which one definitively has the requested range.
          enforceGetLogsBlockRange: true
 
    # Enable for a specific network:
    networks:
      - type: evm
        evm:
          chainId: 137
          integrity:
            enforceHighestBlock: true
            enforceGetLogsBlockRange: true

How highest-known block number is tracked?

For every upstream, eRPC has a running state poller running in the background to fetch the latest/finalized block number regularly (plus ad-hoc scenarios explained below). The state poller interval can be configured with:

projects:
  - id: main
 
    # Configure for all upstreams:
    upstreamDefaults:
      evm:
        statePollerInterval: 30s # Default: 30s
        statePollerDebounce: 5s # Default: 5s (or equal to block time if the chainId is a known chain)
 
    # Configure per-upstream:
    upstreams:
      - endpoint: https://xxxxxxxxxx.xyz/chain-137
        evm:
          chainId: 42161
          statePollerInterval: 30s
          statePollerDebounce: 5s

Additionally these are the ad-hoc scenarios where highest-known block number is updated:

  • When a call to eth_blockNumber is made, the result will be checked against existing highest-known block number.
  • When a call to eth_getBlockByNumber(latest) is made, the result will be checked against existing highest-known block number.
  • When a call to eth_getLogs is made and range-end is a block higher than chosen upstream's latest block, a pro-active poll happens on state poller (respecting the debouncing interval) to update the highest-known block number. If still upstream is behind the requested range-end then it will be skipped.

In the config above statePollerInterval is the interval to poll the latest/finalized block number from the upstream. statePollerDebounce is the minimum interval required before next poll happens, this is mainly useful for when pro-active polls are triggered as explained for eth_getLogs. The statePollerDebounce value must be equal to block time of the chain.

eth_blockNumber behavior

This method is intercepted by the integrity module to ensure the highest known block number is returned.

  1. First request will be sent to the chosen upstream.
  2. The response will be checked against high-known block number (across all upstreams state pollers).
  • During this check if any upstream's "last update to latest block" is older than statePollerDebounce it will try to fetch the latest block from the upstream (i.e. we make sure we have most recent view of upstream's latest block).
  1. If the response is older than the high-known block number, the response will be replaced with the high-known block number.

Relevant Prometheus metrics:

  • erpc_upstream_stale_latest_block_total - Total number of times an upstream returned a stale latest block number (vs others).
  • erpc_upstream_stale_finalized_block_total - Total number of times an upstream returned a stale finalized block number (vs others).

eth_getBlockByNumber behavior

This method is intercepted by the integrity module to ensure the highest known block number is used for "latest" and "finalized" block tags.

  1. First request will be sent to the chosen upstream.
  2. The response will be checked against high-known block number (across all upstreams state pollers).
  • During this check if any upstream's "last update to latest block" is older than statePollerDebounce it will try to fetch the latest block from the upstream (i.e. we make sure we have most recent view of upstream's latest block).
  1. If the response is older than the high-known block number, then the request will be sent to all 'other' upstreams (excluding the current one).

Relevant Prometheus metrics:

  • erpc_upstream_stale_latest_block_total - Total number of times an upstream returned a stale latest block number (vs others).
  • erpc_upstream_stale_finalized_block_total - Total number of times an upstream returned a stale finalized block number (vs others).

eth_getLogs behavior

This method is intercepted by the integrity module to ensure fromBlock and toBlock parameters are within the available block range of the chosen upstream.

  1. We check the toBlock against the chosen upstream's latest block number. If latest block is stale (previously updated < statePollerDebounce) then we force a poll to update the latest block number.
  2. If the toBlock is still higher than the latest block number, then we skip to the next upstream.
  3. We check the fromBlock is within the range of the chosen upstream relative to its "maxAvailableRecentBlocks" if configured, otherwise we skip to the next upstream.
  4. If both fromBlock and toBlock are within the range of the chosen upstream, then request is sent to the chosen upstream.

Relevant Prometheus metrics:

  • erpc_upstream_evm_get_logs_stale_upper_bound_total - Total number of times eth_getLogs was skipped due to upstream latest block being less than requested toBlock.
  • erpc_upstream_evm_get_logs_stale_lower_bound_total - Total number of times eth_getLogs was skipped due to fromBlock being less than upstream's available block range.