diff --git a/Cargo.lock b/Cargo.lock index c28ec11836d09..1d622f61d870f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,7 +500,7 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cast" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono 0.2.25", "ethers-core", @@ -715,7 +715,7 @@ dependencies = [ "indenter", "once_cell", "owo-colors", - "tracing-error", + "tracing-error 0.1.2", ] [[package]] @@ -727,7 +727,7 @@ dependencies = [ "once_cell", "owo-colors", "tracing-core", - "tracing-error", + "tracing-error 0.1.2", ] [[package]] @@ -1602,7 +1602,7 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "forge" -version = "0.1.0" +version = "0.2.0" dependencies = [ "comfy-table", "ethers", @@ -1621,12 +1621,12 @@ dependencies = [ "serde_json", "tokio", "tracing", - "tracing-subscriber 0.2.25", + "tracing-subscriber 0.3.9", ] [[package]] name = "forge-fmt" -version = "0.1.0" +version = "0.2.0" dependencies = [ "indent_write", "pretty_assertions", @@ -1677,7 +1677,7 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "0.1.0" +version = "0.2.0" dependencies = [ "ansi_term", "atty", @@ -1713,7 +1713,8 @@ dependencies = [ "tokio", "toml", "tracing", - "tracing-subscriber 0.2.25", + "tracing-error 0.2.0", + "tracing-subscriber 0.3.9", "ui", "vergen", "walkdir", @@ -1734,7 +1735,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "0.1.0" +version = "0.2.0" dependencies = [ "Inflector", "dirs-next", @@ -1752,7 +1753,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "0.1.0" +version = "0.2.0" dependencies = [ "ansi_term", "bytes", @@ -1772,12 +1773,14 @@ dependencies = [ "thiserror", "tokio", "tracing", - "tracing-subscriber 0.2.25", + "tracing-error 0.2.0", + "tracing-subscriber 0.3.9", + "url", ] [[package]] name = "foundry-utils" -version = "0.1.0" +version = "0.2.0" dependencies = [ "ethers", "ethers-addressbook", @@ -2079,6 +2082,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hidapi-rusb" @@ -2523,15 +2529,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "matchers" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" -dependencies = [ - "regex-automata", -] - [[package]] name = "matchers" version = "0.1.0" @@ -3626,23 +3623,25 @@ dependencies = [ [[package]] name = "revm" version = "1.2.0" -source = "git+https://github.com/bluealloy/revm#cb5fbb9adc36c8ee127dce92259356cecbb829b8" +source = "git+https://github.com/bluealloy/revm#0437dcdc658f8a950e4c652d587d266a4bec35ff" dependencies = [ "arrayref", "auto_impl", "bytes", "hashbrown 0.12.0", + "hex", "num_enum", "primitive-types", "revm_precompiles", "rlp", + "serde", "sha3 0.10.1", ] [[package]] name = "revm_precompiles" version = "0.4.0" -source = "git+https://github.com/bluealloy/revm#cb5fbb9adc36c8ee127dce92259356cecbb829b8" +source = "git+https://github.com/bluealloy/revm#0437dcdc658f8a950e4c652d587d266a4bec35ff" dependencies = [ "bytes", "k256", @@ -4623,6 +4622,16 @@ dependencies = [ "tracing-subscriber 0.2.25", ] +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.9", +] + [[package]] name = "tracing-futures" version = "0.2.5" @@ -4644,36 +4653,15 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" dependencies = [ - "ansi_term", - "chrono 0.4.19", - "lazy_static", - "matchers 0.0.1", - "regex", - "serde", - "serde_json", "sharded-slab", - "smallvec", "thread_local", - "tracing", "tracing-core", - "tracing-log", - "tracing-serde", ] [[package]] @@ -4684,7 +4672,7 @@ checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" dependencies = [ "ansi_term", "lazy_static", - "matchers 0.1.0", + "matchers", "regex", "sharded-slab", "smallvec", @@ -4758,7 +4746,7 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ui" -version = "0.1.0" +version = "0.2.0" dependencies = [ "crossterm 0.22.1", "ethers", diff --git a/Cargo.toml b/Cargo.toml index 5c2a9ab64cf89..f788086996c8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,7 @@ members = [ "config", "fmt", "ui", - "evm", - "node", - "node/node-core" + "evm" ] [profile.test] diff --git a/cast/Cargo.toml b/cast/Cargo.toml index 2ea38ea4153c1..f23b98eece040 100644 --- a/cast/Cargo.toml +++ b/cast/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cast" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index de3fad4f7f10b..0630ec7b7ab80 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "foundry-cli" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" @@ -39,7 +39,8 @@ tokio = { version = "1.11.0", features = ["macros"] } regex = { version = "1.5.4", default-features = false } ansi_term = "0.12.1" rpassword = "5.0.1" -tracing-subscriber = "0.2.20" +tracing-error = "0.2.0" +tracing-subscriber = { version = "0.3", features = ["registry", "env-filter", "fmt"] } tracing = "0.1.26" hex = "0.4.3" rayon = "1.5.1" diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs index 83a9d846841ae..174119915d644 100644 --- a/cli/src/cmd/mod.rs +++ b/cli/src/cmd/mod.rs @@ -5,19 +5,8 @@ //! implement `figment::Provider` which allows the subcommand to override the config's defaults, see //! [`foundry_config::Config`]. -pub mod bind; -pub mod build; -pub mod config; -pub mod create; -pub mod flatten; -pub mod init; -pub mod install; -pub mod node; -pub mod remappings; -pub mod run; -pub mod snapshot; -pub mod test; -pub mod verify; +pub mod cast; +pub mod forge; // Re-export our shared utilities mod utils; diff --git a/cli/src/opts/evm.rs b/cli/src/opts/evm.rs index c2a160becc987..428b9e7f72b89 100644 --- a/cli/src/opts/evm.rs +++ b/cli/src/opts/evm.rs @@ -177,43 +177,3 @@ pub struct EnvArgs { #[serde(skip_serializing_if = "Option::is_none")] pub block_gas_limit: Option, } - -impl EnvArgs { - #[cfg(feature = "sputnik")] - pub fn sputnik_state(&self) -> MemoryVicinity { - MemoryVicinity { - chain_id: self.chain_id.unwrap_or_default().into(), - - gas_price: self.gas_price.unwrap_or_default().into(), - origin: self.tx_origin.unwrap_or_default(), - - block_coinbase: self.block_coinbase.unwrap_or_default(), - block_number: self.block_number.unwrap_or_default().into(), - block_timestamp: self.block_timestamp.unwrap_or_default().into(), - block_difficulty: self.block_difficulty.unwrap_or_default().into(), - block_base_fee_per_gas: self.block_base_fee_per_gas.unwrap_or_default().into(), - block_gas_limit: self - .block_gas_limit - .unwrap_or_else(|| self.gas_limit.unwrap_or_default()) - .into(), - block_hashes: Vec::new(), - } - } - - #[cfg(feature = "evmodin")] - pub fn evmodin_state(&self) -> MockedHost { - let mut host = MockedHost::default(); - - host.tx_context.chain_id = self.chain_id.unwrap_or_default().into(); - host.tx_context.tx_gas_price = self.gas_price.unwrap_or_default().into(); - host.tx_context.tx_origin = self.tx_origin.unwrap_or_default(); - host.tx_context.block_coinbase = self.block_coinbase.unwrap_or_default(); - host.tx_context.block_number = self.block_number.unwrap_or_default(); - host.tx_context.block_timestamp = self.block_timestamp.unwrap_or_default(); - host.tx_context.block_difficulty = self.block_difficulty.unwrap_or_default().into(); - host.tx_context.block_gas_limit = - self.block_gas_limit.unwrap_or(self.gas_limit.unwrap_or_default()); - - host - } -} diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index 9faf5713aeb57..7e49e21a6d5e1 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -2,10 +2,19 @@ use clap::{Parser, Subcommand, ValueHint}; use ethers::{solc::EvmVersion, types::Address}; use std::{path::PathBuf, str::FromStr}; -use crate::cmd::{ - bind::BindArgs, build::BuildArgs, config, create::CreateArgs, flatten, init::InitArgs, - install::InstallArgs, node::NodeArgs, remappings::RemappingArgs, run::RunArgs, snapshot, - test::TestArgs, +use crate::cmd::forge::{ + bind::BindArgs, + build::BuildArgs, + config, + create::CreateArgs, + flatten, + init::InitArgs, + inspect, + install::InstallArgs, + remappings::RemappingArgs, + run::RunArgs, + snapshot, test, tree, + verify::{VerifyArgs, VerifyCheckArgs}, }; use serde::Serialize; diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 7d699d5f9cec3..35896d6388fff 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -5,6 +5,12 @@ use std::{ time::Duration, }; +use ethers::{solc::EvmVersion, types::U256}; +use forge::executor::{opts::EvmOpts, Fork, SpecId}; +use foundry_config::{caching::StorageCachingConfig, Config}; +use tracing_error::ErrorLayer; +use tracing_subscriber::prelude::*; + use ethers::{ providers::{Middleware, Provider}, solc::EvmVersion, @@ -60,10 +66,11 @@ impl> FoundryPathExt for T { /// Initializes a tracing Subscriber for logging #[allow(dead_code)] pub fn subscriber() { - tracing_subscriber::FmtSubscriber::builder() - // .with_timer(tracing_subscriber::fmt::time::uptime()) - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); + tracing_subscriber::Registry::default() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(ErrorLayer::default()) + .with(tracing_subscriber::fmt::layer()) + .init() } pub fn evm_spec(evm: &EvmVersion) -> SpecId { @@ -75,17 +82,6 @@ pub fn evm_spec(evm: &EvmVersion) -> SpecId { } } -#[cfg(feature = "evmodin-evm")] -#[allow(dead_code)] -pub fn evmodin_cfg(evm: &EvmVersion) -> Revision { - match evm { - EvmVersion::Istanbul => Revision::Istanbul, - EvmVersion::Berlin => Revision::Berlin, - EvmVersion::London => Revision::London, - _ => panic!("Unsupported EVM version"), - } -} - /// Securely reads a secret from stdin, or proceeds to return a fallback value /// which was provided in cleartext via CLI or env var #[allow(dead_code)] @@ -170,14 +166,20 @@ pub fn block_on(future: F) -> F::Output { /// - storage is allowed (`no_storage_caching = false`) /// /// If all these criteria are met, then storage caching is enabled and storage info will be written -/// to [Config::data_dir()]//block/storage.json +/// to [Config::foundry_cache_dir()]///storage.json /// /// for `mainnet` and `--fork-block-number 14435000` on mac the corresponding storage cache will be -/// at `$HOME`/Library/Application Support/foundry/mainnet/14435000/storage.json` +/// at `~/.foundry/cache/mainnet/14435000/storage.json` pub fn get_fork(evm_opts: &EvmOpts, config: &StorageCachingConfig) -> Option { - fn get_cache_storage_path( + /// Returns the path where the cache file should be stored + /// + /// or `None` if caching should not be enabled + /// + /// See also [ Config::foundry_block_cache_file()] + fn get_block_storage_path( evm_opts: &EvmOpts, config: &StorageCachingConfig, + chain_id: u64, ) -> Option { if evm_opts.no_storage_caching { // storage caching explicitly opted out of @@ -186,33 +188,23 @@ pub fn get_fork(evm_opts: &EvmOpts, config: &StorageCachingConfig) -> Option { - let chain_id: u64 = chain_id.try_into().ok()?; - if config.enable_for_chain_id(chain_id) { - let chain = if let Ok(chain) = ethers::types::Chain::try_from(chain_id) { - chain.to_string() - } else { - format!("{}", chain_id) - }; - return Some(Config::data_dir().ok()?.join(chain).join(format!("{}", block))) - } - } - Err(err) => { - tracing::warn!("Failed to get chain id for {}: {:?}", url, err); - } - } + + if config.enable_for_endpoint(url) && config.enable_for_chain_id(chain_id) { + return Config::foundry_block_cache_file(chain_id, block) } None } if let Some(ref url) = evm_opts.fork_url { - let cache_storage = get_cache_storage_path(evm_opts, config); - let fork = Fork { url: url.clone(), pin_block: evm_opts.fork_block_number, cache_storage }; + let chain_id = evm_opts.get_chain_id(); + let cache_storage = get_block_storage_path(evm_opts, config, chain_id); + let fork = Fork { + url: url.clone(), + pin_block: evm_opts.fork_block_number, + cache_path: cache_storage, + chain_id, + }; return Some(fork) } diff --git a/config/Cargo.toml b/config/Cargo.toml index bec87627b4733..ba5059a02c42c 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "foundry-config" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = """ Foundry configuration diff --git a/config/src/lib.rs b/config/src/lib.rs index 274bc4363a361..f6ecaa7ebfbfd 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -3,6 +3,7 @@ extern crate core; use std::{ borrow::Cow, + fmt, path::{Path, PathBuf}, str::FromStr, }; @@ -709,6 +710,22 @@ impl Config { dirs_next::home_dir().map(|p| p.join(Config::FOUNDRY_DIR_NAME)) } + /// Returns the path to foundry's cache dir `~/.foundry/cache` + pub fn foundry_cache_dir() -> Option { + Self::foundry_dir().map(|p| p.join("cache")) + } + + /// Returns the path to the cache file of the `block` on the `chain` + /// `~/.foundry/cache///storage.json` + pub fn foundry_block_cache_file(chain_id: impl Into, block: u64) -> Option { + Some( + Config::foundry_cache_dir()? + .join(chain_id.into().to_string()) + .join(format!("{}", block)) + .join("storage.json"), + ) + } + #[doc = r#"Returns the path to `foundry`'s data directory inside the user's data directory |Platform | Value | Example | | ------- | ------------------------------------- | -------------------------------- | @@ -1278,6 +1295,42 @@ impl Chain { } } +impl fmt::Display for Chain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Chain::Named(chain) => chain.fmt(f), + Chain::Id(id) => { + if let Ok(chain) = ethers_core::types::Chain::try_from(*id) { + chain.fmt(f) + } else { + id.fmt(f) + } + } + } + } +} + +impl From for Chain { + fn from(id: ethers_core::types::Chain) -> Self { + Chain::Named(id) + } +} + +impl From for Chain { + fn from(id: u64) -> Self { + Chain::Id(id) + } +} + +impl From for u64 { + fn from(c: Chain) -> Self { + match c { + Chain::Named(c) => c as u64, + Chain::Id(id) => id, + } + } +} + impl<'de> Deserialize<'de> for Chain { fn deserialize(deserializer: D) -> Result where @@ -1324,15 +1377,6 @@ mod from_str_lowercase { } } -impl From for u64 { - fn from(c: Chain) -> Self { - match c { - Chain::Named(c) => c as u64, - Chain::Id(id) => id, - } - } -} - fn canonic(path: impl Into) -> PathBuf { let path = path.into(); ethers_solc::utils::canonicalize(&path).unwrap_or(path) diff --git a/evm/Cargo.toml b/evm/Cargo.toml index 6edae15b7c929..94676aa4c1288 100644 --- a/evm/Cargo.toml +++ b/evm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "foundry-evm" -version = "0.1.0" +version = "0.2.0" edition = "2021" # TODO: We can probably reduce dependencies here or in the forge crate @@ -19,7 +19,8 @@ thiserror = "1.0.29" # Logging tracing = "0.1.26" -tracing-subscriber = "0.2.20" +tracing-subscriber = "0.3" +tracing-error = "0.2.0" # Threading/futures tokio = { version = "1.10.1" } @@ -30,13 +31,14 @@ once_cell = "1.9.0" # EVM bytes = "1.1.0" hashbrown = "0.12" -revm = { package = "revm", git = "https://github.com/bluealloy/revm", default-features = false, features = ["std", "k256"] } +revm = { package = "revm", git = "https://github.com/bluealloy/revm", default-features = false, features = ["std", "k256", "with-serde"] } # Fuzzer proptest = "1.0.0" # Display ansi_term = "0.12.1" +url = "2.2.2" [dev-dependencies] tempfile = "3.3.0" diff --git a/evm/src/executor/builder.rs b/evm/src/executor/builder.rs index 76461b0e6b9c4..3bb2714398e95 100644 --- a/evm/src/executor/builder.rs +++ b/evm/src/executor/builder.rs @@ -3,19 +3,16 @@ use revm::{ db::{DatabaseRef, EmptyDB}, Env, SpecId, }; -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; -use super::{ - fork::{SharedBackend, SharedMemCache}, - inspector::InspectorStackConfig, - Executor, -}; +use super::{fork::SharedBackend, inspector::InspectorStackConfig, Executor}; -use crate::storage::StorageMap; use ethers::types::{H160, H256, U256}; -use parking_lot::lock_api::RwLock; +use crate::executor::fork::{BlockchainDb, BlockchainDbMeta}; + use revm::AccountInfo; +use url::Url; #[derive(Default, Debug)] pub struct ExecutorBuilder { @@ -29,36 +26,41 @@ pub struct ExecutorBuilder { #[derive(Clone, Debug)] pub struct Fork { /// Where to read the cached storage from - pub cache_storage: Option, + pub cache_path: Option, /// The URL to a node for fetching remote state pub url: String, /// The block to fork against pub pin_block: Option, + /// chain id retrieved from the endpoint + pub chain_id: u64, } impl Fork { /// Initialises the Storage Backend /// - /// If configured, then this will initialise the backend with the storage cahce - fn into_backend(self) -> SharedBackend { - let Fork { cache_storage, url, pin_block } = self; + /// If configured, then this will initialise the backend with the storage cache + pub fn into_backend(self, env: &Env) -> SharedBackend { + let Fork { cache_path, url, pin_block, chain_id } = self; + + let host = Url::parse(&url) + .ok() + .and_then(|url| url.host().map(|host| host.to_string())) + .unwrap_or_else(|| url.clone()); + let provider = Provider::try_from(url).expect("Failed to establish provider"); - let mut storage_map = if let Some(cached_storage) = cache_storage { - StorageMap::read(cached_storage) - } else { - StorageMap::transient() - }; - - SharedBackend::new( - provider, - SharedMemCache { - storage: Arc::new(RwLock::new(storage_map.take_storage())), - ..Default::default() - }, - pin_block.map(Into::into), - storage_map, - ) + let mut meta = + BlockchainDbMeta { cfg_env: env.cfg.clone(), block_env: env.block.clone(), host }; + + // update the meta to match the forked config + meta.cfg_env.chain_id = chain_id.into(); + if let Some(pin) = pin_block { + meta.block_env.number = pin.into(); + } + + let db = BlockchainDb::new(meta, cache_path); + + SharedBackend::new(provider, db, pin_block.map(Into::into)) } } @@ -69,9 +71,9 @@ pub enum Backend { impl Backend { /// Instantiates a new backend union based on whether there was or not a fork url specified - fn new(fork: Option) -> Self { + fn new(fork: Option, env: &Env) -> Self { if let Some(fork) = fork { - Backend::Forked(fork.into_backend()) + Backend::Forked(fork.into_backend(env)) } else { Backend::Simple(EmptyDB()) } @@ -160,7 +162,7 @@ impl ExecutorBuilder { /// Builds the executor as configured. pub fn build(self) -> Executor { - let db = Backend::new(self.fork); + let db = Backend::new(self.fork, &self.env); Executor::new(db, self.env, self.inspector_config) } } diff --git a/evm/src/executor/fork/cache.rs b/evm/src/executor/fork/cache.rs new file mode 100644 index 0000000000000..918644fc567f7 --- /dev/null +++ b/evm/src/executor/fork/cache.rs @@ -0,0 +1,227 @@ +//! Cache related abstraction +use ethers::types::{Address, H256, U256}; +use parking_lot::RwLock; +use revm::AccountInfo; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; +use std::{collections::BTreeMap, fs, io::BufWriter, path::PathBuf, sync::Arc}; +use tracing::{trace, trace_span, warn}; +use tracing_error::InstrumentResult; + +pub type StorageInfo = BTreeMap; + +/// A shareable Block database +#[derive(Clone, Debug)] +pub struct BlockchainDb { + /// Contains all the data + db: Arc, + /// metadata of the current config + meta: Arc>, + /// the cache that can be flushed + cache: Arc, +} + +impl BlockchainDb { + /// Creates a new instance of the [BlockchainDb] + /// + /// if a `cache_path` is provided it attempts to load a previously stored [JsonBlockCacheData] + /// and will try to use the cached entries it holds. + /// + /// This will return a new and empty [MemDb] if + /// - `cache_path` is `None` + /// - the file the `cache_path` points to, does not exist + /// - the file contains malformed data, or if it couldn't be read + /// - the provided `meta` differs from [BlockchainDbMeta] that's stored on disk + pub fn new(meta: BlockchainDbMeta, cache_path: Option) -> Self { + // read cache and check if metadata matches + let cache = cache_path + .as_ref() + .and_then(|p| { + JsonBlockCacheDB::load(p).ok().filter(|cache| { + if meta != *cache.meta().read() { + warn!(target:"cache", "non-matching block metadata"); + false + } else { + true + } + }) + }) + .unwrap_or_else(|| JsonBlockCacheDB::new(Arc::new(RwLock::new(meta)), cache_path)); + + Self { db: Arc::clone(cache.db()), meta: Arc::clone(cache.meta()), cache: Arc::new(cache) } + } + + /// Returns the map that holds the account related info + pub fn accounts(&self) -> &RwLock> { + &self.db.accounts + } + + /// Returns the map that holds the storage related info + pub fn storage(&self) -> &RwLock> { + &self.db.storage + } + + /// Returns the map that holds all the block hashes + pub fn block_hashes(&self) -> &RwLock> { + &self.db.block_hashes + } + + /// Returns the [revm::Env] related metadata + pub fn meta(&self) -> &Arc> { + &self.meta + } + + /// Returns the inner cache + pub fn cache(&self) -> &Arc { + &self.cache + } +} + +/// relevant identifying markers in the context of [BlockchainDb] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct BlockchainDbMeta { + pub cfg_env: revm::CfgEnv, + pub block_env: revm::BlockEnv, + pub host: String, +} + +/// In Memory cache containing all fetched accounts and storage slots +/// and their values from RPC +#[derive(Debug, Default)] +pub struct MemDb { + /// Account related data + pub accounts: RwLock>, + /// Storage related data + pub storage: RwLock>, + /// All retrieved block hashes + pub block_hashes: RwLock>, +} + +/// A [BlockCacheDB] that stores the cached content in a json file +#[derive(Debug)] +pub struct JsonBlockCacheDB { + /// Where this cache file is stored. + /// + /// If this is a [None] then caching is disabled + cache_path: Option, + /// Object that's stored in a json file + data: JsonBlockCacheData, +} + +impl JsonBlockCacheDB { + /// Creates a new instance. + fn new(meta: Arc>, cache_path: Option) -> Self { + Self { cache_path, data: JsonBlockCacheData { meta, data: Arc::new(Default::default()) } } + } + + /// Loads the contents of the diskmap file and returns the read object + /// + /// # Errors + /// This will fail if + /// - the `path` does not exist + /// - the format does not match [JsonBlockCacheData] + pub fn load(path: impl Into) -> eyre::Result { + let path = path.into(); + trace!(target: "cache", "reading json cache path={:?}", path); + let span = trace_span!("cache", "path={:?}", &path); + let _enter = span.enter(); + let file = std::fs::File::open(&path).in_current_span()?; + let file = std::io::BufReader::new(file); + let data = serde_json::from_reader(file).in_current_span()?; + Ok(Self { cache_path: Some(path), data }) + } + + /// Returns the [MemDb] it holds access to + pub fn db(&self) -> &Arc { + &self.data.data + } + + /// Metadata stored alongside the data + pub fn meta(&self) -> &Arc> { + &self.data.meta + } + + /// Returns `true` if this is a transient cache and nothing will be flushed + pub fn is_transient(&self) -> bool { + self.cache_path.is_none() + } + + /// Flushes the DB to disk if caching is enabled + pub fn flush(&self) { + // writes the data to a json file + if let Some(ref path) = self.cache_path { + trace!(target: "cache", "saving json cache path={:?}", path); + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + let _ = fs::File::create(path) + .map_err(|e| warn!(target: "cache", "Failed to open json cache for writing: {}", e)) + .and_then(|f| { + serde_json::to_writer(BufWriter::new(f), &self.data) + .map_err(|e| warn!(target: "cache" ,"Failed to write to json cache: {}", e)) + }); + } + } +} + +/// The Data the [JsonBlockCacheDB] can read and flush +/// +/// This will be deserialized in a JSON object with the keys: +/// `["meta", "accounts", "storage", "block_hashes"]` +#[derive(Debug)] +pub struct JsonBlockCacheData { + pub meta: Arc>, + pub data: Arc, +} + +impl Serialize for JsonBlockCacheData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(4))?; + + let meta = self.meta.read(); + map.serialize_entry("meta", &*meta)?; + drop(meta); + + let accounts = self.data.accounts.read(); + map.serialize_entry("accounts", &*accounts)?; + drop(accounts); + + let storage = self.data.storage.read(); + map.serialize_entry("storage", &*storage)?; + drop(storage); + + let block_hashes = self.data.block_hashes.read(); + map.serialize_entry("block_hashes", &*block_hashes)?; + drop(block_hashes); + + map.end() + } +} + +impl<'de> Deserialize<'de> for JsonBlockCacheData { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct Data { + meta: BlockchainDbMeta, + accounts: BTreeMap, + storage: BTreeMap, + block_hashes: BTreeMap, + } + + let Data { meta, accounts, storage, block_hashes } = Data::deserialize(deserializer)?; + + Ok(JsonBlockCacheData { + meta: Arc::new(RwLock::new(meta)), + data: Arc::new(MemDb { + accounts: RwLock::new(accounts), + storage: RwLock::new(storage), + block_hashes: RwLock::new(block_hashes), + }), + }) + } +} diff --git a/evm/src/executor/fork/mod.rs b/evm/src/executor/fork/mod.rs index c7ba153c78f6d..aaa8398477de7 100644 --- a/evm/src/executor/fork/mod.rs +++ b/evm/src/executor/fork/mod.rs @@ -1,5 +1,8 @@ mod backend; -pub use backend::{SharedBackend, SharedMemCache}; +pub use backend::SharedBackend; mod init; pub use init::environment; + +mod cache; +pub use cache::{BlockchainDb, BlockchainDbMeta, JsonBlockCacheDB}; diff --git a/evm/src/executor/opts.rs b/evm/src/executor/opts.rs index 393728792f30e..67d5ea9cddbcb 100644 --- a/evm/src/executor/opts.rs +++ b/evm/src/executor/opts.rs @@ -1,5 +1,5 @@ use ethers::{ - providers::Provider, + providers::{Middleware, Provider}, types::{Address, U256}, }; use foundry_utils::RuntimeOrHandle; @@ -36,39 +36,6 @@ pub struct EvmOpts { pub verbosity: u8, } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Env { - /// the block gas limit - pub gas_limit: u64, - - /// the chainid opcode value - pub chain_id: Option, - - /// the tx.gasprice value during EVM execution - pub gas_price: u64, - - /// the base fee in a block - pub block_base_fee_per_gas: u64, - - /// the tx.origin value during EVM execution - pub tx_origin: Address, - - /// the block.coinbase value during EVM execution - pub block_coinbase: Address, - - /// the block.timestamp value during EVM execution - pub block_timestamp: u64, - - /// the block.number value during EVM execution" - pub block_number: u64, - - /// the block.difficulty value during EVM execution - pub block_difficulty: u64, - - /// the block.gaslimit value during EVM execution - pub block_gas_limit: Option, -} - impl EvmOpts { pub fn evm_env(&self) -> revm::Env { if let Some(ref fork_url) = self.fork_url { @@ -106,4 +73,62 @@ impl EvmOpts { } } } + + /// Returns the configured chain id, which will be + /// - the value of `chain_id` if set + /// - mainnet if `fork_url` contains "mainnet" + /// - the chain if `fork_url` is set and the endpoints returned its chain id successfully + /// - mainnet otherwise + pub fn get_chain_id(&self) -> u64 { + use ethers::types::Chain; + if let Some(id) = self.env.chain_id { + return id + } + if let Some(ref url) = self.fork_url { + if url.contains("mainnet") { + tracing::trace!("auto detected mainnet chain from url {}", url); + return Chain::Mainnet as u64 + } + let provider = Provider::try_from(url.as_str()) + .unwrap_or_else(|_| panic!("Failed to establish provider to {}", url)); + + if let Ok(id) = foundry_utils::RuntimeOrHandle::new().block_on(provider.get_chainid()) { + return id.as_u64() + } + } + Chain::Mainnet as u64 + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Env { + /// the block gas limit + pub gas_limit: u64, + + /// the chainid opcode value + pub chain_id: Option, + + /// the tx.gasprice value during EVM execution + pub gas_price: u64, + + /// the base fee in a block + pub block_base_fee_per_gas: u64, + + /// the tx.origin value during EVM execution + pub tx_origin: Address, + + /// the block.coinbase value during EVM execution + pub block_coinbase: Address, + + /// the block.timestamp value during EVM execution + pub block_timestamp: u64, + + /// the block.number value during EVM execution" + pub block_number: u64, + + /// the block.difficulty value during EVM execution + pub block_difficulty: u64, + + /// the block.gaslimit value during EVM execution + pub block_gas_limit: Option, } diff --git a/evm/src/lib.rs b/evm/src/lib.rs index 7b837c52194ac..22a9cd53d04c8 100644 --- a/evm/src/lib.rs +++ b/evm/src/lib.rs @@ -14,9 +14,6 @@ pub use executor::abi; /// Fuzzing wrapper for executors pub mod fuzz; -/// Support for storing things and disk. -pub mod storage; - // Re-exports pub use ethers::types::Address; pub use hashbrown::HashMap; diff --git a/evm/test-data/storage.json b/evm/test-data/storage.json new file mode 100644 index 0000000000000..ba12e7de0e21c --- /dev/null +++ b/evm/test-data/storage.json @@ -0,0 +1,41 @@ +{ + "meta": { + "cfg_env": { + "chain_id": "0x1", + "spec_id": "LATEST", + "perf_all_precompiles_have_balance": false + }, + "block_env": { + "number": "0xdc42b8", + "coinbase": "0x0000000000000000000000000000000000000000", + "timestamp": "0x1", + "difficulty": "0x0", + "basefee": "0x0", + "gas_limit": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "host": "mainnet.infura.io" + }, + "accounts": { + "0x63091244180ae240c87d1f528f5f269134cb07b3": { + "balance": "0x0", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "code": null, + "nonce": 0 + } + }, + "storage": { + "0x63091244180ae240c87d1f528f5f269134cb07b3": { + "0x0": "0x0", + "0x1": "0x0", + "0x2": "0x0", + "0x3": "0x0", + "0x4": "0x0", + "0x5": "0x0", + "0x6": "0x0", + "0x7": "0x0", + "0x8": "0x0", + "0x9": "0x0" + } + }, + "block_hashes": {} +} \ No newline at end of file diff --git a/fmt/Cargo.toml b/fmt/Cargo.toml index e748c1c432ef2..ccef530618d88 100644 --- a/fmt/Cargo.toml +++ b/fmt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "forge-fmt" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "Foundry's solidity formatting and linting support" diff --git a/forge/Cargo.toml b/forge/Cargo.toml index 6df46babb8632..8a1111c12d8b2 100644 --- a/forge/Cargo.toml +++ b/forge/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "forge" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" @@ -19,7 +19,7 @@ glob = "0.3.0" # TODO: Trim down tokio = { version = "1.10.1" } tracing = "0.1.26" -tracing-subscriber = "0.2.20" +tracing-subscriber = "0.3" proptest = "1.0.0" rayon = "1.5" rlp = "0.5.1" diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index b0fb8628fe6c3..691a067f88c02 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -1,17 +1,9 @@ -use crate::{runner::TestResult, ContractRunner, TestFilter}; -use ethers::prelude::artifacts::CompactContractBytecode; -use evm_adapters::{ - evm_opts::{BackendKind, EvmOpts}, - sputnik::cheatcodes::{CONSOLE_ABI, HEVMCONSOLE_ABI, HEVM_ABI}, -}; -use foundry_utils::PostLinkInput; -use sputnik::{backend::Backend, Config}; - +use crate::{ContractRunner, TestFilter, TestResult}; use ethers::{ - abi::{Abi, Event, Function}, - prelude::ArtifactOutput, - solc::{Artifact, Project}, - types::{Address, H256, U256}, + abi::Abi, + prelude::{artifacts::CompactContractBytecode, ArtifactId, ArtifactOutput}, + solc::{Artifact, ProjectCompileOutput}, + types::{Address, Bytes, U256}, }; use eyre::Result; use foundry_evm::executor::{opts::EvmOpts, DatabaseRef, Executor, ExecutorBuilder, Fork, SpecId}; diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 0b2965f5332b4..669a7b924483a 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -177,11 +177,55 @@ impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { // We set the nonce of the deployer accounts to 1 to get the same addresses as DappTools self.executor.set_nonce(self.sender, 1); - // deploy an instance of the contract inside the runner in the EVM - let output = - executor.deploy(self.sender, self.code.clone(), 0u32.into()).expect("couldn't deploy"); - executor.set_balance(output.retdata, self.evm_opts.initial_balance); - Ok((output.retdata, executor, output.logs)) + // Deploy libraries + let mut traces: Vec<(TraceKind, CallTraceArena)> = self + .predeploy_libs + .iter() + .filter_map(|code| { + let DeployResult { traces, .. } = self + .executor + .deploy(self.sender, code.0.clone(), 0u32.into()) + .expect("couldn't deploy library"); + + traces + }) + .map(|traces| (TraceKind::Deployment, traces)) + .collect(); + + // Deploy an instance of the contract + let DeployResult { address, mut logs, traces: constructor_traces, .. } = self + .executor + .deploy(self.sender, self.code.0.clone(), 0u32.into()) + .expect("couldn't deploy"); + traces.extend(constructor_traces.map(|traces| (TraceKind::Deployment, traces)).into_iter()); + self.executor.set_balance(address, self.initial_balance); + + // Optionally call the `setUp` function + Ok(if setup { + tracing::trace!("setting up"); + let (setup_failed, setup_logs, setup_traces, labeled_addresses, reason) = match self + .executor + .setup(address) + { + Ok(CallResult { traces, labels, logs, .. }) => (false, logs, traces, labels, None), + Err(EvmError::Execution { traces, labels, logs, reason, .. }) => { + (true, logs, traces, labels, Some(format!("Setup failed: {}", reason))) + } + Err(e) => ( + true, + Vec::new(), + None, + BTreeMap::new(), + Some(format!("Setup failed: {}", &e.to_string())), + ), + }; + traces.extend(setup_traces.map(|traces| (TraceKind::Setup, traces)).into_iter()); + logs.extend_from_slice(&setup_logs); + + TestSetup { address, logs, traces, labeled_addresses, setup_failed, reason } + } else { + TestSetup { address, logs, traces, ..Default::default() } + }) } /// Runs all tests for a contract whose names match the provided regular expression @@ -259,76 +303,42 @@ impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { // Run unit test let start = Instant::now(); - // the expected result depends on the function name - // DAppTools' ds-test will not revert inside its `assertEq`-like functions - // which allows to test multiple assertions in 1 test function while also - // preserving logs. - let should_fail = func.name.starts_with("testFail"); - tracing::debug!(func = ?func.signature(), should_fail, "unit-testing"); - - let (address, mut evm, init_logs) = self.new_sputnik_evm()?; - - let errors_abi = self.execution_info.as_ref().map(|(_, _, errors)| errors); - let errors_abi = if let Some(ref abi) = errors_abi { abi } else { self.contract }; - - let mut logs = init_logs; - - let mut traces: Option> = None; - let mut identified_contracts: Option> = None; - - // clear out the deployment trace - evm.reset_traces(); - - // call the setup function in each test to reset the test's state. - if setup { - tracing::trace!("setting up"); - let setup_logs = match evm.setup(address) { - Ok((_reason, setup_logs)) => setup_logs, - Err(e) => { - // if tracing is enabled, just return it as a failed test - // otherwise abort - if evm.tracing_enabled() { - self.update_traces( - &mut traces, - &mut identified_contracts, - known_contracts, - setup, - &mut evm, - ); - } - - return Ok(TestResult { - success: false, - reason: Some("Setup failed: ".to_string() + &e.to_string()), - gas_used: 0, - counterexample: None, - logs, - kind: TestKind::Standard(0), - traces, - identified_contracts, - debug_calls: if evm.state().debug_enabled { - Some(evm.debug_calls()) - } else { - None - }, - labeled_addresses: evm.state().labels.clone(), - }) - } - }; - logs.extend_from_slice(&setup_logs); - } - - let (status, reason, gas_used, logs) = match evm.call::<(), _, _>( - self.sender, - address, - func.clone(), - (), - 0.into(), - Some(errors_abi), - ) { - Ok(output) => { - logs.extend(output.logs.clone()); - (output.status, None, output.gas, output.logs) + let (reverted, reason, gas, stipend, execution_traces, state_changeset) = match self + .executor + .call::<(), _, _>(self.sender, address, func.clone(), (), 0.into(), self.errors) + { + Ok(CallResult { + reverted, + gas, + stipend, + logs: execution_logs, + traces: execution_trace, + labels: new_labels, + state_changeset, + .. + }) => { + labeled_addresses.extend(new_labels); + logs.extend(execution_logs); + (reverted, None, gas, stipend, execution_trace, state_changeset) + } + Err(EvmError::Execution { + reverted, + reason, + gas, + stipend, + logs: execution_logs, + traces: execution_trace, + labels: new_labels, + state_changeset, + .. + }) => { + labeled_addresses.extend(new_labels); + logs.extend(execution_logs); + (reverted, Some(reason), gas, stipend, execution_trace, state_changeset) + } + Err(err) => { + tracing::error!(?err); + return Err(err.into()) } }; traces.extend(execution_traces.map(|traces| (TraceKind::Execution, traces)).into_iter()); @@ -388,103 +398,6 @@ impl<'a, DB: DatabaseRef + Send + Sync> ContractRunner<'a, DB> { success = %result.success ); - // clear out the deployment trace - evm.reset_traces(); - - // call the setup function in each test to reset the test's state. - if setup { - tracing::trace!("setting up"); - match evm.setup(address) { - Ok((_reason, _setup_logs)) => {} - Err(e) => { - // if tracing is enabled, just return it as a failed test - // otherwise abort - if evm.tracing_enabled() { - self.update_traces( - &mut traces, - &mut identified_contracts, - known_contracts, - setup, - &mut evm, - ); - } - return Ok(TestResult { - success: false, - reason: Some("Setup failed: ".to_string() + &e.to_string()), - gas_used: 0, - counterexample: None, - logs: vec![], - kind: TestKind::Fuzz(FuzzedCases::new(vec![])), - traces, - identified_contracts, - debug_calls: if evm.state().debug_enabled { - Some(evm.debug_calls()) - } else { - None - }, - labeled_addresses: evm.state().labels.clone(), - }) - } - } - } - - let mut logs = init_logs; - - let prev = evm.set_tracing_enabled(false); - - // instantiate the fuzzed evm in line - let evm = FuzzedExecutor::new(&mut evm, runner, self.sender); - let FuzzTestResult { cases, test_error } = - evm.fuzz(func, address, should_fail, Some(self.contract)); - - let evm = evm.into_inner(); - if let Some(ref error) = test_error { - // we want traces for a failed fuzz - if let TestError::Fail(_reason, bytes) = &error.test_error { - if prev { - let _ = evm.set_tracing_enabled(true); - } - let output = evm.call_raw(self.sender, address, bytes.clone(), 0.into(), false)?; - if is_fail(evm, output.status) { - logs.extend(output.logs); - // add reverted logs - logs.extend(evm.all_logs()); - } else { - logs.extend(output.logs); - } - self.update_traces( - &mut traces, - &mut identified_contracts, - known_contracts, - setup, - evm, - ); - } - } - - let success = test_error.is_none(); - let mut counterexample = None; - let mut reason = None; - if let Some(err) = test_error { - match err.test_error { - TestError::Fail(_, value) => { - // skip the function selector when decoding - let args = func.decode_input(&value.as_ref()[4..])?; - let counter = CounterExample { calldata: value.clone(), args }; - counterexample = Some(counter); - tracing::info!("Found minimal failing case: {}", hex::encode(&value)); - } - result => panic!("Unexpected test result: {:?}", result), - } - if !err.revert_reason.is_empty() { - reason = Some(err.revert_reason); - } - } - - let duration = Instant::now().duration_since(start); - tracing::debug!(?duration, %success); - - // from that call? Ok(TestResult { success: result.success, reason: result.reason, diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 6d9274c99d9ca..54c2f7feafea2 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ui" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 3bd0c8ea14c44..562df0f9122d5 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "foundry-utils" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT OR Apache-2.0" @@ -31,4 +31,4 @@ ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = fals [features] -test = ["tracing-subscriber"] \ No newline at end of file +test = ["tracing-subscriber"]