From 764c69cc758f15183ce104ccd6c18d9374b03162 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 13 Jan 2022 23:42:59 +0200 Subject: [PATCH] Parallel EVM Tests (#444) * feat(fuzz): expose function to get internal evm * refactor(evm): move EvmOpts from cli to evm-adapters * feat(evm): add helper for creating sputnik backend * feat(forge): add base evm opts for test usage * feat(evm): derive default for EvmOpts * test(forge): add utils for instantiating backend * feat(forge): instantiate runner with EvmOpts instead of an EVM This allows us to instantiate as many EVMs as we want inside of the runner, which in turn will enable running tests in parallel * feat(forge): pass evm by reference instead of using self.evm * feat(forge): run unit tests with unique evm instantiation previously we'd reuse the same EVM, now, we use a different EVM per test, allowing us to get rid of the mutable reference on self * feat(forge): run fuzz tests with unique evm instantiations * test(forge): adjust tests to new instantiation style * feat(forge): run tests in parallel with rayon * feat(evm-adapters): put backend behind enum to avoid trait object * chore(forge): move fuzzer instead of ref * feat(forge): make multi contract runner compatible with new runner * feat(forge): parallelize multi contract runner by file * chore(cli): remove unused helper functions * fix(cli/run): use new contract runner initialization There's a TODO here around how we should do the evm.debug_calls check which we should figure out * fix(cli/test): use evm_opts instead of directly passing evm * chore: formatting fixes * chore: update lockfile * fix(evm-adapters): correctly init test caller and origin fixes https://github.com/gakonst/foundry/issues/249 fixes https://github.com/gakonst/foundry/issues/253 * chore: clippy lint on unreachable code w disabled features * fix: instantiate evm cfg without contract size limit * fix debugging (#445) * merge cleanup Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com> Co-authored-by: Brock --- Cargo.lock | 3 + cli/Cargo.toml | 2 +- cli/src/cmd/build.rs | 120 +----------- cli/src/cmd/run.rs | 68 ++++--- cli/src/cmd/test.rs | 51 ++--- cli/src/opts/forge.rs | 82 +------- cli/src/utils.rs | 65 +------ evm-adapters/Cargo.toml | 11 +- evm-adapters/src/evm_opts.rs | 254 ++++++++++++++++++++++++ evm-adapters/src/fuzz.rs | 4 + evm-adapters/src/lib.rs | 3 + evm-adapters/src/sputnik/evm.rs | 8 +- forge/Cargo.toml | 11 +- forge/src/lib.rs | 22 +++ forge/src/multi_runner.rs | 128 ++++++------ forge/src/runner.rs | 333 +++++++++++++++++--------------- 16 files changed, 609 insertions(+), 556 deletions(-) create mode 100644 evm-adapters/src/evm_opts.rs diff --git a/Cargo.lock b/Cargo.lock index fae1ec6df31e..1edadb78665e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,8 @@ version = "0.1.0" dependencies = [ "ansi_term", "bytes", + "clap", + "clap_complete", "ethers", "evm", "evmodin", @@ -1702,6 +1704,7 @@ dependencies = [ "glob", "hex", "proptest", + "rayon", "regex", "semver", "serde", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 28dc059fd748..e7a41941ee1d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -76,4 +76,4 @@ doc = false [[bin]] name = "forge" path = "src/forge.rs" -doc = false \ No newline at end of file +doc = false diff --git a/cli/src/cmd/build.rs b/cli/src/cmd/build.rs index 1e2ae25c3c56..fd9edfdce65c 100644 --- a/cli/src/cmd/build.rs +++ b/cli/src/cmd/build.rs @@ -1,12 +1,9 @@ //! build command -use ethers::{ - solc::{ - artifacts::{Optimizer, Settings}, - remappings::Remapping, - MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, - }, - types::Address, +use ethers::solc::{ + artifacts::{Optimizer, Settings}, + remappings::Remapping, + MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, SolcConfig, }; use std::{ collections::BTreeMap, @@ -17,10 +14,6 @@ use std::{ use crate::{cmd::Cmd, opts::forge::CompilerArgs, utils}; use clap::{Parser, ValueHint}; -#[cfg(feature = "evmodin-evm")] -use evmodin::util::mocked_host::MockedHost; -#[cfg(feature = "sputnik-evm")] -use sputnik::backend::MemoryVicinity; #[derive(Debug, Clone, Parser)] pub struct BuildArgs { @@ -250,108 +243,3 @@ impl BuildArgs { Ok(project) } } - -#[derive(Clone, Debug)] -pub enum EvmType { - #[cfg(feature = "sputnik-evm")] - Sputnik, - #[cfg(feature = "evmodin-evm")] - EvmOdin, -} - -impl FromStr for EvmType { - type Err = eyre::Error; - - fn from_str(s: &str) -> Result { - Ok(match s.to_lowercase().as_str() { - #[cfg(feature = "sputnik-evm")] - "sputnik" => EvmType::Sputnik, - #[cfg(feature = "evmodin-evm")] - "evmodin" => EvmType::EvmOdin, - other => eyre::bail!("unknown EVM type {}", other), - }) - } -} - -#[derive(Debug, Clone, Parser)] -pub struct Env { - #[clap(help = "the block gas limit", long, default_value_t = u64::MAX)] - pub gas_limit: u64, - - #[clap(help = "the chainid opcode value", long, default_value = "1")] - pub chain_id: u64, - - #[clap(help = "the tx.gasprice value during EVM execution", long, default_value = "0")] - pub gas_price: u64, - - #[clap(help = "the base fee in a block", long, default_value = "0")] - pub block_base_fee_per_gas: u64, - - #[clap( - help = "the tx.origin value during EVM execution", - long, - default_value = "0x0000000000000000000000000000000000000000" - )] - pub tx_origin: Address, - - #[clap( - help = "the block.coinbase value during EVM execution", - long, - default_value = "0x0000000000000000000000000000000000000000" - )] - pub block_coinbase: Address, - #[clap( - help = "the block.timestamp value during EVM execution", - long, - default_value = "0", - env = "DAPP_TEST_TIMESTAMP" - )] - pub block_timestamp: u64, - - #[clap(help = "the block.number value during EVM execution", long, default_value = "0")] - #[clap(env = "DAPP_TEST_NUMBER")] - pub block_number: u64, - - #[clap(help = "the block.difficulty value during EVM execution", long, default_value = "0")] - pub block_difficulty: u64, - - #[clap(help = "the block.gaslimit value during EVM execution", long)] - pub block_gas_limit: Option, - // TODO: Add configuration option for base fee. -} - -impl Env { - #[cfg(feature = "sputnik-evm")] - pub fn sputnik_state(&self) -> MemoryVicinity { - MemoryVicinity { - chain_id: self.chain_id.into(), - - gas_price: self.gas_price.into(), - origin: self.tx_origin, - - block_coinbase: self.block_coinbase, - block_number: self.block_number.into(), - block_timestamp: self.block_timestamp.into(), - block_difficulty: self.block_difficulty.into(), - block_base_fee_per_gas: self.block_base_fee_per_gas.into(), - block_gas_limit: self.block_gas_limit.unwrap_or(self.gas_limit).into(), - block_hashes: Vec::new(), - } - } - - #[cfg(feature = "evmodin-evm")] - pub fn evmodin_state(&self) -> MockedHost { - let mut host = MockedHost::default(); - - host.tx_context.chain_id = self.chain_id.into(); - host.tx_context.tx_gas_price = self.gas_price.into(); - host.tx_context.tx_origin = self.tx_origin; - host.tx_context.block_coinbase = self.block_coinbase; - host.tx_context.block_number = self.block_number; - host.tx_context.block_timestamp = self.block_timestamp; - host.tx_context.block_difficulty = self.block_difficulty.into(); - host.tx_context.block_gas_limit = self.block_gas_limit.unwrap_or(self.gas_limit); - - host - } -} diff --git a/cli/src/cmd/run.rs b/cli/src/cmd/run.rs index e33040fd1057..8242a2667a40 100644 --- a/cli/src/cmd/run.rs +++ b/cli/src/cmd/run.rs @@ -1,7 +1,4 @@ -use crate::{ - cmd::{build::BuildArgs, compile, manual_compile, Cmd}, - opts::forge::EvmOpts, -}; +use crate::cmd::{build::BuildArgs, compile, manual_compile, Cmd}; use clap::{Parser, ValueHint}; use ethers::abi::Abi; use forge::ContractRunner; @@ -14,13 +11,15 @@ use ethers::solc::{ MinimalCombinedArtifacts, Project, ProjectPathsConfig, SolcConfig, }; -use evm_adapters::Evm; - use ansi_term::Colour; use ethers::{ prelude::{artifacts::ContractBytecode, Artifact}, solc::artifacts::{CompactContractSome, ContractBytecodeSome}, }; +use evm_adapters::{ + evm_opts::{BackendKind, EvmOpts}, + sputnik::{cheatcodes::debugger::DebugArena, helpers::vm}, +}; #[derive(Debug, Clone, Parser)] pub struct RunArgs { @@ -79,25 +78,40 @@ impl Cmd for RunArgs { let CompactContractSome { abi, bin, .. } = contract; // this should never fail if compilation was successful let bytecode = bin.into_bytes().unwrap(); - - // 2. instantiate the EVM w forked backend if needed / pre-funded account(s) - let mut cfg = crate::utils::sputnik_cfg(self.opts.compiler.evm_version); - let vicinity = self.evm_opts.vicinity()?; - let mut evm = crate::utils::sputnik_helpers::evm(&evm_opts, &mut cfg, &vicinity)?; - - // 3. deploy the contract - let (addr, _, _, logs) = evm.deploy(self.evm_opts.sender, bytecode, 0u32.into())?; - - // 4. set up the runner - let mut runner = - ContractRunner::new(&mut evm, &abi, addr, Some(self.evm_opts.sender), &logs); - - // 5. run the test function & potentially the setup let needs_setup = abi.functions().any(|func| func.name == "setUp"); - let result = runner.run_test(&func, needs_setup, Some(&known_contracts))?; - if self.evm_opts.debug { - // 6. Boot up debugger + let cfg = crate::utils::sputnik_cfg(&self.opts.compiler.evm_version); + let vicinity = evm_opts.vicinity()?; + let backend = evm_opts.backend(&vicinity)?; + + // need to match on the backend type + let result = match backend { + BackendKind::Simple(ref backend) => { + let runner = ContractRunner::new( + &evm_opts, + &cfg, + backend, + &abi, + bytecode, + Some(evm_opts.sender), + ); + runner.run_test(&func, needs_setup, Some(&known_contracts))? + } + BackendKind::Shared(ref backend) => { + let runner = ContractRunner::new( + &evm_opts, + &cfg, + backend, + &abi, + bytecode, + Some(evm_opts.sender), + ); + runner.run_test(&func, needs_setup, Some(&known_contracts))? + } + }; + + if evm_opts.debug { + // 4. Boot up debugger let source_code: BTreeMap = sources .iter() .map(|(id, path)| { @@ -123,7 +137,7 @@ impl Cmd for RunArgs { }) .collect(); - let calls = evm.debug_calls(); + let calls: Vec = result.debug_calls.expect("Debug must be enabled by now"); println!("debugging"); let index = if needs_setup && calls.len() > 1 { 1 } else { 0 }; let mut flattened = Vec::new(); @@ -149,14 +163,14 @@ impl Cmd for RunArgs { if evm_opts.verbosity > 4 || !result.success { // print setup calls as well traces.iter().for_each(|trace| { - trace.pretty_print(0, &known_contracts, &mut ident, runner.evm, ""); + trace.pretty_print(0, &known_contracts, &mut ident, &vm(), ""); }); } else if !traces.is_empty() { traces.last().expect("no last but not empty").pretty_print( 0, &known_contracts, &mut ident, - runner.evm, + &vm(), "", ); } @@ -165,7 +179,7 @@ impl Cmd for RunArgs { println!(); } } else { - // 6. print the result nicely + // 5. print the result nicely if result.success { println!("{}", Colour::Green.paint("Script ran successfully.")); } else { diff --git a/cli/src/cmd/test.rs b/cli/src/cmd/test.rs index e58669300b3f..0fd8f4960f4d 100644 --- a/cli/src/cmd/test.rs +++ b/cli/src/cmd/test.rs @@ -1,16 +1,10 @@ //! Test command -use crate::{ - cmd::{ - build::{BuildArgs, EvmType}, - Cmd, - }, - opts::forge::EvmOpts, - utils, -}; +use crate::cmd::{build::BuildArgs, Cmd}; use ansi_term::Colour; use clap::{AppSettings, Parser}; use ethers::solc::{ArtifactOutput, Project}; +use evm_adapters::{evm_opts::EvmOpts, sputnik::helpers::vm}; use forge::{MultiContractRunnerBuilder, TestFilter}; use std::collections::BTreeMap; @@ -118,35 +112,16 @@ impl Cmd for TestArgs { let project = opts.project()?; // prepare the test builder + let mut evm_cfg = crate::utils::sputnik_cfg(&opts.compiler.evm_version); + evm_cfg.create_contract_limit = None; + let builder = MultiContractRunnerBuilder::default() .fuzzer(fuzzer) .initial_balance(evm_opts.initial_balance) + .evm_cfg(evm_cfg) .sender(evm_opts.sender); - // run the tests depending on the chosen EVM - match evm_opts.evm_type { - #[cfg(feature = "sputnik-evm")] - EvmType::Sputnik => { - let mut cfg = utils::sputnik_cfg(opts.compiler.evm_version); - let vicinity = evm_opts.vicinity()?; - let evm = utils::sputnik_helpers::evm(&evm_opts, &mut cfg, &vicinity)?; - test(builder, project, evm, filter, json, evm_opts.verbosity, allow_failure) - } - #[cfg(feature = "evmodin-evm")] - EvmType::EvmOdin => { - use evm_adapters::evmodin::EvmOdin; - use evmodin::tracing::NoopTracer; - - let revision = utils::evmodin_cfg(opts.compiler.evm_version); - - // TODO: Replace this with a proper host. We'll want this to also be - // provided generically when we add the Forking host(s). - let host = evm_opts.env.evmodin_state(); - - let evm = EvmOdin::new(host, evm_opts.env.gas_limit, revision, NoopTracer); - test(builder, project, evm, filter, json, evm_opts.verbosity, allow_failure) - } - } + test(builder, project, evm_opts, filter, json, allow_failure) } } @@ -221,16 +196,16 @@ impl TestOutcome { } /// Runs all the tests -fn test>( +fn test( builder: MultiContractRunnerBuilder, project: Project, - evm: E, + evm_opts: EvmOpts, filter: Filter, json: bool, - verbosity: u8, allow_failure: bool, ) -> eyre::Result { - let mut runner = builder.build(project, evm)?; + let verbosity = evm_opts.verbosity; + let mut runner = builder.build(project, evm_opts)?; let results = runner.test(&filter)?; @@ -301,7 +276,7 @@ fn test>( 0, &runner.known_contracts, &mut ident, - &runner.evm, + &vm(), "", ); }); @@ -310,7 +285,7 @@ fn test>( 0, &runner.known_contracts, &mut ident, - &runner.evm, + &vm(), "", ); } diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index e8977868c72b..19e0d720bd88 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -3,7 +3,9 @@ use clap::{Parser, Subcommand, ValueHint}; use ethers::{solc::EvmVersion, types::Address}; use std::{path::PathBuf, str::FromStr}; -use crate::cmd::{build::BuildArgs, create::CreateArgs, run::RunArgs, snapshot, test}; +use crate::cmd::{ + build::BuildArgs, create::CreateArgs, remappings::RemappingArgs, run::RunArgs, snapshot, test, +}; #[derive(Debug, Parser)] pub struct Opts { @@ -110,84 +112,6 @@ pub struct CompilerArgs { pub optimize_runs: u32, } -use crate::cmd::{ - build::{Env, EvmType}, - remappings::RemappingArgs, -}; -use ethers::types::U256; - -#[derive(Debug, Clone, Parser)] -pub struct EvmOpts { - #[clap(flatten)] - pub env: Env, - - #[clap( - long, - short, - help = "the EVM type you want to use (e.g. sputnik, evmodin)", - default_value = "sputnik" - )] - pub evm_type: EvmType, - - #[clap(help = "fetch state over a remote instead of starting from empty state", long, short)] - #[clap(alias = "rpc-url")] - pub fork_url: Option, - - #[clap(help = "pins the block number for the state fork", long)] - #[clap(env = "DAPP_FORK_BLOCK")] - pub fork_block_number: Option, - - #[clap( - help = "the initial balance of each deployed test contract", - long, - default_value = "0xffffffffffffffffffffffff" - )] - pub initial_balance: U256, - - #[clap( - help = "the address which will be executing all tests", - long, - default_value = "0x0000000000000000000000000000000000000000", - env = "DAPP_TEST_ADDRESS" - )] - pub sender: Address, - - #[clap(help = "enables the FFI cheatcode", long)] - pub ffi: bool, - - #[clap( - help = r#"Verbosity mode of EVM output as number of occurrences of the `v` flag (-v, -vv, -vvv, etc.) - 3: print test trace for failing tests - 4: always print test trace, print setup for failing tests - 5: always print test trace and setup -"#, - long, - short, - parse(from_occurrences) - )] - pub verbosity: u8, - - #[clap(help = "enable debugger", long)] - pub debug: bool, -} - -impl EvmOpts { - #[cfg(feature = "sputnik-evm")] - pub fn vicinity(&self) -> eyre::Result { - Ok(if let Some(ref url) = self.fork_url { - let provider = ethers::providers::Provider::try_from(url.as_str())?; - let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt"); - rt.block_on(evm_adapters::sputnik::vicinity( - &provider, - self.fork_block_number, - Some(self.env.tx_origin), - ))? - } else { - self.env.sputnik_state() - }) - } -} - /// Represents the common dapp argument pattern for `:` where `:` is /// optional. #[derive(Clone, Debug)] diff --git a/cli/src/utils.rs b/cli/src/utils.rs index 089b1781dc22..2a19f676021a 100644 --- a/cli/src/utils.rs +++ b/cli/src/utils.rs @@ -1,9 +1,5 @@ -use ethers::{ - providers::Provider, - solc::{artifacts::Contract, EvmVersion}, -}; +use ethers::solc::{artifacts::Contract, EvmVersion}; -use evm_adapters::sputnik::Executor; use eyre::{ContextCompat, WrapErr}; use std::{env::VarError, path::PathBuf, process::Command}; @@ -12,8 +8,6 @@ use evmodin::Revision; #[cfg(feature = "sputnik-evm")] use sputnik::Config; -use crate::opts::forge::EvmOpts; - /// Default local RPC endpoint const LOCAL_RPC_URL: &str = "http://127.0.0.1:8545"; @@ -89,7 +83,7 @@ pub fn find_git_root_path() -> eyre::Result { } #[cfg(feature = "sputnik-evm")] -pub fn sputnik_cfg(evm: EvmVersion) -> Config { +pub fn sputnik_cfg(evm: &EvmVersion) -> Config { match evm { EvmVersion::Istanbul => Config::istanbul(), EvmVersion::Berlin => Config::berlin(), @@ -98,61 +92,8 @@ pub fn sputnik_cfg(evm: EvmVersion) -> Config { } } -#[cfg(feature = "sputnik-evm")] -pub mod sputnik_helpers { - use super::*; - use ethers::types::U256; - use sputnik::{ - backend::{Backend, MemoryBackend, MemoryVicinity}, - Config, - }; - use std::sync::Arc; - - use evm_adapters::{ - sputnik::{helpers::TestSputnikVM, ForkMemoryBackend, PRECOMPILES_MAP}, - FAUCET_ACCOUNT, - }; - - /// Creates a new Sputnik EVM given the [`EvmOpts`] (specifying whether to fork or not), a VM - /// Hard Fork config, and the initial state from the memory vicinity. - pub fn evm<'a>( - opts: &EvmOpts, - cfg: &'a mut Config, - vicinity: &'a MemoryVicinity, - ) -> eyre::Result>>> { - // We disable the contract size limit by default, because Solidity - // test smart contracts are likely to be >24kb - cfg.create_contract_limit = None; - - let mut backend = MemoryBackend::new(vicinity, Default::default()); - // max out the balance of the faucet - let faucet = backend.state_mut().entry(*FAUCET_ACCOUNT).or_insert_with(Default::default); - faucet.balance = U256::MAX; - - let backend: Box = if let Some(ref url) = opts.fork_url { - let provider = Provider::try_from(url.as_str())?; - let init_state = backend.state().clone(); - let backend = - ForkMemoryBackend::new(provider, backend, opts.fork_block_number, init_state); - Box::new(backend) - } else { - Box::new(backend) - }; - let backend = Arc::new(backend); - - Ok(Executor::new_with_cheatcodes( - backend, - opts.env.gas_limit, - cfg, - &*PRECOMPILES_MAP, - opts.ffi, - opts.verbosity > 2, - opts.debug, - )) - } -} - #[cfg(feature = "evmodin-evm")] +#[allow(dead_code)] pub fn evmodin_cfg(evm: EvmVersion) -> Revision { match evm { EvmVersion::Istanbul => Revision::Istanbul, diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index 66b64de90820..35ad56b275b1 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -11,7 +11,7 @@ foundry-utils = { path = "./../utils" } sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm", optional = true, features = ["tracing"] } -evmodin = { git = "https://github.com/vorot93/evmodin", optional = true } +evmodin = { git = "https://github.com/vorot93/evmodin", optional = true, features = ["util"] } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } eyre = "0.6.5" @@ -29,6 +29,15 @@ serde_json = "1.0.72" serde = "1.0.130" ansi_term = "0.12.1" +# for evm config to be usable in clis +clap = { version = "3.0.6", features = [ + "derive", + "env", + "unicode", + "wrap_help", +] } +clap_complete = "3.0.2" + [dev-dependencies] evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/evm-adapters/src/evm_opts.rs b/evm-adapters/src/evm_opts.rs new file mode 100644 index 000000000000..061580ed2d17 --- /dev/null +++ b/evm-adapters/src/evm_opts.rs @@ -0,0 +1,254 @@ +use clap::Parser; +use ethers::types::{Address, U256}; +use std::str::FromStr; + +#[cfg(feature = "evmodin")] +use evmodin::util::mocked_host::MockedHost; +#[cfg(feature = "sputnik")] +use sputnik::backend::MemoryVicinity; + +#[derive(Clone, Debug)] +pub enum EvmType { + #[cfg(feature = "sputnik")] + Sputnik, + #[cfg(feature = "evmodin")] + EvmOdin, +} + +#[cfg(any(feature = "sputnik", feature = "evmodin"))] +impl Default for EvmType { + fn default() -> Self { + // if sputnik is enabled, default to it + #[cfg(feature = "sputnik")] + #[rustfmt::skip] + return EvmType::Sputnik; + // if not, fall back to evmodin + #[allow(unreachable_code)] + #[cfg(feature = "evmodin")] + EvmType::EvmOdin + } +} + +impl FromStr for EvmType { + type Err = eyre::Error; + + fn from_str(s: &str) -> Result { + // silence this warning which indicates that if no evm features are + // enabled, the Ok(...) will never be reached. + #[allow(unreachable_code)] + Ok(match s.to_lowercase().as_str() { + #[cfg(feature = "sputnik")] + "sputnik" => EvmType::Sputnik, + #[cfg(feature = "evmodin")] + "evmodin" => EvmType::EvmOdin, + other => eyre::bail!("unknown EVM type {}", other), + }) + } +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(any(feature = "sputnik", feature = "evmodin"), derive(Default))] +pub struct EvmOpts { + #[clap(flatten)] + pub env: Env, + + #[clap( + long, + short, + help = "the EVM type you want to use (e.g. sputnik, evmodin)", + default_value = "sputnik" + )] + pub evm_type: EvmType, + + #[clap(help = "fetch state over a remote instead of starting from empty state", long, short)] + #[clap(alias = "rpc-url")] + pub fork_url: Option, + + #[clap(help = "pins the block number for the state fork", long)] + #[clap(env = "DAPP_FORK_BLOCK")] + pub fork_block_number: Option, + + #[clap( + help = "the initial balance of each deployed test contract", + long, + default_value = "0xffffffffffffffffffffffff" + )] + pub initial_balance: U256, + + #[clap( + help = "the address which will be executing all tests", + long, + default_value = "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", + env = "DAPP_TEST_CALLER" + )] + pub sender: Address, + + #[clap(help = "enables the FFI cheatcode", long)] + pub ffi: bool, + + #[clap( + help = r#"Verbosity mode of EVM output as number of occurences of the `v` flag (-v, -vv, -vvv, etc.) + 3: print test trace for failing tests + 4: always print test trace, print setup for failing tests + 5: always print test trace and setup +"#, + long, + short, + parse(from_occurrences) + )] + pub verbosity: u8, + + #[clap(help = "enable debugger", long)] + pub debug: bool, +} + +#[cfg(feature = "sputnik")] +pub use sputnik_helpers::BackendKind; + +// Helper functions for sputnik +#[cfg(feature = "sputnik")] +mod sputnik_helpers { + use super::*; + + use crate::{sputnik::cache::SharedBackend, FAUCET_ACCOUNT}; + use ::sputnik::backend::MemoryBackend; + use ethers::providers::Provider; + + pub enum BackendKind<'a> { + Simple(MemoryBackend<'a>), + Shared(SharedBackend), + } + + impl EvmOpts { + #[cfg(feature = "sputnik")] + pub fn backend<'a>( + &'a self, + vicinity: &'a MemoryVicinity, + ) -> eyre::Result> { + let mut backend = MemoryBackend::new(vicinity, Default::default()); + // max out the balance of the faucet + let faucet = + backend.state_mut().entry(*FAUCET_ACCOUNT).or_insert_with(Default::default); + faucet.balance = U256::MAX; + + let backend = if let Some(ref url) = self.fork_url { + let provider = Provider::try_from(url.as_str())?; + let init_state = backend.state().clone(); + let cache = crate::sputnik::new_shared_cache(init_state); + let backend = SharedBackend::new( + provider, + cache, + vicinity.clone(), + self.fork_block_number.map(Into::into), + ); + BackendKind::Shared(backend) + } else { + BackendKind::Simple(backend) + }; + + Ok(backend) + } + + #[cfg(feature = "sputnik")] + pub fn vicinity(&self) -> eyre::Result { + Ok(if let Some(ref url) = self.fork_url { + let provider = ethers::providers::Provider::try_from(url.as_str())?; + let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt"); + rt.block_on(crate::sputnik::vicinity( + &provider, + self.fork_block_number, + Some(self.env.tx_origin), + ))? + } else { + self.env.sputnik_state() + }) + } + } +} + +#[derive(Debug, Clone, Default, Parser)] +pub struct Env { + // structopt does not let use `u64::MAX`: + // https://doc.rust-lang.org/std/primitive.u64.html#associatedconstant.MAX + #[clap(help = "the block gas limit", long, default_value = "18446744073709551615")] + pub gas_limit: u64, + + #[clap(help = "the chainid opcode value", long, default_value = "1")] + pub chain_id: u64, + + #[clap(help = "the tx.gasprice value during EVM execution", long, default_value = "0")] + pub gas_price: u64, + + #[clap(help = "the base fee in a block", long, default_value = "0")] + pub block_base_fee_per_gas: u64, + + #[clap( + help = "the tx.origin value during EVM execution", + long, + default_value = "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", + env = "DAPP_TEST_ORIGIN" + )] + pub tx_origin: Address, + + #[clap( + help = "the block.coinbase value during EVM execution", + long, + // TODO: It'd be nice if we could use Address::zero() here. + default_value = "0x0000000000000000000000000000000000000000" + )] + pub block_coinbase: Address, + #[clap( + help = "the block.timestamp value during EVM execution", + long, + default_value = "0", + env = "DAPP_TEST_TIMESTAMP" + )] + pub block_timestamp: u64, + + #[clap(help = "the block.number value during EVM execution", long, default_value = "0")] + #[clap(env = "DAPP_TEST_NUMBER")] + pub block_number: u64, + + #[clap(help = "the block.difficulty value during EVM execution", long, default_value = "0")] + pub block_difficulty: u64, + + #[clap(help = "the block.gaslimit value during EVM execution", long)] + pub block_gas_limit: Option, + // TODO: Add configuration option for base fee. +} + +impl Env { + #[cfg(feature = "sputnik")] + pub fn sputnik_state(&self) -> MemoryVicinity { + MemoryVicinity { + chain_id: self.chain_id.into(), + + gas_price: self.gas_price.into(), + origin: self.tx_origin, + + block_coinbase: self.block_coinbase, + block_number: self.block_number.into(), + block_timestamp: self.block_timestamp.into(), + block_difficulty: self.block_difficulty.into(), + block_base_fee_per_gas: self.block_base_fee_per_gas.into(), + block_gas_limit: self.block_gas_limit.unwrap_or(self.gas_limit).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.into(); + host.tx_context.tx_gas_price = self.gas_price.into(); + host.tx_context.tx_origin = self.tx_origin; + host.tx_context.block_coinbase = self.block_coinbase; + host.tx_context.block_number = self.block_number; + host.tx_context.block_timestamp = self.block_timestamp; + host.tx_context.block_difficulty = self.block_difficulty.into(); + host.tx_context.block_gas_limit = self.block_gas_limit.unwrap_or(self.gas_limit); + + host + } +} diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 8a23fe477e35..ebb40a45bde1 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -30,6 +30,10 @@ pub struct FuzzedExecutor<'a, E, S> { } impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { + pub fn into_inner(self) -> &'a mut E { + self.evm.into_inner() + } + /// Returns a mutable reference to the fuzzer's internal EVM instance pub fn as_mut(&self) -> RefMut<'_, &'a mut E> { self.evm.borrow_mut() diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index cbbdae27df82..ba288618a647 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -18,6 +18,9 @@ pub mod fuzz; pub mod call_tracing; +/// Helpers for easily constructing EVM objects. +pub mod evm_opts; + use ethers::{ abi::{Detokenize, Tokenize}, contract::{decode_function_data, encode_function_data}, diff --git a/evm-adapters/src/sputnik/evm.rs b/evm-adapters/src/sputnik/evm.rs index 5eeb08634f50..ff606ef46edc 100644 --- a/evm-adapters/src/sputnik/evm.rs +++ b/evm-adapters/src/sputnik/evm.rs @@ -218,18 +218,18 @@ pub mod helpers { CheatcodeStackExecutor<'a, 'a, B, BTreeMap>, >; - static CFG: Lazy = Lazy::new(Config::london); + pub static CFG: Lazy = Lazy::new(Config::london); /// London config without a contract size limit. Useful for testing but is a depature from /// mainnet rules. - static CFG_NO_LMT: Lazy = Lazy::new(|| { + pub static CFG_NO_LMT: Lazy = Lazy::new(|| { let mut cfg = Config::london(); cfg.create_contract_limit = None; cfg }); - static VICINITY: Lazy = Lazy::new(new_vicinity); - const GAS_LIMIT: u64 = 30_000_000; + pub static VICINITY: Lazy = Lazy::new(new_vicinity); + pub const GAS_LIMIT: u64 = 30_000_000; /// Instantiates a Sputnik EVM with enabled cheatcodes + FFI and a simple non-forking in memory /// backend and tracing disabled diff --git a/forge/Cargo.toml b/forge/Cargo.toml index 6fdf1ef33ced..b14337dec8a7 100644 --- a/forge/Cargo.toml +++ b/forge/Cargo.toml @@ -8,11 +8,10 @@ license = "MIT OR Apache-2.0" [dependencies] foundry-utils = { path = "./../utils" } -evm-adapters = { path = "./../evm-adapters", default-features = false } +evm-adapters = { path = "./../evm-adapters", features = ["sputnik", "sputnik-helpers"] } # ethers = { version = "0.5.2" } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full"] } - eyre = "0.6.5" semver = "1.0.4" serde_json = "1.0.67" @@ -25,9 +24,13 @@ tokio = { version = "1.10.1" } tracing = "0.1.26" tracing-subscriber = "0.2.20" proptest = "1.0.0" +rayon = "1.5" + +# load sputnik for parallel evm usage +sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm" } +# For now we ignore evmodin in Forge. +# evmodin = { git = "https://github.com/vorot93/evmodin" } [dev-dependencies] -evm-adapters = { path = "./../evm-adapters", features = ["sputnik", "sputnik-helpers", "evmodin", "evmodin-helpers"] } evmodin = { git = "https://github.com/vorot93/evmodin", features = ["util"] } -evm = { git = "https://github.com/rust-blockchain/evm" } ethers = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["solc-full", "solc-tests"] } diff --git a/forge/src/lib.rs b/forge/src/lib.rs index 89c93703c848..a62df5af68e6 100644 --- a/forge/src/lib.rs +++ b/forge/src/lib.rs @@ -15,8 +15,15 @@ pub mod test_helpers { use ethers::{ prelude::Lazy, solc::{CompilerOutput, Project, ProjectPathsConfig}, + types::U256, + }; + use evm_adapters::{ + evm_opts::{Env, EvmOpts, EvmType}, + sputnik::helpers::VICINITY, + FAUCET_ACCOUNT, }; use regex::Regex; + use sputnik::backend::MemoryBackend; pub static COMPILED: Lazy = Lazy::new(|| { // NB: should we add a test-helper function that makes creating these @@ -27,6 +34,21 @@ pub mod test_helpers { project.compile().unwrap().output() }); + pub static EVM_OPTS: Lazy = Lazy::new(|| EvmOpts { + env: Env { gas_limit: 18446744073709551615, chain_id: 1, ..Default::default() }, + initial_balance: U256::MAX, + evm_type: EvmType::Sputnik, + ..Default::default() + }); + + pub static BACKEND: Lazy> = Lazy::new(|| { + let mut backend = MemoryBackend::new(&*VICINITY, Default::default()); + // max out the balance of the faucet + let faucet = backend.state_mut().entry(*FAUCET_ACCOUNT).or_insert_with(Default::default); + faucet.balance = U256::MAX; + backend + }); + pub struct Filter { test_regex: Regex, contract_regex: Regex, diff --git a/forge/src/multi_runner.rs b/forge/src/multi_runner.rs index cd3c1dfbf0b6..90c2e452bf2a 100644 --- a/forge/src/multi_runner.rs +++ b/forge/src/multi_runner.rs @@ -1,9 +1,9 @@ use crate::{runner::TestResult, ContractRunner, TestFilter}; +use evm_adapters::evm_opts::{BackendKind, EvmOpts}; +use sputnik::{backend::Backend, Config}; use ethers::solc::Artifact; -use evm_adapters::Evm; - use ethers::{ abi::Abi, prelude::ArtifactOutput, @@ -14,7 +14,8 @@ use ethers::{ use proptest::test_runner::TestRunner; use eyre::Result; -use std::{collections::BTreeMap, marker::PhantomData}; +use rayon::prelude::*; +use std::collections::BTreeMap; /// Builder used for instantiating the multi-contract runner #[derive(Debug, Default)] @@ -26,20 +27,17 @@ pub struct MultiContractRunnerBuilder { pub sender: Option
, /// The initial balance for each one of the deployed smart contracts pub initial_balance: U256, + /// The EVM Configuration to use + pub evm_cfg: Option, } impl MultiContractRunnerBuilder { /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm - pub fn build( - self, - project: Project, - mut evm: E, - ) -> Result> + pub fn build(self, project: Project, evm_opts: EvmOpts) -> Result where // TODO: Can we remove the static? It's due to the `into_artifacts()` call below A: ArtifactOutput + 'static, - E: Evm, { println!("compiling..."); let output = project.compile()?; @@ -52,14 +50,11 @@ impl MultiContractRunnerBuilder { println!("success."); } - let sender = self.sender.unwrap_or_default(); - let initial_balance = self.initial_balance; - // This is just the contracts compiled, but we need to merge this with the read cached // artifacts let contracts = output.into_artifacts(); let mut known_contracts: BTreeMap)> = Default::default(); - let mut deployed_contracts: BTreeMap)> = + let mut deployable_contracts: BTreeMap = Default::default(); for (fname, contract) in contracts { @@ -70,15 +65,7 @@ impl MultiContractRunnerBuilder { continue } - if abi.constructor.as_ref().map(|c| c.inputs.is_empty()).unwrap_or(true) && - abi.functions().any(|func| func.name.starts_with("test")) - { - let span = tracing::trace_span!("deploying", ?fname); - let _enter = span.enter(); - let (addr, _, _, logs) = evm.deploy(sender, bytecode.clone(), 0u32.into())?; - evm.set_balance(addr, initial_balance); - deployed_contracts.insert(fname.clone(), (abi.clone(), addr, logs)); - } + deployable_contracts.insert(fname.clone(), (abi.clone(), bytecode.clone())); let split = fname.split(':').collect::>(); let contract_name = if split.len() > 1 { split[1] } else { split[0] }; @@ -89,11 +76,11 @@ impl MultiContractRunnerBuilder { } Ok(MultiContractRunner { - contracts: deployed_contracts, + contracts: deployable_contracts, known_contracts, identified_contracts: Default::default(), - evm, - state: PhantomData, + evm_opts, + evm_cfg: self.evm_cfg.unwrap_or_else(Config::london), sender: self.sender, fuzzer: self.fuzzer, }) @@ -116,46 +103,57 @@ impl MultiContractRunnerBuilder { self.fuzzer = Some(fuzzer); self } + + #[must_use] + pub fn evm_cfg(mut self, evm_cfg: Config) -> Self { + self.evm_cfg = Some(evm_cfg); + self + } } /// A multi contract runner receives a set of contracts deployed in an EVM instance and proceeds /// to run all test functions in these contracts. -pub struct MultiContractRunner { - /// Mapping of contract name to compiled bytecode, deployed address and logs emitted during - /// deployment - pub contracts: BTreeMap)>, +pub struct MultiContractRunner { + /// Mapping of contract name to Abi and creation bytecode + pub contracts: BTreeMap, /// Compiled contracts by name that have an Abi and runtime bytecode pub known_contracts: BTreeMap)>, /// Identified contracts by test pub identified_contracts: BTreeMap>, /// The EVM instance used in the test runner - pub evm: E, + pub evm_opts: EvmOpts, + /// The EVM revision config + pub evm_cfg: Config, /// The fuzzer which will be used to run parametric tests (w/ non-0 solidity args) fuzzer: Option, /// The address which will be used as the `from` field in all EVM calls sender: Option
, - /// Market type for the EVM state being used - state: PhantomData, } -impl MultiContractRunner -where - E: Evm, - S: Clone, -{ +impl MultiContractRunner { pub fn test( &mut self, - filter: &impl TestFilter, + filter: &(impl TestFilter + Send + Sync), ) -> Result>> { // TODO: Convert to iterator, ideally parallel one? let contracts = std::mem::take(&mut self.contracts); - let init_state: S = self.evm.state().clone(); + let vicinity = self.evm_opts.vicinity()?; + let backend = self.evm_opts.backend(&vicinity)?; + let results = contracts - .iter() + .par_iter() .filter(|(name, _)| filter.matches_contract(name)) - .map(|(name, (abi, address, logs))| { - let result = self.run_tests(name, abi, *address, logs, filter, &init_state)?; + .map(|(name, (abi, deploy_code))| { + // unavoidable duplication here? + let result = match backend { + BackendKind::Simple(ref backend) => { + self.run_tests(name, abi, backend, deploy_code.clone(), filter)? + } + BackendKind::Shared(ref backend) => { + self.run_tests(name, abi, backend, deploy_code.clone(), filter)? + } + }; Ok((name.clone(), result)) }) .filter_map(|x: Result<_>| x.ok()) @@ -174,25 +172,30 @@ where err, fields(name = %_name) )] - fn run_tests( - &mut self, + fn run_tests( + &self, _name: &str, contract: &Abi, - address: Address, - init_logs: &[String], + backend: &B, + deploy_code: ethers::prelude::Bytes, filter: &impl TestFilter, - init_state: &S, ) -> Result> { - let mut runner = - ContractRunner::new(&mut self.evm, contract, address, self.sender, init_logs); - runner.run_tests(filter, self.fuzzer.as_mut(), init_state, Some(&self.known_contracts)) + let runner = ContractRunner::new( + &self.evm_opts, + &self.evm_cfg, + backend, + contract, + deploy_code, + self.sender, + ); + runner.run_tests(filter, self.fuzzer.clone(), Some(&self.known_contracts)) } } #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::Filter; + use crate::test_helpers::{Filter, EVM_OPTS}; use ethers::solc::ProjectPathsConfig; use std::path::PathBuf; @@ -212,12 +215,12 @@ mod tests { .unwrap() } - fn runner>(evm: E) -> MultiContractRunner { - MultiContractRunnerBuilder::default().build(project(), evm).unwrap() + fn runner() -> MultiContractRunner { + MultiContractRunnerBuilder::default().build(project(), EVM_OPTS.clone()).unwrap() } - fn test_multi_runner>(evm: E) { - let mut runner = runner(evm); + fn test_multi_runner() { + let mut runner = runner(); let results = runner.test(&Filter::new(".*", ".*")).unwrap(); // 8 contracts being built @@ -240,8 +243,8 @@ mod tests { assert!(only_gm["GmTest.json:GmTest"]["testGm()"].success); } - fn test_abstract_contract>(evm: E) { - let mut runner = runner(evm); + fn test_abstract_contract() { + let mut runner = runner(); let results = runner.test(&Filter::new(".*", ".*")).unwrap(); assert!(results.get("Tests.json:Tests").is_none()); assert!(results.get("ATests.json:ATests").is_some()); @@ -250,14 +253,11 @@ mod tests { mod sputnik { use super::*; - use evm_adapters::sputnik::helpers::vm; use std::collections::HashMap; #[test] fn test_sputnik_debug_logs() { - let evm = vm(); - - let mut runner = runner(evm); + let mut runner = runner(); let results = runner.test(&Filter::new(".*", ".*")).unwrap(); let reasons = results["DebugLogsTest.json:DebugLogsTest"] @@ -289,14 +289,12 @@ mod tests { #[test] fn test_sputnik_multi_runner() { - test_multi_runner(vm()); + test_multi_runner(); } #[test] fn test_sputnik_abstract_contract() { - test_abstract_contract(vm()); + test_abstract_contract(); } } - - // TODO: Add EvmOdin tests once we get the Mocked Host working } diff --git a/forge/src/runner.rs b/forge/src/runner.rs index 0c2501951ab0..a8859928e859 100644 --- a/forge/src/runner.rs +++ b/forge/src/runner.rs @@ -1,19 +1,26 @@ use crate::TestFilter; +use evm_adapters::{ + evm_opts::EvmOpts, + sputnik::{helpers::TestSputnikVM, Executor, PRECOMPILES_MAP}, +}; +use rayon::iter::ParallelIterator; +use sputnik::{backend::Backend, Config}; use ethers::{ abi::{Abi, Function, Token}, types::{Address, Bytes}, }; -use evm_adapters::call_tracing::CallTraceArena; - use evm_adapters::{ + call_tracing::CallTraceArena, fuzz::{FuzzTestResult, FuzzedCases, FuzzedExecutor}, + sputnik::cheatcodes::debugger::DebugArena, Evm, EvmError, }; use eyre::Result; -use std::{collections::BTreeMap, fmt, marker::PhantomData, time::Instant}; +use std::{collections::BTreeMap, fmt, time::Instant}; use proptest::test_runner::{TestError, TestRunner}; +use rayon::iter::IntoParallelRefIterator; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -64,6 +71,10 @@ pub struct TestResult { /// Identified contracts pub identified_contracts: Option>, + + /// Debug Steps + #[serde(skip)] + pub debug_calls: Option>, } impl TestResult { @@ -129,52 +140,67 @@ impl TestKind { } } -pub struct ContractRunner<'a, S, E> { - /// Mutable reference to the EVM type. - /// This is a temporary hack to work around the mutability restrictions of - /// [`proptest::TestRunner::run`] which takes a `Fn` preventing interior mutability. [See also](https://github.com/gakonst/dapptools-rs/pull/44). - /// Wrapping it like that allows the `test` function to gain mutable access regardless and - /// since we don't use any parallelized fuzzing yet the `test` function has exclusive access of - /// the mutable reference over time of its existence. - pub evm: &'a mut E, +pub struct ContractRunner<'a, B> { + // EVM Config Options + /// The options used to instantiate a new EVM. + pub evm_opts: &'a EvmOpts, + /// The backend used by the VM. + pub backend: &'a B, + /// The VM Configuration to use for the runner (London, Berlin , ...) + pub evm_cfg: &'a Config, + + // Contract deployment options /// The deployed contract's ABI pub contract: &'a Abi, /// The deployed contract's address - pub address: Address, + // This is cheap to clone due to [`bytes::Bytes`], so OK to own + pub code: ethers::prelude::Bytes, /// The address which will be used as the `from` field in all EVM calls pub sender: Address, - /// Any logs emitted in the constructor of the specific contract - pub init_logs: &'a [String], - // need to constrain the trait generic - state: PhantomData, } -impl<'a, S, E> ContractRunner<'a, S, E> { +impl<'a, B: Backend> ContractRunner<'a, B> { pub fn new( - evm: &'a mut E, + evm_opts: &'a EvmOpts, + evm_cfg: &'a Config, + backend: &'a B, contract: &'a Abi, - address: Address, + code: ethers::prelude::Bytes, sender: Option
, - init_logs: &'a [String], ) -> Self { - Self { - evm, - contract, - address, - init_logs, - state: PhantomData, - sender: sender.unwrap_or_default(), - } + Self { evm_opts, evm_cfg, backend, contract, code, sender: sender.unwrap_or_default() } } } -impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { +// Require that the backend is Cloneable. This allows us to use the `SharedBackend` from +// evm-adapters which is clone-able. +impl<'a, B: Backend + Clone + Send + Sync> ContractRunner<'a, B> { + /// Creates a new EVM and deploys the test contract inside the runner + /// from the sending account. + pub fn new_sputnik_evm(&'a self) -> eyre::Result<(Address, TestSputnikVM<'a, B>, Vec)> { + // create the EVM, clone the backend. + let mut executor = Executor::new_with_cheatcodes( + self.backend.clone(), + self.evm_opts.env.gas_limit, + self.evm_cfg, + &*PRECOMPILES_MAP, + self.evm_opts.ffi, + self.evm_opts.verbosity > 2, + self.evm_opts.debug, + ); + + // deploy an instance of the contract inside the runner in the EVM + let (addr, _, _, logs) = + executor.deploy(self.sender, self.code.clone(), 0u32.into()).expect("couldn't deploy"); + executor.set_balance(addr, self.evm_opts.initial_balance); + Ok((addr, executor, logs)) + } + /// Runs all tests for a contract whose names match the provided regular expression pub fn run_tests( - &mut self, + &self, filter: &impl TestFilter, - fuzzer: Option<&mut TestRunner>, - init_state: &S, + fuzzer: Option, known_contracts: Option<&BTreeMap)>>, ) -> Result> { tracing::info!("starting tests"); @@ -190,11 +216,9 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { // run all unit tests let unit_tests = test_fns - .iter() + .par_iter() .filter(|func| func.inputs.is_empty()) .map(|func| { - // Before each test run executes, ensure we're at our initial state. - self.evm.reset(init_state.clone()); let result = self.run_test(func, needs_setup, known_contracts)?; Ok((func.signature(), result)) }) @@ -202,10 +226,9 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let map = if let Some(fuzzer) = fuzzer { let fuzz_tests = test_fns - .iter() + .par_iter() .filter(|func| !func.inputs.is_empty()) .map(|func| { - self.evm.reset(init_state.clone()); let result = self.run_fuzz_test(func, needs_setup, fuzzer.clone(), known_contracts)?; Ok((func.signature(), result)) @@ -229,7 +252,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { #[tracing::instrument(name = "test", skip_all, fields(name = %func.signature()))] pub fn run_test( - &mut self, + &self, func: &Function, setup: bool, known_contracts: Option<&BTreeMap)>>, @@ -242,9 +265,9 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "unit-testing"); - let mut logs = self.init_logs.to_vec(); + let (address, mut evm, init_logs) = self.new_sputnik_evm()?; - self.evm.reset_traces(); + let mut logs = init_logs; let mut traces: Option> = None; let mut identified_contracts: Option> = None; @@ -252,17 +275,18 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { // call the setup function in each test to reset the test's state. if setup { tracing::trace!("setting up"); - let setup_logs = match self.evm.setup(self.address) { + 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 self.evm.tracing_enabled() { + if evm.tracing_enabled() { self.update_traces( &mut traces, &mut identified_contracts, known_contracts, setup, + &mut evm, ); } @@ -275,40 +299,46 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Standard(0), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { + Some(evm.debug_calls()) + } else { + None + }, }) } }; logs.extend_from_slice(&setup_logs); } - let (status, reason, gas_used, logs) = match self.evm.call::<(), _, _>( - self.sender, - self.address, - func.clone(), - (), - 0.into(), - ) { - Ok((_, status, gas_used, execution_logs)) => { - logs.extend(execution_logs); - (status, None, gas_used, logs) - } - Err(err) => match err { - EvmError::Execution { reason, gas_used, logs: execution_logs } => { + let (status, reason, gas_used, logs) = + match evm.call::<(), _, _>(self.sender, address, func.clone(), (), 0.into()) { + Ok((_, status, gas_used, execution_logs)) => { logs.extend(execution_logs); - // add reverted logs - logs.extend(self.evm.all_logs()); - (E::revert(), Some(reason), gas_used, logs) + (status, None, gas_used, logs) } - err => { - tracing::error!(?err); - return Err(err.into()) - } - }, - }; + Err(err) => match err { + EvmError::Execution { reason, gas_used, logs: execution_logs } => { + logs.extend(execution_logs); + // add reverted logs + logs.extend(evm.all_logs()); + (revert(&evm), Some(reason), gas_used, logs) + } + err => { + tracing::error!(?err); + return Err(err.into()) + } + }, + }; - self.update_traces(&mut traces, &mut identified_contracts, known_contracts, setup); + self.update_traces( + &mut traces, + &mut identified_contracts, + known_contracts, + setup, + &mut evm, + ); - let success = self.evm.check_success(self.address, &status, should_fail); + let success = evm.check_success(address, &status, should_fail); let duration = Instant::now().duration_since(start); tracing::debug!(?duration, %success, %gas_used); @@ -321,12 +351,13 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Standard(gas_used), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { Some(evm.debug_calls()) } else { None }, }) } #[tracing::instrument(name = "fuzz-test", skip_all, fields(name = %func.signature()))] pub fn run_fuzz_test( - &mut self, + &self, func: &Function, setup: bool, runner: TestRunner, @@ -337,7 +368,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let should_fail = func.name.starts_with("testFail"); tracing::debug!(func = ?func.signature(), should_fail, "fuzzing"); - self.evm.reset_traces(); + let (address, mut evm, init_logs) = self.new_sputnik_evm()?; let mut traces: Option> = None; let mut identified_contracts: Option> = None; @@ -345,17 +376,18 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { // call the setup function in each test to reset the test's state. if setup { tracing::trace!("setting up"); - match self.evm.setup(self.address) { + match evm.setup(address) { Ok((_reason, _setup_logs)) => {} Err(e) => { // if tracing is enabled, just return it as a failed test // otherwise abort - if self.evm.tracing_enabled() { + if evm.tracing_enabled() { self.update_traces( &mut traces, &mut identified_contracts, known_contracts, setup, + &mut evm, ); } return Ok(TestResult { @@ -367,35 +399,47 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Fuzz(FuzzedCases::new(vec![])), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { + Some(evm.debug_calls()) + } else { + None + }, }) } } } - let mut logs = self.init_logs.to_vec(); + let mut logs = init_logs; - let prev = self.evm.set_tracing_enabled(false); + let prev = evm.set_tracing_enabled(false); // instantiate the fuzzed evm in line - let evm = FuzzedExecutor::new(self.evm, runner, self.sender); - let FuzzTestResult { cases, test_error } = evm.fuzz(func, self.address, should_fail); + let evm = FuzzedExecutor::new(&mut evm, runner, self.sender); + let FuzzTestResult { cases, test_error } = evm.fuzz(func, address, should_fail); + 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 _ = self.evm.set_tracing_enabled(true); + let _ = evm.set_tracing_enabled(true); } let (_retdata, status, _gas, execution_logs) = - self.evm.call_raw(self.sender, self.address, bytes.clone(), 0.into(), false)?; - if >::is_fail(&status) { + evm.call_raw(self.sender, address, bytes.clone(), 0.into(), false)?; + if is_fail(evm, status) { logs.extend(execution_logs); // add reverted logs - logs.extend(self.evm.all_logs()); + logs.extend(evm.all_logs()); } else { logs.extend(execution_logs); } - self.update_traces(&mut traces, &mut identified_contracts, known_contracts, setup); + self.update_traces( + &mut traces, + &mut identified_contracts, + known_contracts, + setup, + evm, + ); } } @@ -421,8 +465,6 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { let duration = Instant::now().duration_since(start); tracing::debug!(?duration, %success); - // reset tracing to previous value in case next test *isn't* a fuzz test - self.evm.set_tracing_enabled(prev); // from that call? Ok(TestResult { success, @@ -433,18 +475,20 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { kind: TestKind::Fuzz(cases), traces, identified_contracts, + debug_calls: if evm.state().debug_enabled { Some(evm.debug_calls()) } else { None }, }) } - fn update_traces( - &mut self, + fn update_traces>( + &self, traces: &mut Option>, identified_contracts: &mut Option>, known_contracts: Option<&BTreeMap)>>, setup: bool, + evm: &mut E, ) { - let evm_traces = self.evm.traces(); - if !evm_traces.is_empty() && self.evm.tracing_enabled() { + let evm_traces = evm.traces(); + if !evm_traces.is_empty() && evm.tracing_enabled() { let mut ident = BTreeMap::new(); // create an iter over the traces let mut trace_iter = evm_traces.into_iter(); @@ -456,7 +500,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { 0, known_contracts.expect("traces enabled but no identified_contracts"), &mut ident, - self.evm, + evm, ); temp_traces.push(setup); } @@ -466,7 +510,7 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { 0, known_contracts.expect("traces enabled but no identified_contracts"), &mut ident, - self.evm, + evm, ); temp_traces.push(test_trace); } @@ -475,49 +519,62 @@ impl<'a, S: Clone, E: Evm> ContractRunner<'a, S, E> { *identified_contracts = Some(ident); *traces = Some(temp_traces); } - self.evm.reset_traces(); + evm.reset_traces(); } } +// Helper functions for getting the revert status for a `ReturnReason` without having +// to specify the full EVM signature +fn is_fail + evm_adapters::Evm, T>( + _evm: &mut E, + status: T, +) -> bool { + >::is_fail(&status) +} + +fn revert + evm_adapters::Evm, T>(_evm: &E) -> T { + >::revert() +} + #[cfg(test)] mod tests { use super::*; - use crate::test_helpers::{Filter, COMPILED}; + use crate::test_helpers::{Filter, BACKEND, COMPILED, EVM_OPTS}; use ethers::solc::artifacts::CompactContractRef; - use evm_adapters::sputnik::helpers::vm; mod sputnik { + use ::sputnik::backend::MemoryBackend; + use evm_adapters::sputnik::helpers::CFG_NO_LMT; use foundry_utils::get_func; use proptest::test_runner::Config as FuzzConfig; use super::*; + pub fn runner<'a>( + abi: &'a Abi, + code: ethers::prelude::Bytes, + ) -> ContractRunner<'a, MemoryBackend<'a>> { + ContractRunner::new(&*EVM_OPTS, &*CFG_NO_LMT, &*BACKEND, abi, code, None) + } + #[test] fn test_runner() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let evm = vm(); - super::test_runner(evm, compiled); + super::test_runner(compiled); } #[test] fn test_function_overriding() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let init_state = evm.state().clone(); - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; - let mut fuzzer = TestRunner::new(cfg); - let results = runner - .run_tests(&Filter::new("testGreeting", ".*"), Some(&mut fuzzer), &init_state, None) - .unwrap(); + let fuzzer = TestRunner::new(cfg); + let results = + runner.run_tests(&Filter::new("testGreeting", ".*"), Some(fuzzer), None).unwrap(); assert!(results["testGreeting()"].success); assert!(results["testGreeting(string)"].success); assert!(results["testGreeting(string,string)"].success); @@ -526,22 +583,14 @@ mod tests { #[test] fn test_fuzzing_counterexamples() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let init_state = evm.state().clone(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; - let mut fuzzer = TestRunner::new(cfg); - let results = runner - .run_tests(&Filter::new("testFuzz.*", ".*"), Some(&mut fuzzer), &init_state, None) - .unwrap(); + let fuzzer = TestRunner::new(cfg); + let results = + runner.run_tests(&Filter::new("testFuzz.*", ".*"), Some(fuzzer), None).unwrap(); for (_, res) in results { assert!(!res.success); assert!(res.counterexample.is_some()); @@ -551,13 +600,8 @@ mod tests { #[test] fn test_fuzzing_ok() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -571,13 +615,8 @@ mod tests { #[test] fn test_fuzz_shrinking() { let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - let mut evm = vm(); - let (addr, _, _, _) = evm - .deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()) - .unwrap(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + let (_, code, _) = compiled.into_parts_or_default(); + let runner = runner(compiled.abi.as_ref().unwrap(), code); let mut cfg = FuzzConfig::default(); cfg.failure_persistence = None; @@ -611,35 +650,11 @@ mod tests { } } - mod evmodin_test { - use super::*; - use ::evmodin::{tracing::NoopTracer, util::mocked_host::MockedHost, Revision}; - use evm_adapters::evmodin::EvmOdin; - - #[test] - #[ignore] - fn test_runner() { - let revision = Revision::Istanbul; - let compiled = COMPILED.find("GreeterTest").expect("could not find contract"); - - let host = MockedHost::default(); - - let gas_limit = 12_000_000; - let evm = EvmOdin::new(host, gas_limit, revision, NoopTracer); - super::test_runner(evm, compiled); - } - } - - pub fn test_runner>(mut evm: E, compiled: CompactContractRef) { - let (addr, _, _, _) = - evm.deploy(Address::zero(), compiled.bytecode().unwrap().clone(), 0.into()).unwrap(); - - let init_state = evm.state().clone(); - - let mut runner = - ContractRunner::new(&mut evm, compiled.abi.as_ref().unwrap(), addr, None, &[]); + pub fn test_runner(compiled: CompactContractRef) { + let (_, code, _) = compiled.into_parts_or_default(); + let runner = sputnik::runner(compiled.abi.as_ref().unwrap(), code); - let res = runner.run_tests(&Filter::new(".*", ".*"), None, &init_state, None).unwrap(); + let res = runner.run_tests(&Filter::new(".*", ".*"), None, None).unwrap(); assert!(!res.is_empty()); assert!(res.iter().all(|(_, result)| result.success)); }