anvil mines consecutive blocks with identical timestamps on default automine path
This is the repro @grandizzy asked for in #9142:
@Rubilmax mind to open a ticket with a way to reproduce the problem you see? thank you!
That ticket never got filed, so here it is.
What it says on the box
Default anvil (default automine, no --block-time, no setBlockTimestampInterval tweak) mines consecutive blocks with the identical block.timestamp. Confirmed with four ordinary cast send transfers, no flags:
block 2 ts 1788310641
block 3 ts 1788310641
block 4 ts 1788310641A forward time jump makes it worse - after anvil_mine(1, 10000), every subsequent block freezes at that same timestamp:
blocks 2-5 all ts 1788320438Where
crates/anvil/src/eth/backend/time.rs:181-183:
// Ensures that the timestamp is always increasing
if next_timestamp < last_timestamp { next_timestamp = last_timestamp + 1; }Comment says "always increasing", comparison is <, so equality sails through untouched. Sibling set_next_block_timestamp (~line 127/131) has a doc comment saying it "fails if it's before (or at the same time) the last timestamp" but only checks < too.
Why it matters
Yellow Paper eq. 55 requires Hs > P(H)Hs (strictly increasing). geth rejects header.Time <= parent.Time with errOlderBlockTime. Hardhat and Ganache both guarantee parent+1. anvil is the odd one out on the default path.
Concretely, this makes downstream test suites go green when they should catch a real bug:
- Uniswap V2
_update:if (timeElapsed > 0)- TWAP accumulators never accumulate across same-second blocks - Uniswap V3
Oracle.write:if (last.blockTimestamp == blockTimestamp) return- the observation write is silently dropped - Synthetix
StakingRewards.rewardPerTokenand similar reward-streaming contracts: zero accrual for that block
A TWAP-manipulation guard test written against anvil can pass vacuously because the timestamp never actually advances between the manipulation block and the read block.
Prior art
#13694 proposed exactly this fix (< -> <=) and got a positive review (zerosnacks: "Makes sense, thanks!"), then the stale-bot closed it 2026-06-29 for inactivity, not on merit.
The catch: #9142 itself is a legitimate feature request for same-timestamp blocks under --block-time 0, and it shipped. A naive blanket <= would break that. The distinction that needs preserving: same-timestamp is fine when it was explicitly requested (interval == Some(0) or an exact setBlockTimestampInterval/setNextBlockTimestamp override), but should not leak into the default automine path where nothing asked for it. That's plausibly why #13694 stalled rather than merged outright - it needed that narrower framing, not just the comparison flip.
Suggested fix shape
Clamp to strictly-increasing on the default path; only permit equal timestamps when the same-timestamp mode was explicitly opted into (matching what #9142 actually asked for, not a blanket relaxation).
Happy to open a PR with that narrower fix if that's the right shape - flagging as an issue first since #13694's exact form apparently needed adjustment.
Source: foundry-rs/foundry