From 456da156e07b1ede01c08c4f48ef36eed4094f17 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:03:45 +0530 Subject: [PATCH 1/8] feat(`anvil`): persist accounts in `ForkedStorage` --- crates/anvil/src/eth/backend/fork.rs | 51 ++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index 7d887f697572..482a62b176cb 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -29,10 +29,12 @@ use parking_lot::{ lock_api::{RwLockReadGuard, RwLockWriteGuard}, RawRwLock, RwLock, }; -use revm::primitives::BlobExcessGasAndPrice; -use std::{sync::Arc, time::Duration}; +use revm::primitives::{AccountInfo, BlobExcessGasAndPrice, Bytecode, KECCAK_EMPTY}; +use std::{collections::BTreeMap, sync::Arc, time::Duration}; use tokio::sync::RwLock as AsyncRwLock; +use super::db::{SerializableAccountRecord, SerializableState}; + /// Represents a fork of a remote client /// /// This type contains a subset of the [`EthApi`](crate::eth::EthApi) functions but will exclusively @@ -270,11 +272,19 @@ impl ClientFork { blocknumber: u64, ) -> Result { trace!(target: "backend::fork", "get_balance={:?}", address); + if let Some(account) = self.storage_read().account_at.get(&(address, blocknumber)).cloned() + { + return Ok(account.balance); + } + self.provider().get_balance(address).block_id(blocknumber.into()).await } pub async fn get_nonce(&self, address: Address, block: u64) -> Result { trace!(target: "backend::fork", "get_nonce={:?}", address); + if let Some(account) = self.storage_read().account_at.get(&(address, block)).cloned() { + return Ok(account.nonce); + } self.provider().get_transaction_count(address).block_id(block.into()).await } @@ -284,6 +294,16 @@ impl ClientFork { blocknumber: u64, ) -> Result { trace!(target: "backend::fork", "get_account={:?}", address); + if let Some(account) = self.storage_read().account_at.get(&(address, blocknumber)).cloned() + { + let acc = Account { + balance: account.balance, + nonce: account.nonce, + code_hash: account.code_hash, + storage_root: Default::default(), + }; + return Ok(acc); + } self.provider().get_account(address).block_id(blocknumber.into()).await } @@ -601,6 +621,14 @@ impl ClientFork { block } + + pub fn load_state(&mut self, state: SerializableState) { + let mut storage = self.storage_write(); + + if let Some(block) = state.block { + storage.load_accounts(state.accounts, block.number.to()); + } + } } /// Contains all fork metadata @@ -695,6 +723,7 @@ pub struct ForkedStorage { pub block_traces: HashMap>, pub block_receipts: HashMap>, pub code_at: HashMap<(Address, u64), Bytes>, + pub account_at: HashMap<(Address, u64), AccountInfo>, } impl ForkedStorage { @@ -703,4 +732,22 @@ impl ForkedStorage { // simply replace with a completely new, empty instance *self = Self::default() } + + /// Load the accounts. + pub fn load_accounts( + &mut self, + accounts: BTreeMap, + block_number: u64, + ) { + accounts.into_iter().map(|(k, v)| { + let info = AccountInfo { + balance: v.balance, + nonce: v.nonce, + code_hash: KECCAK_EMPTY, + code: if v.code.is_empty() { None } else { Some(Bytecode::new_raw(v.code)) }, + }; + + self.account_at.insert((k, block_number), info) + }); + } } From b650f56fe52f79be3eb7c8ab4d2ad1aaca08a32f Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:22:47 +0530 Subject: [PATCH 2/8] load accounts --- crates/anvil/src/eth/backend/fork.rs | 4 ++-- crates/anvil/src/eth/backend/mem/mod.rs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index 482a62b176cb..eeeb7f5866fa 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -739,7 +739,7 @@ impl ForkedStorage { accounts: BTreeMap, block_number: u64, ) { - accounts.into_iter().map(|(k, v)| { + accounts.into_iter().for_each(|(k, v)| { let info = AccountInfo { balance: v.balance, nonce: v.nonce, @@ -747,7 +747,7 @@ impl ForkedStorage { code: if v.code.is_empty() { None } else { Some(Bytecode::new_raw(v.code)) }, }; - self.account_at.insert((k, block_number), info) + self.account_at.insert((k, block_number), info); }); } } diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 73adaa97f3fd..21049ed56778 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -929,6 +929,10 @@ impl Backend { .into()); } + if let Some(mut fork) = self.get_fork() { + fork.load_state(state.clone()); + } + if let Some(historical_states) = state.historical_states { self.states.write().load_states(historical_states); } From 9bf9910852e2b34e25b5f00759053e960857b5e6 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:34:31 +0530 Subject: [PATCH 3/8] test --- crates/anvil/tests/it/state.rs | 92 +++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/crates/anvil/tests/it/state.rs b/crates/anvil/tests/it/state.rs index cb8f3b9ebbca..274d3eb2568c 100644 --- a/crates/anvil/tests/it/state.rs +++ b/crates/anvil/tests/it/state.rs @@ -1,10 +1,13 @@ //! general eth api tests use crate::abi::Greeter; -use alloy_primitives::{Bytes, Uint, U256}; +use alloy_network::{ReceiptResponse, TransactionBuilder}; +use alloy_primitives::{address, utils::Unit, Bytes, Uint, U256}; use alloy_provider::Provider; -use alloy_rpc_types::BlockId; +use alloy_rpc_types::{BlockId, TransactionRequest}; +use alloy_serde::WithOtherFields; use anvil::{spawn, NodeConfig}; +use foundry_test_utils::rpc::next_http_rpc_endpoint; #[tokio::test(flavor = "multi_thread")] async fn can_load_state() { @@ -155,3 +158,88 @@ async fn can_preserve_historical_states_between_dump_and_load() { assert_eq!(greeting_after_change, "World!"); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_fork_load_state() { + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(next_http_rpc_endpoint())) + .with_fork_block_number(Some(21070682u64)), + ) + .await; + + let bob = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); + let alice = address!("9276449EaC5b4f7Bc17cFC6700f7BeeB86F9bCd0"); + + let provider = handle.http_provider(); + + let init_nonce_bob = provider.get_transaction_count(bob).await.unwrap(); + + let init_balance_alice = provider.get_balance(alice).await.unwrap(); + + let value = Unit::ETHER.wei().saturating_mul(U256::from(1)); // 1 ether + let tx = TransactionRequest::default().with_to(alice).with_value(value).with_from(bob); + let tx = WithOtherFields::new(tx); + + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + assert!(receipt.status()); + + let serialized_state = api.serialized_state(false).await.unwrap(); + + let state_dump_block = api.block_number().unwrap(); + + let (api, handle) = spawn( + NodeConfig::test() + .with_eth_rpc_url(Some(next_http_rpc_endpoint())) + .with_fork_block_number(Some(21070686u64)) // Forked chain has moved forward + .with_init_state(Some(serialized_state)), + ) + .await; + + let provider = handle.http_provider(); + + let restart_nonce_bob = provider.get_transaction_count(bob).await.unwrap(); + + let restart_balance_alice = provider.get_balance(alice).await.unwrap(); + + println!("init_nonce_bob: {}, restart_nonce_bob: {}", init_nonce_bob, restart_nonce_bob); + + println!( + "init_balance_alice: {}, restart_balance_alice: {}", + init_balance_alice, restart_balance_alice + ); + + assert_eq!(init_nonce_bob + 1, restart_nonce_bob); + + assert_eq!(init_balance_alice + value, restart_balance_alice); + + // Send another tx to check if the state is preserved + + let tx = TransactionRequest::default().with_to(alice).with_value(value).with_from(bob); + let tx = WithOtherFields::new(tx); + + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + assert!(receipt.status()); + + let latest_nonce_bob = provider.get_transaction_count(bob).await.unwrap(); + + let latest_balance_alice = provider.get_balance(alice).await.unwrap(); + + println!( + "latest_nonce_bob: {}, latest_balance_alice: {}", + latest_nonce_bob, latest_balance_alice + ); + + let tx = TransactionRequest::default() + .with_to(alice) + .with_value(value) + .with_from(bob) + .with_nonce(latest_nonce_bob); + let tx = WithOtherFields::new(tx); + + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + assert!(receipt.status()); +} From 95e1ddcd1380ce31f4b3e9070e898df3a0d32648 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:44:12 +0530 Subject: [PATCH 4/8] fix(`anvil`): override storage.best_number with fork_block_num if loading state on a fork url --- crates/anvil/src/eth/backend/mem/mod.rs | 32 +++++++++++++++---------- crates/anvil/tests/it/state.rs | 31 ++++++++++++------------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 21049ed56778..47f32f7b6f56 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -907,19 +907,27 @@ impl Backend { // Set the current best block number. // Defaults to block number for compatibility with existing state files. + let fork_num_and_hash = self.get_fork().map(|f| (f.block_number(), f.block_hash())); - let best_number = state.best_block_number.unwrap_or(block.number.to::()); - self.blockchain.storage.write().best_number = best_number; - - // Set the current best block hash; - let best_hash = - self.blockchain.storage.read().hash(best_number.into()).ok_or_else(|| { - BlockchainError::RpcError(RpcError::internal_error_with(format!( - "Best hash not found for best number {best_number}", - ))) - })?; - - self.blockchain.storage.write().best_hash = best_hash; + if let Some((number, hash)) = fork_num_and_hash { + // If loading state file on a fork, set best number to the fork block number. + // Ref: + self.blockchain.storage.write().best_number = U64::from(number); + self.blockchain.storage.write().best_hash = hash; + } else { + let best_number = state.best_block_number.unwrap_or(block.number.to::()); + self.blockchain.storage.write().best_number = best_number; + + // Set the current best block hash; + let best_hash = + self.blockchain.storage.read().hash(best_number.into()).ok_or_else(|| { + BlockchainError::RpcError(RpcError::internal_error_with(format!( + "Best hash not found for best number {best_number}", + ))) + })?; + + self.blockchain.storage.write().best_hash = best_hash; + } } 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 274d3eb2568c..c5e2c402536c 100644 --- a/crates/anvil/tests/it/state.rs +++ b/crates/anvil/tests/it/state.rs @@ -197,19 +197,17 @@ async fn test_fork_load_state() { ) .await; + // Ensure the initial block number is the fork_block_number and not the state_dump_block + let block_number = api.block_number().unwrap(); + assert_eq!(block_number, U256::from(21070686u64)); + assert_ne!(block_number, state_dump_block); + let provider = handle.http_provider(); let restart_nonce_bob = provider.get_transaction_count(bob).await.unwrap(); let restart_balance_alice = provider.get_balance(alice).await.unwrap(); - println!("init_nonce_bob: {}, restart_nonce_bob: {}", init_nonce_bob, restart_nonce_bob); - - println!( - "init_balance_alice: {}, restart_balance_alice: {}", - init_balance_alice, restart_balance_alice - ); - assert_eq!(init_nonce_bob + 1, restart_nonce_bob); assert_eq!(init_balance_alice + value, restart_balance_alice); @@ -223,23 +221,26 @@ async fn test_fork_load_state() { assert!(receipt.status()); - let latest_nonce_bob = provider.get_transaction_count(bob).await.unwrap(); - - let latest_balance_alice = provider.get_balance(alice).await.unwrap(); + let nonce_bob = provider.get_transaction_count(bob).await.unwrap(); - println!( - "latest_nonce_bob: {}, latest_balance_alice: {}", - latest_nonce_bob, latest_balance_alice - ); + let balance_alice = provider.get_balance(alice).await.unwrap(); let tx = TransactionRequest::default() .with_to(alice) .with_value(value) .with_from(bob) - .with_nonce(latest_nonce_bob); + .with_nonce(nonce_bob); let tx = WithOtherFields::new(tx); let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); assert!(receipt.status()); + + let latest_nonce_bob = provider.get_transaction_count(bob).await.unwrap(); + + let latest_balance_alice = provider.get_balance(alice).await.unwrap(); + + assert_eq!(nonce_bob + 1, latest_nonce_bob); + + assert_eq!(balance_alice + value, latest_balance_alice); } From aa5e2be136c7d68ae4f962b85b8df54e62ba8dfa Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:46:15 +0530 Subject: [PATCH 5/8] Revert "load accounts" This reverts commit b650f56fe52f79be3eb7c8ab4d2ad1aaca08a32f. --- crates/anvil/src/eth/backend/fork.rs | 4 ++-- crates/anvil/src/eth/backend/mem/mod.rs | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index eeeb7f5866fa..482a62b176cb 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -739,7 +739,7 @@ impl ForkedStorage { accounts: BTreeMap, block_number: u64, ) { - accounts.into_iter().for_each(|(k, v)| { + accounts.into_iter().map(|(k, v)| { let info = AccountInfo { balance: v.balance, nonce: v.nonce, @@ -747,7 +747,7 @@ impl ForkedStorage { code: if v.code.is_empty() { None } else { Some(Bytecode::new_raw(v.code)) }, }; - self.account_at.insert((k, block_number), info); + self.account_at.insert((k, block_number), info) }); } } diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 47f32f7b6f56..c60d7357166d 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -937,10 +937,6 @@ impl Backend { .into()); } - if let Some(mut fork) = self.get_fork() { - fork.load_state(state.clone()); - } - if let Some(historical_states) = state.historical_states { self.states.write().load_states(historical_states); } From e9387579fde1c8583d087c58f7cc26b04a9e7305 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:47:43 +0530 Subject: [PATCH 6/8] Revert "feat(`anvil`): persist accounts in `ForkedStorage`" This reverts commit 456da156e07b1ede01c08c4f48ef36eed4094f17. --- crates/anvil/src/eth/backend/fork.rs | 51 ++-------------------------- 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/crates/anvil/src/eth/backend/fork.rs b/crates/anvil/src/eth/backend/fork.rs index 482a62b176cb..7d887f697572 100644 --- a/crates/anvil/src/eth/backend/fork.rs +++ b/crates/anvil/src/eth/backend/fork.rs @@ -29,12 +29,10 @@ use parking_lot::{ lock_api::{RwLockReadGuard, RwLockWriteGuard}, RawRwLock, RwLock, }; -use revm::primitives::{AccountInfo, BlobExcessGasAndPrice, Bytecode, KECCAK_EMPTY}; -use std::{collections::BTreeMap, sync::Arc, time::Duration}; +use revm::primitives::BlobExcessGasAndPrice; +use std::{sync::Arc, time::Duration}; use tokio::sync::RwLock as AsyncRwLock; -use super::db::{SerializableAccountRecord, SerializableState}; - /// Represents a fork of a remote client /// /// This type contains a subset of the [`EthApi`](crate::eth::EthApi) functions but will exclusively @@ -272,19 +270,11 @@ impl ClientFork { blocknumber: u64, ) -> Result { trace!(target: "backend::fork", "get_balance={:?}", address); - if let Some(account) = self.storage_read().account_at.get(&(address, blocknumber)).cloned() - { - return Ok(account.balance); - } - self.provider().get_balance(address).block_id(blocknumber.into()).await } pub async fn get_nonce(&self, address: Address, block: u64) -> Result { trace!(target: "backend::fork", "get_nonce={:?}", address); - if let Some(account) = self.storage_read().account_at.get(&(address, block)).cloned() { - return Ok(account.nonce); - } self.provider().get_transaction_count(address).block_id(block.into()).await } @@ -294,16 +284,6 @@ impl ClientFork { blocknumber: u64, ) -> Result { trace!(target: "backend::fork", "get_account={:?}", address); - if let Some(account) = self.storage_read().account_at.get(&(address, blocknumber)).cloned() - { - let acc = Account { - balance: account.balance, - nonce: account.nonce, - code_hash: account.code_hash, - storage_root: Default::default(), - }; - return Ok(acc); - } self.provider().get_account(address).block_id(blocknumber.into()).await } @@ -621,14 +601,6 @@ impl ClientFork { block } - - pub fn load_state(&mut self, state: SerializableState) { - let mut storage = self.storage_write(); - - if let Some(block) = state.block { - storage.load_accounts(state.accounts, block.number.to()); - } - } } /// Contains all fork metadata @@ -723,7 +695,6 @@ pub struct ForkedStorage { pub block_traces: HashMap>, pub block_receipts: HashMap>, pub code_at: HashMap<(Address, u64), Bytes>, - pub account_at: HashMap<(Address, u64), AccountInfo>, } impl ForkedStorage { @@ -732,22 +703,4 @@ impl ForkedStorage { // simply replace with a completely new, empty instance *self = Self::default() } - - /// Load the accounts. - pub fn load_accounts( - &mut self, - accounts: BTreeMap, - block_number: u64, - ) { - accounts.into_iter().map(|(k, v)| { - let info = AccountInfo { - balance: v.balance, - nonce: v.nonce, - code_hash: KECCAK_EMPTY, - code: if v.code.is_empty() { None } else { Some(Bytecode::new_raw(v.code)) }, - }; - - self.account_at.insert((k, block_number), info) - }); - } } From 8aaa56dcea2e721fe5f551b2fdf1dc1af1d0fcb1 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:31:13 +0530 Subject: [PATCH 7/8] nit --------- Co-authored-by: grandizzy --- crates/anvil/tests/it/state.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/anvil/tests/it/state.rs b/crates/anvil/tests/it/state.rs index c5e2c402536c..f7736de2f403 100644 --- a/crates/anvil/tests/it/state.rs +++ b/crates/anvil/tests/it/state.rs @@ -159,6 +159,7 @@ async fn can_preserve_historical_states_between_dump_and_load() { assert_eq!(greeting_after_change, "World!"); } +// #[tokio::test(flavor = "multi_thread")] async fn test_fork_load_state() { let (api, handle) = spawn( From 576b57ad18ad1576eae9853aa76feb97f31b4fb1 Mon Sep 17 00:00:00 2001 From: Yash Atreya <44857776+yash-atreya@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:56:12 +0530 Subject: [PATCH 8/8] nit --- crates/anvil/src/eth/backend/mem/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index c60d7357166d..65ab8c32952b 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -911,7 +911,7 @@ impl Backend { if let Some((number, hash)) = fork_num_and_hash { // If loading state file on a fork, set best number to the fork block number. - // Ref: + // Ref: https://github.com/foundry-rs/foundry/pull/9215#issue-2618681838 self.blockchain.storage.write().best_number = U64::from(number); self.blockchain.storage.write().best_hash = hash; } else {