Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include block env in --dump state #6763

Merged
Merged
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
17 changes: 11 additions & 6 deletions crates/anvil/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use rand::{rngs::StdRng, SeedableRng};
use std::{
future::Future,
net::IpAddr,
path::PathBuf,
path::{Path, PathBuf},
pin::Pin,
str::FromStr,
sync::{
Expand Down Expand Up @@ -119,8 +119,8 @@ pub struct NodeArgs {

/// This is an alias for both --load-state and --dump-state.
///
/// It initializes the chain with the state stored at the file, if it exists, and dumps the
/// chain's state on exit.
/// It initializes the chain with the state and block environment stored at the file, if it
/// exists, and dumps the chain's state on exit.
#[clap(
long,
value_name = "PATH",
Expand All @@ -133,13 +133,13 @@ pub struct NodeArgs {
)]
pub state: Option<StateFile>,

/// Interval in seconds at which the status is to be dumped to disk.
/// Interval in seconds at which the state and block environment is to be dumped to disk.
///
/// See --state and --dump-state
#[clap(short, long, value_name = "SECONDS")]
pub state_interval: Option<u64>,

/// Dump the state of chain on exit to the given file.
/// Dump the state and block environment of chain on exit to the given file.
///
/// If the value is a directory, the state will be written to `<VALUE>/state.json`.
#[clap(long, value_name = "PATH", conflicts_with = "init")]
Expand Down Expand Up @@ -616,7 +616,12 @@ impl StateFile {
/// This is used as the clap `value_parser` implementation to parse from file but only if it
/// exists
fn parse(path: &str) -> Result<Self, String> {
let mut path = PathBuf::from(path);
Self::parse_path(path)
}

/// Parse from file but only if it exists
pub fn parse_path(path: impl AsRef<Path>) -> Result<Self, String> {
let mut path = path.as_ref().to_path_buf();
if path.is_dir() {
path = path.join("state.json");
}
Expand Down
21 changes: 12 additions & 9 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
cmd::StateFile,
eth::{
backend::{
db::{Db, SerializableState},
Expand Down Expand Up @@ -48,7 +49,7 @@ use std::{
fmt::Write as FmtWrite,
fs::File,
net::{IpAddr, Ipv4Addr},
path::PathBuf,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
Expand Down Expand Up @@ -437,13 +438,20 @@ impl NodeConfig {
self
}

/// Sets a custom code size limit
/// Sets the init state if any
#[must_use]
pub fn with_init_state(mut self, init_state: Option<SerializableState>) -> Self {
self.init_state = init_state;
self
}

/// Loads the init state from a file if it exists
#[must_use]
pub fn with_init_state_path(mut self, path: impl AsRef<Path>) -> Self {
self.init_state = StateFile::parse_path(path).ok().and_then(|file| file.state);
self
}

/// Sets the chain ID
#[must_use]
pub fn with_chain_id<U: Into<u64>>(mut self, chain_id: Option<U>) -> Self {
Expand Down Expand Up @@ -866,13 +874,8 @@ impl NodeConfig {
.expect("Failed to create default create2 deployer");
}

if let Some(ref state) = self.init_state {
backend
.get_db()
.write()
.await
.load_state(state.clone())
.expect("Failed to load init state");
if let Some(state) = self.init_state.clone() {
backend.load_state(state).await.expect("Failed to load init state");
}

backend
Expand Down
2 changes: 1 addition & 1 deletion crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1721,7 +1721,7 @@ impl EthApi {
/// Handler for RPC call: `anvil_loadState`
pub async fn anvil_load_state(&self, buf: Bytes) -> Result<bool> {
node_info!("anvil_loadState");
self.backend.load_state(buf).await
self.backend.load_state_bytes(buf).await
}

/// Retrieves the Anvil node configuration params.
Expand Down
10 changes: 7 additions & 3 deletions crates/anvil/src/eth/backend/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use foundry_evm::{
hashbrown::HashMap,
revm::{
db::{CacheDB, DatabaseRef, DbAccount},
primitives::{Bytecode, KECCAK_EMPTY},
primitives::{BlockEnv, Bytecode, KECCAK_EMPTY},
Database, DatabaseCommit,
},
};
Expand Down Expand Up @@ -126,7 +126,7 @@ pub trait Db:
fn insert_block_hash(&mut self, number: U256, hash: B256);

/// Write all chain data to serialized bytes buffer
fn dump_state(&self) -> DatabaseResult<Option<SerializableState>>;
fn dump_state(&self, at: BlockEnv) -> DatabaseResult<Option<SerializableState>>;

/// Deserialize and add all chain data to the backend storage
fn load_state(&mut self, state: SerializableState) -> DatabaseResult<bool> {
Expand Down Expand Up @@ -196,7 +196,7 @@ impl<T: DatabaseRef<Error = DatabaseError> + Send + Sync + Clone + fmt::Debug> D
self.block_hashes.insert(number, hash);
}

fn dump_state(&self) -> DatabaseResult<Option<SerializableState>> {
fn dump_state(&self, _at: BlockEnv) -> DatabaseResult<Option<SerializableState>> {
Ok(None)
}

Expand Down Expand Up @@ -324,6 +324,10 @@ impl MaybeHashDatabase for StateDb {

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SerializableState {
/// The block number of the state
///
/// Note: This is an Option for backwards compatibility: <https://github.com/foundry-rs/foundry/issues/5460>
pub block: Option<BlockEnv>,
pub accounts: BTreeMap<Address, SerializableAccountRecord>,
}

Expand Down
5 changes: 3 additions & 2 deletions crates/anvil/src/eth/backend/mem/fork_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use foundry_evm::{
};

pub use foundry_evm::fork::database::ForkedDatabase;
use foundry_evm::revm::primitives::BlockEnv;

/// Implement the helper for the fork database
impl Db for ForkedDatabase {
Expand All @@ -31,7 +32,7 @@ impl Db for ForkedDatabase {
self.inner().block_hashes().write().insert(number, hash);
}

fn dump_state(&self) -> DatabaseResult<Option<SerializableState>> {
fn dump_state(&self, at: BlockEnv) -> DatabaseResult<Option<SerializableState>> {
let mut db = self.database().clone();
let accounts = self
.database()
Expand All @@ -56,7 +57,7 @@ impl Db for ForkedDatabase {
))
})
.collect::<Result<_, _>>()?;
Ok(Some(SerializableState { accounts }))
Ok(Some(SerializableState { block: Some(at), accounts }))
}

fn snapshot(&mut self) -> U256 {
Expand Down
8 changes: 4 additions & 4 deletions crates/anvil/src/eth/backend/mem/in_memory_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use foundry_evm::{
};

// reexport for convenience
use foundry_evm::backend::RevertSnapshotAction;
pub use foundry_evm::{backend::MemDb, revm::db::DatabaseRef};
use foundry_evm::{backend::RevertSnapshotAction, revm::primitives::BlockEnv};

impl Db for MemDb {
fn insert_account(&mut self, address: Address, account: AccountInfo) {
Expand All @@ -32,7 +32,7 @@ impl Db for MemDb {
self.inner.block_hashes.insert(number, hash);
}

fn dump_state(&self) -> DatabaseResult<Option<SerializableState>> {
fn dump_state(&self, at: BlockEnv) -> DatabaseResult<Option<SerializableState>> {
let accounts = self
.inner
.accounts
Expand All @@ -57,7 +57,7 @@ impl Db for MemDb {
})
.collect::<Result<_, _>>()?;

Ok(Some(SerializableState { accounts }))
Ok(Some(SerializableState { block: Some(at), accounts }))
}

/// Creates a new snapshot
Expand Down Expand Up @@ -167,7 +167,7 @@ mod tests {

dump_db.set_storage_at(test_addr, U256::from(1234567), U256::from(1)).unwrap();

let state = dump_db.dump_state().unwrap().unwrap();
let state = dump_db.dump_state(Default::default()).unwrap().unwrap();

let mut load_db = MemDb::default();

Expand Down
36 changes: 23 additions & 13 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,8 @@ impl Backend {

/// Get the current state.
pub async fn serialized_state(&self) -> Result<SerializableState, BlockchainError> {
let state = self.db.read().await.dump_state()?;
let at = self.env.read().block.clone();
let state = self.db.read().await.dump_state(at)?;
state.ok_or_else(|| {
RpcError::invalid_params("Dumping state not supported with the current configuration")
.into()
Expand All @@ -735,8 +736,25 @@ impl Backend {
Ok(encoder.finish().unwrap_or_default().into())
}

/// Apply [SerializableState] data to the backend storage.
pub async fn load_state(&self, state: SerializableState) -> Result<bool, BlockchainError> {
// reset the block env
if let Some(block) = state.block.clone() {
self.env.write().block = block;
}

if !self.db.write().await.load_state(state)? {
Err(RpcError::invalid_params(
"Loading state not supported with the current configuration",
)
.into())
} else {
Ok(true)
}
}

/// Deserialize and add all chain data to the backend storage
pub async fn load_state(&self, buf: Bytes) -> Result<bool, BlockchainError> {
pub async fn load_state_bytes(&self, buf: Bytes) -> Result<bool, BlockchainError> {
let orig_buf = &buf.0[..];
let mut decoder = GzDecoder::new(orig_buf);
let mut decoded_data = Vec::new();
Expand All @@ -751,14 +769,7 @@ impl Backend {
})
.map_err(|_| BlockchainError::FailedToDecodeStateDump)?;

if !self.db.write().await.load_state(state)? {
Err(RpcError::invalid_params(
"Loading state not supported with the current configuration",
)
.into())
} else {
Ok(true)
}
self.load_state(state).await
}

/// Returns the environment for the next block
Expand Down Expand Up @@ -1060,15 +1071,14 @@ impl Backend {
env.block.basefee = base;
}

let gas_price =
gas_price.map(|g| g).or(max_fee_per_gas.map(|g| g)).unwrap_or_else(|| self.gas_price());
let gas_price = gas_price.or(max_fee_per_gas).unwrap_or_else(|| self.gas_price());
let caller = from.unwrap_or_default();

env.tx = TxEnv {
caller: caller.to_alloy(),
gas_limit: gas_limit.as_u64(),
gas_price,
gas_priority_fee: max_priority_fee_per_gas.map(|f| f),
gas_priority_fee: max_priority_fee_per_gas,
transact_to: match to {
Some(addr) => TransactTo::Call(addr.to_alloy()),
None => TransactTo::Create(CreateScheme::Create),
Expand Down
2 changes: 2 additions & 0 deletions crates/anvil/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ mod proof;
mod pubsub;
// mod revert; // TODO uncomment <https://github.com/gakonst/ethers-rs/issues/2186>
mod otterscan;

mod sign;
mod state;
mod traces;
mod transaction;
mod txpool;
Expand Down
23 changes: 23 additions & 0 deletions crates/anvil/tests/it/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! general eth api tests

use anvil::{spawn, NodeConfig};

#[tokio::test(flavor = "multi_thread")]
async fn can_load_state() {
let tmp = tempfile::tempdir().unwrap();
let state_file = tmp.path().join("state.json");

let (api, _handle) = spawn(NodeConfig::test()).await;

api.mine_one().await;

let num = api.block_number().unwrap();

let state = api.serialized_state().await.unwrap();
foundry_common::fs::write_json_file(&state_file, &state).unwrap();

let (api, _handle) = spawn(NodeConfig::test().with_init_state_path(state_file)).await;

let num2 = api.block_number().unwrap();
assert_eq!(num, num2);
}
Loading