diff --git a/Cargo.lock b/Cargo.lock index ebc0c4e0bbe1..bb06adac6ca3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,23 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-node-bindings" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27444ea67d360508753022807cdd0b49a95c878924c9c5f8f32668b7d7768245" +dependencies = [ + "alloy-genesis", + "alloy-primitives", + "k256", + "rand", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tracing", + "url", +] + [[package]] name = "alloy-primitives" version = "0.8.12" @@ -875,6 +892,7 @@ dependencies = [ "alloy-json-abi", "alloy-json-rpc", "alloy-network", + "alloy-node-bindings", "alloy-primitives", "alloy-provider", "alloy-pubsub", diff --git a/Cargo.toml b/Cargo.toml index f5ec94b906bc..b2008ad5b64d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -203,6 +203,7 @@ alloy-transport = { version = "0.6.4", default-features = false } alloy-transport-http = { version = "0.6.4", default-features = false } alloy-transport-ipc = { version = "0.6.4", default-features = false } alloy-transport-ws = { version = "0.6.4", default-features = false } +alloy-node-bindings = { version = "0.6.4", default-features = false } ## alloy-core alloy-dyn-abi = "0.8.11" diff --git a/crates/anvil/Cargo.toml b/crates/anvil/Cargo.toml index b3389d2eccae..39a8bc649dcb 100644 --- a/crates/anvil/Cargo.toml +++ b/crates/anvil/Cargo.toml @@ -117,6 +117,7 @@ alloy-rpc-client = { workspace = true, features = ["pubsub"] } alloy-transport-ipc = { workspace = true, features = ["mock"] } alloy-provider = { workspace = true, features = ["txpool-api"] } alloy-transport-ws.workspace = true +alloy-node-bindings.workspace = true alloy-json-rpc.workspace = true alloy-pubsub.workspace = true foundry-test-utils.workspace = true diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index 839dae0160b2..eda009418c14 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -189,6 +189,10 @@ pub struct NodeArgs { #[command(flatten)] pub server_config: ServerConfig, + + /// Path to the cache directory where states are stored. + #[arg(long, value_name = "PATH")] + pub cache_path: Option, } #[cfg(windows)] @@ -274,7 +278,8 @@ impl NodeArgs { .with_alphanet(self.evm_opts.alphanet) .with_disable_default_create2_deployer(self.evm_opts.disable_default_create2_deployer) .with_slots_in_an_epoch(self.slots_in_an_epoch) - .with_memory_limit(self.evm_opts.memory_limit)) + .with_memory_limit(self.evm_opts.memory_limit) + .with_cache_path(self.cache_path)) } fn account_generator(&self) -> AccountGenerator { diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index 714f91e6d7a8..ada48232904b 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -189,6 +189,8 @@ pub struct NodeConfig { pub alphanet: bool, /// Do not print log messages. pub silent: bool, + /// The path where states are cached. + pub cache_path: Option, } impl NodeConfig { @@ -465,6 +467,7 @@ impl Default for NodeConfig { precompile_factory: None, alphanet: false, silent: false, + cache_path: None, } } } @@ -969,6 +972,13 @@ impl NodeConfig { self } + /// Sets the path where states are cached + #[must_use] + pub fn with_cache_path(mut self, cache_path: Option) -> Self { + self.cache_path = cache_path; + self + } + /// Configures everything related to env, backend and database and returns the /// [Backend](mem::Backend) /// @@ -1051,6 +1061,7 @@ impl NodeConfig { self.max_persisted_states, self.transaction_block_keeper, self.block_time, + self.cache_path.clone(), Arc::new(tokio::sync::RwLock::new(self.clone())), ) .await; diff --git a/crates/anvil/src/eth/backend/mem/cache.rs b/crates/anvil/src/eth/backend/mem/cache.rs index e51aaae7e1ae..51b92c3d65d7 100644 --- a/crates/anvil/src/eth/backend/mem/cache.rs +++ b/crates/anvil/src/eth/backend/mem/cache.rs @@ -18,6 +18,10 @@ pub struct DiskStateCache { } impl DiskStateCache { + /// Specify the path where to create the tempdir in + pub fn with_path(self, temp_path: PathBuf) -> Self { + Self { temp_path: Some(temp_path), temp_dir: None } + } /// Returns the cache file for the given hash fn with_cache_file(&mut self, hash: B256, f: F) -> Option where diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 0db343358fec..83718ad821aa 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -103,12 +103,12 @@ use revm::{ use std::{ collections::BTreeMap, io::{Read, Write}, + path::PathBuf, sync::Arc, time::Duration, }; use storage::{Blockchain, MinedTransaction, DEFAULT_HISTORY_LIMIT}; use tokio::sync::RwLock as AsyncRwLock; - pub mod cache; pub mod fork_db; pub mod in_memory_db; @@ -227,6 +227,7 @@ impl Backend { max_persisted_states: Option, transaction_block_keeper: Option, automine_block_time: Option, + cache_path: Option, node_config: Arc>, ) -> Self { // if this is a fork then adjust the blockchain storage @@ -249,7 +250,7 @@ impl Backend { genesis.timestamp }; - let states = if prune_state_history_config.is_config_enabled() { + let mut states = if prune_state_history_config.is_config_enabled() { // if prune state history is enabled, configure the state cache only for memory prune_state_history_config .max_memory_history @@ -264,6 +265,10 @@ impl Backend { Default::default() }; + if let Some(cache_path) = cache_path { + states = states.disk_path(cache_path); + } + let (slots_in_an_epoch, precompile_factory) = { let cfg = node_config.read().await; (cfg.slots_in_an_epoch, cfg.precompile_factory.clone()) diff --git a/crates/anvil/src/eth/backend/mem/storage.rs b/crates/anvil/src/eth/backend/mem/storage.rs index d5a72fccbbb6..056b886277c9 100644 --- a/crates/anvil/src/eth/backend/mem/storage.rs +++ b/crates/anvil/src/eth/backend/mem/storage.rs @@ -41,7 +41,7 @@ use foundry_evm::{ }; use parking_lot::RwLock; use revm::primitives::SpecId; -use std::{collections::VecDeque, fmt, sync::Arc, time::Duration}; +use std::{collections::VecDeque, fmt, path::PathBuf, sync::Arc, time::Duration}; // use yansi::Paint; // === various limits in number of blocks === @@ -94,6 +94,12 @@ impl InMemoryBlockStates { self } + /// Configures the path on disk where the states will cached. + pub fn disk_path(mut self, path: PathBuf) -> Self { + self.disk_cache = self.disk_cache.with_path(path); + self + } + /// This modifies the `limit` what to keep stored in memory. /// /// This will ensure the new limit adjusts based on the block time. diff --git a/crates/anvil/tests/it/anvil.rs b/crates/anvil/tests/it/anvil.rs index 65eeac70baf7..b5ed0c85312f 100644 --- a/crates/anvil/tests/it/anvil.rs +++ b/crates/anvil/tests/it/anvil.rs @@ -2,9 +2,11 @@ use alloy_consensus::EMPTY_ROOT_HASH; use alloy_eips::BlockNumberOrTag; -use alloy_primitives::Address; +use alloy_node_bindings::utils::run_with_tempdir; +use alloy_primitives::{Address, U256}; use alloy_provider::Provider; use anvil::{spawn, EthereumHardfork, NodeConfig}; +use std::time::Duration; #[tokio::test(flavor = "multi_thread")] async fn test_can_change_mining_mode() { @@ -118,3 +120,33 @@ async fn test_cancun_fields() { assert!(block.header.blob_gas_used.is_some()); assert!(block.header.excess_blob_gas.is_some()); } + +#[tokio::test(flavor = "multi_thread")] +#[cfg(not(windows))] +async fn test_cache_path() { + run_with_tempdir("custom-anvil-cache", |tmp_dir| async move { + let cache_path = tmp_dir.join("cache"); + let (api, _handle) = spawn( + NodeConfig::test() + .with_cache_path(Some(cache_path.clone())) + .with_max_persisted_states(Some(5_usize)) + .with_blocktime(Some(Duration::from_millis(1))), + ) + .await; + + api.anvil_mine(Some(U256::from(1000)), None).await.unwrap(); + + // sleep to ensure the cache is written + tokio::time::sleep(Duration::from_secs(2)).await; + + assert!(cache_path.exists()); + assert!(cache_path.read_dir().unwrap().count() > 0); + + // Clean the directory, this is to prevent an error when temp_dir is dropped. + let _ = std::fs::remove_dir_all(cache_path); + + //sleep to ensure OS file handles are released + tokio::time::sleep(Duration::from_secs(1)).await; + }) + .await; +}