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
- Start a mainnet node that has not yet synced past block 12,965,000 (London), with the shipped
foundation.json.
- Call
eth_estimateGas repeatedly.
- 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
Description
Chainspec file is misconfigured! Timestamp transition is configured to happen before the last block transition.is emitted at WARN once pereth_estimateGascall 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
activationargument 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_calldoes not trigger it. Onlyeth_estimateGasdoes, and exactly one line per call rather than one per binary-search iteration.Steps to Reproduce
foundation.json.eth_estimateGasrepeatedly.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_estimateGascall. Measured on a syncing mainnet node over its whole retained log: 87,517Chainspec file is misconfiguredlines against 87,481eth_estimateGasresponses — 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) andgnosis.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:55onmasterafter #12155's nullability plumbing; the warning line itself is byte-identical), insideSpecProviderBase.GetSpec(ForkActivation activation)— one of the hottest paths in the client, guarded only byif (_logger.IsWarn):The RPC asymmetry is explained in
Nethermind.Facade/BlockchainBridge.cs::463clones the header and doesNumber += 1,:468setscallHeader.Timestamp = Math.Max(..., timestamper.UnixTime.Seconds)— this is where wall-clock time enters — and:471callsGetSpec(callHeader)with activation(head+1, now).eth_callenters at:180withtreatBlockHeaderAsParentBlock: false, so:468never runs.eth_estimateGasenters at:244withtrue. One line per call rather than per iteration becauseGasEstimator.cs:74resolves 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:193constructs every timestamp transition as(biggestBlockTransition, releaseStartTimestamp), so_firstTimestampActivation.BlockNumberis 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.ChainSpecBasedSpecProviderand the test-onlyCustomSpecProviderare the only subclasses.Two notes for whoever fixes it:
ChainSpecBasedSpecProviderTests.cs:152, 12 cases assertingiLogger.Received(1).Warn(<exact string>)— i.e. tests that currently pin the false positive.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 fromGetSpecis the straightforward option.There is no operator mitigation: the only guard is
IsWarn, so silencing this means silencing every warning the node emits.Desktop
e6d3d2f5ab); also present onmaster