Skip to content

"Chainspec file is misconfigured!" is a false positive and fires once per eth_estimateGas call on mainnet (the check compares the chainspec against the caller's activation) #13202

Description

@stdevMac

Description

Chainspec file is misconfigured! Timestamp transition is configured to happen before the last block transition. is emitted at WARN once per eth_estimateGas call on mainnet, running the chainspec we ship, and once per replayed block during historical sync.

The warning is a false positive. The check compares the chainspec against the caller's activation argument rather than against the chainspec itself, so it fires whenever a caller passes a current timestamp together with a block number below the chain's first timestamp fork — that is, whenever the node has not yet synced past that fork. The chainspec is correct; the node is simply behind.

eth_call does not trigger it. Only eth_estimateGas does, and exactly one line per call rather than one per binary-search iteration.

Steps to Reproduce

  1. Start a mainnet node that has not yet synced past block 12,965,000 (London), with the shipped foundation.json.
  2. Call eth_estimateGas repeatedly.
  3. Count the warning lines against the number of calls.

Or, for the replay path: restart a node that is a long way behind and watch the log during block replay.

Actual behavior

One warning line per eth_estimateGas call. Measured on a syncing mainnet node over its whole retained log: 87,517 Chainspec file is misconfigured lines against 87,481 eth_estimateGas responses — 1:1 to within 36 lines, with the chainspec line immediately preceding each response. A second mainnet node over a 900 s window: 40,592 against 40,600.

On the replay path the warning becomes essentially the node's entire output: on a gnosis node mid-replay, 51,238 of 51,650 lines in a 120 s window (99.2%), peaking at 5,875 lines in a single second.

The behaviour is fully predicted by (head < largest block transition) AND (now > earliest timestamp transition). Across a 24-node set spanning five networks and both state backends, all 5 nodes whose head was below their chain's largest block transition emitted it and all 19 whose head was above emitted zero — no false positives, no false negatives.

Expected behavior

No warning for a correct chainspec. foundation.json (12,965,000 / 1,681,338,455) and gnosis.json (19,040,000 / 1,690,889,660) are both correct, and the warning is false on both.

Additional context

The emit site is src/Nethermind/Nethermind.Specs/ChainSpecStyle/SpecProviderBase.cs:53 (now :55 on master after #12155's nullability plumbing; the warning line itself is byte-identical), inside SpecProviderBase.GetSpec(ForkActivation activation) — one of the hottest paths in the client, guarded only by if (_logger.IsWarn):

if (_firstTimestampActivation.Value.Timestamp < activation.Timestamp
    && _firstTimestampActivation.Value.BlockNumber > activation.BlockNumber)
    _logger.Warn("Chainspec file is misconfigured! Timestamp transition is configured to happen before the last block transition.");

The RPC asymmetry is explained in Nethermind.Facade/BlockchainBridge.cs: :463 clones the header and does Number += 1, :468 sets callHeader.Timestamp = Math.Max(..., timestamper.UnixTime.Seconds) — this is where wall-clock time enters — and :471 calls GetSpec(callHeader) with activation (head+1, now). eth_call enters at :180 with treatBlockHeaderAsParentBlock: false, so :468 never runs. eth_estimateGas enters at :244 with true. One line per call rather than per iteration because GasEstimator.cs:74 resolves the spec once from a historic timestamp and reuses it.

This was diagnosed correctly in #10880 and PR #10881 (both March 2026), whose body describes this exact mechanism. #10881 changed only a test file, on the stated grounds that "SpecProviderBase warning remains unchanged — it continues to detect real chainspec misconfigurations". That justification does not hold: ChainSpecBasedSpecProvider.cs:193 constructs every timestamp transition as (biggestBlockTransition, releaseStartTimestamp), so _firstTimestampActivation.BlockNumber is by definition the chain's last block-numbered transition. A construction-time self-comparison is always equal, never greater, so 100% of the predicate's signal comes from the caller's argument and none from the chainspec. ChainSpecBasedSpecProvider and the test-only CustomSpecProvider are the only subclasses.

Two notes for whoever fixes it:

  • A fix must also rewrite ChainSpecBasedSpecProviderTests.cs:152, 12 cases asserting iLogger.Received(1).Warn(<exact string>) — i.e. tests that currently pin the false positive.
  • Do not "fix" it at BlockchainBridge.cs:468. Synthesising a next-block header at wall-clock time is correct behaviour for gas estimation. Per the tautology above, a construction-time version of the check would be dead code, so deleting the check from GetSpec is the straightforward option.

There is no operator mitigation: the only guard is IsWarn, so silencing this means silencing every warning the node emits.

Desktop

  • Operating System: Linux x64
  • Version: 2.0.0-rc (e6d3d2f5ab); also present on master
  • Installation Method: Docker

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions