Skip to content

Unconditional next-block header fetch at chain tip in RPCLogStreamer.GetNextPage causes backfill stall and noisy retries #1879

@neekolas

Description

@neekolas

Severity: Low | Likelihood: Low | Impact: Medium | Type: Vulnerability

Details

GetNextPage performs a reorg check by fetching header(fromBlock+1) before verifying fromBlock < highestBlock; at chain tip this header does not exist, causing repeated errors and a stall until a new block appears.

In RPCLogStreamer.GetNextPage, the function calls HeaderByNumber(fromBlockNumber+1) for reorg detection whenever fromBlockNumber > 0, before ensuring that fromBlockNumber is below the current head. At the chain tip (fromBlockNumber == highestBlock), the next block header is not yet available, so HeaderByNumber(fromBlock+1) fails. The caller (watchContract) treats this as a generic error, sleeps 100ms, and retries, never reaching ErrEndOfBackfill or switching to subscription mode. This causes repeated error logs and a temporary stall at the backfill boundary.

Exploitation

Scenario 1

Malicious or degraded RPC provider returns errors for HeaderByNumber(fromBlock+1) around the backfill cursor, preventing GetNextPage from reaching end-of-backfill and keeping the indexer stuck retrying indefinitely without switching to subscription.

Preconditions / Assumptions:

  • (a) Node uses an untrusted or malfunctioning RPC provider for rpcClient
  • (b) Indexer is near or at the chain tip (fromBlockNumber >= highestBlock)
  • (c) Provider persistently returns errors to HeaderByNumber(fromBlock+1)

Scenario 2

On node restart near chain tip with a quiet chain, GetNextPage probes header(fromBlock+1) which does not exist yet, returning an error; the backfill loop logs, sleeps, and retries until a new block is mined, stalling indexing and producing noisy logs.

Preconditions / Assumptions:

  • (a) Node previously indexed up to chain head for the watched contracts
  • (b) Operator restarts the node
  • (c) Chain is idle or slow so the next block is not yet available when GetNextPage runs
  • (d) RPC provider returns NotFound for non-existent headers

Scenario 3

While the node is stalled at the backfill boundary, an attacker emits many matching events to the watched contract, filling the subscription channel; this causes backpressure and potential subscription reconnection churn until backfill eventually completes.

Preconditions / Assumptions:

  • (a) Node is already stalled at the end-of-backfill boundary
  • (b) Watched contracts are publicly callable
  • (c) Attacker is willing to pay gas to emit many matching logs during the stall

Files impacted

  • pkg/indexer/rpc_streamer/rpc_log_streamer.go

Lines 355-359:

// Do not check for reorgs at block height 0. Genesis does not have a parent.
if fromBlockNumber > 0 {
    nextBlockHeader, err := r.rpcClient.HeaderByNumber(
        ctx,

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions