Skip to content

bug: Possible race condition in eth_feeHistory. Method returns incomplete results due to async cache population race #13680

@LeoPatOZ

Description

@LeoPatOZ

Component

Anvil

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

12.14.0

What version of Foundryup are you on?

foundryup: 1.5.0

What command(s) is the bug in?

No response

Operating System

MacOS 26.3.1

Describe the bug

Description

eth_feeHistory can return incomplete or empty results when called shortly after blocks are mined. The fee history cache is populated asynchronously by FeeHistoryService, which runs as a separate future inside NodeService::poll. This creates a race window where the cache is stale relative to the actual chain head.
The pipeline has three decoupled stages:

  • Backend::mine_block(): stores block + receipts, sends a NewBlockNotification, and returns. The fee history cache is not updated at this point.
  • FeeHistoryService: receives notifications and inserts cache entries, but only when the tokio runtime happens to poll the NodeService task.
  • EthApi::fee_history(): reads from the cache. Missing entries are silently skipped, so the response can have fewer entries than requested with no error.

This means any RPC call that lands between stage 1 and stage 2 will observe a stale cache.

Observed behavior

We have a test that mines 100 blocks, then calls eth_feeHistory twice requesting the last 10 blocks:

First call: returns fee history for only 1 block (block 100)
Second call: returns fee history for all 11 blocks (90–100)

To confirm the root cause, we added panic statements in a local fork of Anvil inside EthApi::fee_history (replacing the silent skip). The panic fired with:

fee history cache missing entry for block 91
(requested range 91..=100, cache has blocks: [0, 1, 2, ..., 42])
The service had only processed up to block 42 when the first RPC call arrived, despite the chain head being at block 100.

Reproduction difficulty

Due to the nature of this bug, it's very difficult to write a PoC that fails consistently. The race depends entirely on when the runtime happens to poll FeeHistoryService relative to incoming RPC requests.

Suggested fix

Insert the fee history cache entry synchronously inside Backend::mine_block(), immediately after the block and receipts are stored. This guarantees the cache is always consistent with the chain head before any RPC call can observe the new block.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions