Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4758,6 +4758,14 @@ impl<N: Network<ReceiptEnvelope = FoundryReceiptEnvelope>> Backend<N> {
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())? {
Expand Down
83 changes: 83 additions & 0 deletions crates/anvil/tests/it/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,89 @@ async fn can_load_state() {
assert_eq!(num, U256::from(num_from_tag));
}

// <https://github.com/foundry-rs/foundry/issues/10331>
#[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}"
);
}

// <https://github.com/foundry-rs/foundry/issues/12645>
#[tokio::test(flavor = "multi_thread")]
async fn finalized_block_hash_consistent_after_load_state() {
Expand Down
Loading