Skip to content

Commit

Permalink
feat: rpc cache path for Solidity tests (#629)
Browse files Browse the repository at this point in the history
  • Loading branch information
agostbiro authored Aug 28, 2024
1 parent 0748836 commit 8fcf891
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 176 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions crates/edr_napi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,6 @@ export interface SolidityTestRunnerConfigArgs {
* Defaults to 33_554_432 (2^25 = 32MiB).
*/
memoryLimit?: bigint
/**
* If set to true, then block data from RPC endpoints in tests will not be
* cached. Otherwise, the data is cached to
* `$HOME/.foundry/cache/<chain id>/<block number>`.
* Defaults to false.
*/
noStorageCaching?: boolean
/**
* If set, all tests are run in fork mode using this url or remote name.
* Defaults to none.
Expand All @@ -494,6 +487,13 @@ export interface SolidityTestRunnerConfigArgs {
* e.g. `{ "optimism": "https://optimism.alchemyapi.io/v2/..." }`
*/
rpcEndpoints?: Record<string, string>
/**
* Optional RPC cache path. If this is none, then no RPC calls will be
* cached, otherwise data is cached to `<rpc_cache_path>/<chain
* id>/<block number>`. Caching can be disabled for specific chains
* with `rpc_storage_caching`.
*/
rpcCachePath?: string
/** What RPC endpoints are cached. Defaults to all. */
rpcStorageCaching?: StorageCachingConfig
/**
Expand Down
110 changes: 85 additions & 25 deletions crates/edr_napi/src/solidity_tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ pub struct SolidityTestRunnerConfigArgs {
/// The memory limit of the EVM in bytes.
/// Defaults to 33_554_432 (2^25 = 32MiB).
pub memory_limit: Option<BigInt>,
// TODO https://github.com/NomicFoundation/edr/issues/591
/// If set to true, then block data from RPC endpoints in tests will not be
/// cached. Otherwise, the data is cached to
/// `$HOME/.foundry/cache/<chain id>/<block number>`.
/// Defaults to false.
pub no_storage_caching: Option<bool>,
/// If set, all tests are run in fork mode using this url or remote name.
/// Defaults to none.
pub eth_rpc_url: Option<String>,
Expand All @@ -104,6 +98,11 @@ pub struct SolidityTestRunnerConfigArgs {
/// Map of RPC endpoints from chain name to RPC urls for fork cheat codes,
/// e.g. `{ "optimism": "https://optimism.alchemyapi.io/v2/..." }`
pub rpc_endpoints: Option<HashMap<String, String>>,
/// Optional RPC cache path. If this is none, then no RPC calls will be
/// cached, otherwise data is cached to `<rpc_cache_path>/<chain
/// id>/<block number>`. Caching can be disabled for specific chains
/// with `rpc_storage_caching`.
pub rpc_cache_path: Option<String>,
/// What RPC endpoints are cached. Defaults to all.
pub rpc_storage_caching: Option<StorageCachingConfig>,
/// The number of seconds to wait before `vm.prompt` reverts with a timeout.
Expand Down Expand Up @@ -142,7 +141,7 @@ impl Debug for SolidityTestRunnerConfigArgs {
.field("block_gas_limit", &self.block_gas_limit)
.field("memory_limit", &self.memory_limit)
.field("eth_rpc_url", &self.eth_rpc_url)
.field("no_storage_caching", &self.no_storage_caching)
.field("rpc_cache_path", &self.rpc_cache_path)
.field("rpc_endpoints", &self.rpc_endpoints)
.field("rpc_storage_caching", &self.rpc_storage_caching)
.field("prompt_timeout", &self.prompt_timeout)
Expand Down Expand Up @@ -179,7 +178,7 @@ impl TryFrom<SolidityTestRunnerConfigArgs> for SolidityTestRunnerConfig {
disable_block_gas_limit,
memory_limit,
eth_rpc_url,
no_storage_caching,
rpc_cache_path,
fork_block_number,
rpc_endpoints,
rpc_storage_caching,
Expand Down Expand Up @@ -207,6 +206,7 @@ impl TryFrom<SolidityTestRunnerConfigArgs> for SolidityTestRunnerConfig {
)
})
.unwrap_or_default(),
rpc_cache_path: rpc_cache_path.map(PathBuf::from),
rpc_storage_caching: rpc_storage_caching
.map(TryFrom::try_from)
.transpose()?
Expand Down Expand Up @@ -266,10 +266,6 @@ impl TryFrom<SolidityTestRunnerConfigArgs> for SolidityTestRunnerConfig {

evm_opts.fork_block_number = fork_block_number.map(TryCast::try_cast).transpose()?;

if let Some(no_storage_caching) = no_storage_caching {
evm_opts.no_storage_caching = no_storage_caching;
}

if let Some(isolate) = isolate {
evm_opts.isolate = isolate;
}
Expand Down Expand Up @@ -732,7 +728,6 @@ impl SolidityTestRunnerConfig {
always_use_create_2_factory: false,
memory_limit: 1 << 25, // 2**25 = 32MiB
isolate: false,
no_storage_caching: false,
disable_block_gas_limit: false,
}
}
Expand All @@ -751,11 +746,11 @@ impl SolidityTestRunnerConfig {
})?
.0;

let enable_caching = self.enable_caching(fork_url, evm_env.cfg.chain_id);
let rpc_cache_path = self.rpc_cache_path(fork_url, evm_env.cfg.chain_id);

Ok(Some(CreateFork {
rpc_cache_path,
url: fork_url.clone(),
enable_caching,
env: evm_env,
evm_opts: self.evm_opts.clone(),
}))
Expand All @@ -765,15 +760,80 @@ impl SolidityTestRunnerConfig {
}

/// Whether caching should be enabled for the given chain id
fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
!self.evm_opts.no_storage_caching
&& self
.cheats_config_options
.rpc_storage_caching
.enable_for_chain_id(chain_id.into())
&& self
.cheats_config_options
.rpc_storage_caching
.enable_for_endpoint(endpoint)
fn rpc_cache_path(&self, endpoint: &str, chain_id: impl Into<u64>) -> Option<PathBuf> {
let enable_for_chain_id = self
.cheats_config_options
.rpc_storage_caching
.enable_for_chain_id(chain_id.into());
let enable_for_endpoint = self
.cheats_config_options
.rpc_storage_caching
.enable_for_endpoint(endpoint);
if enable_for_chain_id && enable_for_endpoint {
self.cheats_config_options.rpc_cache_path.clone()
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use foundry_config::NamedChain;

use super::*;

impl SolidityTestRunnerConfig {
fn new_for_caching_tests(project_root: PathBuf) -> Self {
let cheats_config_options = CheatsConfigOptions {
rpc_endpoints: RpcEndpoints::default(),
rpc_cache_path: Some(project_root.join("rpc-cache")),
rpc_storage_caching: foundry_config::cache::StorageCachingConfig::default(),
unchecked_cheatcode_artifacts: false,
fs_permissions: FsPermissions::default(),
prompt_timeout: 120,
labels: HashMap::default(),
};
Self {
project_root,
debug: false,
trace: false,
cheats_config_options,
evm_opts: Self::default_evm_opts(),
fuzz: FuzzConfig::default(),
invariant: InvariantConfig::default(),
}
}
}

#[test]
fn test_rpc_cache_path() {
let mut config = SolidityTestRunnerConfig::new_for_caching_tests(
"/tmp/fake-path".parse().expect("path ok"),
);

let url = "https://eth-mainnet.alchemyapi";
assert_eq!(
config.rpc_cache_path(url, NamedChain::Mainnet),
Some("/tmp/fake-path/rpc-cache".parse().expect("path ok"))
);
assert!(config.rpc_cache_path(url, NamedChain::Dev).is_none());

config.cheats_config_options.rpc_storage_caching.chains =
foundry_config::cache::CachedChains::None;
assert!(config.rpc_cache_path(url, NamedChain::Mainnet).is_none());

config.cheats_config_options.rpc_storage_caching.chains =
foundry_config::cache::CachedChains::All;
assert!(config.rpc_cache_path(url, NamedChain::Mainnet).is_some());
config.cheats_config_options.rpc_storage_caching.endpoints =
foundry_config::cache::CachedEndpoints::Pattern("sepolia".parse().expect("regex ok"));
assert!(config.rpc_cache_path(url, NamedChain::Mainnet).is_none());

config.cheats_config_options.rpc_storage_caching.endpoints =
foundry_config::cache::CachedEndpoints::All;
assert!(config.rpc_cache_path(url, NamedChain::Mainnet).is_some());
config.cheats_config_options.rpc_cache_path = None;
assert!(config.rpc_cache_path(url, NamedChain::Mainnet).is_none());
}
}
16 changes: 15 additions & 1 deletion crates/foundry/cheatcodes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ pub struct CheatsConfig {
pub always_use_create_2_factory: bool,
/// Sets a timeout for vm.prompt cheatcodes
pub prompt_timeout: Duration,
/// Optional RPC cache path. If this is none, then no RPC calls will be
/// cached, otherwise data is cached to `<rpc_cache_path>/<chain
/// id>/<block number>`. Caching can be disabled for specific chains
/// with `rpc_storage_caching`.
pub rpc_cache_path: Option<PathBuf>,
/// RPC storage caching settings determines what chains and endpoints to
/// cache
pub rpc_storage_caching: StorageCachingConfig,
Expand All @@ -57,6 +62,11 @@ pub struct CheatsConfig {
pub struct CheatsConfigOptions {
/// Multiple rpc endpoints and their aliases
pub rpc_endpoints: RpcEndpoints,
/// Optional RPC cache path. If this is none, then no RPC calls will be
/// cached, otherwise data is cached to `<rpc_cache_path>/<chain
/// id>/<block number>`. Caching can be disabled for specific chains
/// with `rpc_storage_caching`.
pub rpc_cache_path: Option<PathBuf>,
/// RPC storage caching settings determines what chains and endpoints to
/// cache
pub rpc_storage_caching: StorageCachingConfig,
Expand All @@ -78,9 +88,10 @@ impl From<Config> for CheatsConfigOptions {
fn from(value: Config) -> Self {
Self {
rpc_endpoints: value.rpc_endpoints,
rpc_cache_path: value.eth_rpc_url.map(PathBuf::from),
rpc_storage_caching: value.rpc_storage_caching,
unchecked_cheatcode_artifacts: value.unchecked_cheatcode_artifacts,
prompt_timeout: value.prompt_timeout,
rpc_storage_caching: value.rpc_storage_caching,
fs_permissions: value.fs_permissions,
labels: value.labels,
}
Expand All @@ -98,6 +109,7 @@ impl CheatsConfig {
) -> Self {
let CheatsConfigOptions {
rpc_endpoints,
rpc_cache_path,
unchecked_cheatcode_artifacts,
prompt_timeout,
rpc_storage_caching,
Expand All @@ -121,6 +133,7 @@ impl CheatsConfig {
ffi: evm_opts.ffi,
always_use_create_2_factory: evm_opts.always_use_create_2_factory,
prompt_timeout: Duration::from_secs(prompt_timeout),
rpc_cache_path,
rpc_storage_caching,
rpc_endpoints,
fs_permissions,
Expand Down Expand Up @@ -260,6 +273,7 @@ impl Default for CheatsConfig {
ffi: false,
always_use_create_2_factory: false,
prompt_timeout: Duration::from_secs(120),
rpc_cache_path: None,
rpc_storage_caching: StorageCachingConfig::default(),
rpc_endpoints: ResolvedRpcEndpoints::default(),
fs_permissions: FsPermissions::default(),
Expand Down
6 changes: 1 addition & 5 deletions crates/foundry/cheatcodes/src/evm/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,7 @@ fn create_fork_request<DB: DatabaseExt>(
let mut evm_opts = ccx.state.config.evm_opts.clone();
evm_opts.fork_block_number = block;
let fork = CreateFork {
enable_caching: ccx
.state
.config
.rpc_storage_caching
.enable_for_endpoint(&url),
rpc_cache_path: ccx.state.config.rpc_cache_path.clone(),
url,
env: (*ccx.ecx.env).clone(),
evm_opts,
Expand Down
23 changes: 0 additions & 23 deletions crates/foundry/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,15 +917,6 @@ impl Config {
self.auto_detect_solc
}

/// Whether caching should be enabled for the given chain id
pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
!self.no_storage_caching
&& self
.rpc_storage_caching
.enable_for_chain_id(chain_id.into())
&& self.rpc_storage_caching.enable_for_endpoint(endpoint)
}

/// Returns the `ProjectPathsConfig` sub set of the config.
///
/// **NOTE**: this uses the paths as they are and does __not__ modify them,
Expand Down Expand Up @@ -2899,20 +2890,6 @@ mod tests {
);
}

#[test]
fn test_caching() {
let mut config = Config::default();
let chain_id = NamedChain::Mainnet;
let url = "https://eth-mainnet.alchemyapi";
assert!(config.enable_caching(url, chain_id));

config.no_storage_caching = true;
assert!(!config.enable_caching(url, chain_id));

config.no_storage_caching = false;
assert!(!config.enable_caching(url, NamedChain::Dev));
}

#[test]
fn test_install_dir() {
figment::Jail::expect_with(|jail| {
Expand Down
6 changes: 5 additions & 1 deletion crates/foundry/evm/core/src/fork/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,8 +826,12 @@ mod tests {

let (env, _block) = evm_opts.fork_evm_env(&endpoint).await.unwrap();

// TODO https://github.com/NomicFoundation/edr/issues/607
// Construct fork config the same way as the JS runner
let fork = CreateFork {
enable_caching: true,
rpc_cache_path: Some(
Config::foundry_rpc_cache_dir().expect("Could not get rpc cache dir"),
),
url: endpoint,
env: env.clone(),
evm_opts,
Expand Down
21 changes: 19 additions & 2 deletions crates/foundry/evm/core/src/fork/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use revm::primitives::Env;

use super::opts::EvmOpts;
Expand All @@ -10,6 +12,7 @@ pub use init::environment;

mod cache;
pub use cache::{BlockchainDb, BlockchainDbMeta, JsonBlockCacheDB, MemDb};
use foundry_config::Chain;

pub mod database;

Expand All @@ -20,8 +23,10 @@ pub use multi::{ForkId, MultiFork, MultiForkHandler};
/// `url` endpoint.
#[derive(Clone, Debug)]
pub struct CreateFork {
/// Whether to enable rpc storage caching for this fork
pub enable_caching: bool,
/// Optional RPC cache path. If this is none, then no rpc calls will be
/// cached, otherwise data is cached to `<rpc_cache_path>/<chain id>/<block
/// number>`.
pub rpc_cache_path: Option<PathBuf>,
/// The URL to a node for fetching remote state
pub url: String,
/// The env to create this fork, main purpose is to provide some metadata
Expand All @@ -30,3 +35,15 @@ pub struct CreateFork {
/// All env settings as configured by the user
pub evm_opts: EvmOpts,
}

impl CreateFork {
/// Returns the path to the cache dir of the `block` on the `chain`
/// based on the configured rpc cache path.
pub fn block_cache_dir(&self, chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
self.rpc_cache_path.as_ref().map(|rpc_cache_path| {
rpc_cache_path
.join(chain_id.into().to_string())
.join(block.to_string())
})
}
}
8 changes: 1 addition & 7 deletions crates/foundry/evm/core/src/fork/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use std::{
use foundry_common::provider::{
runtime_transport::RuntimeTransport, tower::RetryBackoffService, ProviderBuilder, RetryProvider,
};
use foundry_config::Config;
use futures::{
channel::mpsc::{channel, Receiver, Sender},
stream::{Fuse, Stream},
Expand Down Expand Up @@ -555,12 +554,7 @@ async fn create_fork(mut fork: CreateFork) -> eyre::Result<(ForkId, CreatedFork,
// be different on some L2s (e.g. Arbitrum).
let number = block.header.number.unwrap_or(meta.block_env.number.to());

// determine the cache path if caching is enabled
let cache_path = if fork.enable_caching {
Config::foundry_block_cache_dir(meta.cfg_env.chain_id, number)
} else {
None
};
let cache_path = fork.block_cache_dir(meta.cfg_env.chain_id, number);

let db = BlockchainDb::new(meta, cache_path);
let (backend, handler) = SharedBackend::new(provider, db, Some(number.into()));
Expand Down
Loading

0 comments on commit 8fcf891

Please sign in to comment.