diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 2100fac6f364f..860ba76d1b072 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -4758,6 +4758,14 @@ impl> Backend { header.timestamp, ), )); + + // 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 at or below the fork block), + // comparing by identity since a fork head can share the loaded head's height. + if header.hash_slow() == self.blockchain.storage.read().best_hash { + self.time.reset(header.timestamp); + } } if !self.db.write().await.load_state(state.clone())? { diff --git a/crates/anvil/tests/it/state.rs b/crates/anvil/tests/it/state.rs index 7a0eb359d89ff..058a82afbcdd1 100644 --- a/crates/anvil/tests/it/state.rs +++ b/crates/anvil/tests/it/state.rs @@ -51,6 +51,89 @@ async fn can_load_state() { assert_eq!(num, U256::from(num_from_tag)); } +// +#[tokio::test(flavor = "multi_thread")] +async fn test_load_state_continues_saved_timeline() { + let (api, _handle) = spawn(NodeConfig::test()).await; + + // Move the chain's timeline one year ahead of wall-clock time, then mine on it. + let one_year_ahead = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + + 31_536_000; + api.evm_set_next_block_timestamp(one_year_ahead).unwrap(); + api.mine_one().await; + + let saved_head_timestamp = api + .block_by_number(alloy_eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .timestamp; + assert_eq!(saved_head_timestamp, one_year_ahead); + + let state = api.serialized_state(false).await.unwrap(); + let (api, _handle) = spawn(NodeConfig::test().with_init_state(Some(state))).await; + + api.mine_one().await; + let new_head_timestamp = api + .block_by_number(alloy_eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .timestamp; + + // The block mined after loading the state must continue the saved timeline instead of + // falling back to the wall-clock anchor of the fresh node. + assert!( + new_head_timestamp >= saved_head_timestamp, + "block after load_state went back in time: {new_head_timestamp} < {saved_head_timestamp}" + ); +} + +// Loading a state whose head has the same number as the fork block must keep the fork time +// anchor: the canonical head stays the fork block, so the saved timeline does not apply. +#[tokio::test(flavor = "multi_thread")] +async fn test_load_state_equal_height_fork_keeps_fork_anchor() { + // The state source: one block mined a year ahead of wall-clock time. + let (api_state, _handle_state) = spawn(NodeConfig::test()).await; + let one_year_ahead = + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + + 31_536_000; + api_state.evm_set_next_block_timestamp(one_year_ahead).unwrap(); + api_state.mine_one().await; + let state = api_state.serialized_state(false).await.unwrap(); + + // The fork source: one block mined on the wall-clock timeline, same height as the state. + let (api_remote, handle_remote) = spawn(NodeConfig::test()).await; + api_remote.mine_one().await; + + // Fork the remote head (height 1) and load the state (best height 1 as well): the + // canonical head keeps being the fork block, so its time anchor must be preserved. + let (api, _handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(handle_remote.http_endpoint())) + .with_init_state(Some(state)), + ) + .await; + + api.mine_one().await; + let new_head_timestamp = api + .block_by_number(alloy_eips::BlockNumberOrTag::Latest) + .await + .unwrap() + .unwrap() + .header + .timestamp; + + assert!( + new_head_timestamp < one_year_ahead, + "block after load_state jumped to the loaded state's timeline instead of keeping the \ + fork anchor: {new_head_timestamp} >= {one_year_ahead}" + ); +} + // #[tokio::test(flavor = "multi_thread")] async fn finalized_block_hash_consistent_after_load_state() {