Skip to content

fix(anvil): continue the saved timeline after loading state - #15760

Merged
mablr merged 9 commits into
foundry-rs:masterfrom
0xMars42:fix/anvil-state-time-offset
Aug 7, 2026
Merged

fix(anvil): continue the saved timeline after loading state#15760
mablr merged 9 commits into
foundry-rs:masterfrom
0xMars42:fix/anvil-state-time-offset

Conversation

@0xMars42

@0xMars42 0xMars42 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Motivation

Fixes #10331. Backend::load_state restores the block env, base fee and blob params from the saved head, but never re-anchors the TimeManager. The first block mined after loading a state file therefore takes its timestamp from the node's startup anchor (genesis or fork block) instead of the saved chain head: the loaded chain sits on its saved timeline while the next block jumps back to wall-clock time, which can produce a non-monotonic chain.

Solution

Re-anchor block time to the canonical head load_state selects. When the loaded head stays canonical (plain load, or a state file ahead of the fork block, see #9539), the anchor is its header timestamp read from storage. When the state file is at or below the fork block (see #9215), the canonical head rolls back to the fork block, whose header is not in local storage, so the anchor takes the fork timestamp, the same source reset_fork uses. Resolving the head by identity also keeps the timeline of stale blocks out of the anchor: a state file can carry blocks above its own best block (load_blocks never prunes, so dumping after loading an older state keeps the discarded blocks), and the highest loaded header is then not the canonical head.

This also applies to the anvil_loadState RPC, which goes through the same path: loading a state at runtime re-anchors block time the same way, consistent with --load-state at startup.

Two deliberate semantic notes: a pending evm_setNextBlockTimestamp and a previously applied evm_increaseTime offset do not survive a load, matching the other re-anchor paths (anvil_reset, snapshot revert); and --timestamp combined with --load-state now resolves to the loaded head's timeline (previously the flag survived the load, which could produce a non-monotonic chain against the loaded head).

Regression tests: the original one (chain moved a year ahead of wall-clock, saved, reloaded on a fresh node: the next block continues the saved timeline; fails on master with block after load_state went back in time: 1784120095 < 1815656095), the equal-height fork case (fork anchor kept), the fork rollback (future-dated local block, load a height-0 state onto the fork: latest returns the exact fork head and the next block leaves the discarded timeline), and a dump carrying stale blocks above its best block (the next block continues the canonical timeline). The last two fail on the previous revision of this branch.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

Backend::load_state restores the block env, base fee and blob params
from the saved head but never re-anchors the TimeManager, so the first
block mined after loading a state file took its timestamp from the
node's startup anchor (genesis or fork block) instead of the saved
chain head, jumping back in time. Reset block time to the loaded head's
timestamp where the fees are already restored from that header, like
the rollback path does, skipping the case where the canonical head is a
newer fork block.

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equal-height fork case can still replace the canonical fork time anchor with the loaded state's timestamp. Please use canonical head identity for this guard and add coverage for a loaded state and fork head at the same height.

Comment thread crates/anvil/src/eth/backend/mem/mod.rs Outdated
Compare the loaded head's hash with the canonical best hash instead of
comparing block numbers, so an equal-height fork head keeps its time
anchor. Covered by a test that forks a node at the same height as the
loaded state's head.

@0xMars42 0xMars42 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, the equal-height case did fire the reset: reproduced with a test forking a node at the same height as the loaded state's head, which failed on the previous guard and passes now. The guard compares the loaded head's hash with the canonical best hash instead of the numbers.

mattsse
mattsse previously approved these changes Jul 15, 2026

@mattsse mattsse left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up now guards the time re-anchor by canonical head identity and adds the requested equal-height fork regression. The focused regression test passes locally.

mablr
mablr previously approved these changes Jul 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request is stale because it has been open for 14 days with no activity. It will be closed in 7 days if there is no further activity.

@github-actions github-actions Bot added the stale label Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

✅ Changelog found

The deterministic check will validate the changed entry.

@mablr
mablr dismissed stale reviews from mattsse and themself via 63c656e July 30, 2026 07:32
@mablr
mablr enabled auto-merge (squash) July 30, 2026 07:39
mablr
mablr previously approved these changes Jul 30, 2026
@mablr
mablr requested a review from mattsse July 30, 2026 07:55
Comment thread crates/anvil/src/eth/backend/mem/mod.rs Outdated
@github-actions github-actions Bot added stop-stale and removed stale labels Jul 31, 2026
auto-merge was automatically disabled August 3, 2026 18:46

Head branch was pushed to by a user without write access

@mablr mablr self-assigned this Aug 4, 2026
mablr added 2 commits August 4, 2026 16:32
# Conflicts:
#	crates/anvil/src/eth/backend/mem/mod.rs
Hold the mining lock while applying loaded state so block production cannot race the chain-head and timestamp updates. Re-anchor fork time by canonical head hash so blockless legacy states also clear pending timestamp controls.
@mablr
mablr requested a review from stevencartavia August 4, 2026 15:15
figtracer
figtracer previously approved these changes Aug 4, 2026
@mablr
mablr enabled auto-merge (squash) August 7, 2026 14:55
@figtracer
figtracer self-requested a review August 7, 2026 14:56

@mablr mablr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
Thank you @0xMars42 !

@mablr
mablr merged commit d23b7bc into foundry-rs:master Aug 7, 2026
26 checks passed
@github-project-automation github-project-automation Bot moved this to Done in Foundry Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bug(anvil): persistent state seems to ignore timestamp changes

5 participants