Skip to content

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

Open
0xMars42 wants to merge 2 commits into
foundry-rs:masterfrom
0xMars42:fix/anvil-state-time-offset
Open

fix(anvil): continue the saved timeline after loading state#15760
0xMars42 wants to merge 2 commits into
foundry-rs:masterfrom
0xMars42:fix/anvil-state-time-offset

Conversation

@0xMars42

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

Reset block time to the loaded head's timestamp right where the fees are already restored from that header, mirroring what the rollback path does after resetting the block env. The reset is skipped when the canonical head is not the loaded one (state file older than the fork block, see #9215), where the fork anchor stays correct.

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

The regression test moves the chain one year ahead of wall-clock time, saves the state, reloads it on a fresh node and asserts the next mined block continues the saved timeline. It fails on master with block after load_state went back in time: 1784120095 < 1815656095 and passes with the fix.

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
// Re-anchor block time to the loaded head so subsequent blocks continue the saved
// timeline instead of the node's startup anchor (genesis or fork block). Skip when
// the canonical head is not the loaded one (state file older than the fork block).
if header.number == self.blockchain.storage.read().best_number {

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.

When the loaded state head and fork head have the same block number, the selection above keeps the fork head (best_number > number is false), but this height-only check still treats the loaded header as canonical and resets TimeManager from its timestamp. I reproduced this with a future-dated local block at height N loaded onto a fork also at N; the next block followed the stale state timeline. Comparing the actual canonical hash fixes it:

Suggested change
if header.number == self.blockchain.storage.read().best_number {
if header.hash_slow() == self.blockchain.storage.read().best_hash {

Please also add an equal-height fork regression test.

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 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.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

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

3 participants