From 7add4bab47478f2606f430f64692b145a4774e9e Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 12 Sep 2021 16:25:08 +0300 Subject: [PATCH 1/3] feat: parallelize with rayon --- Cargo.toml | 1 + src/dapp.rs | 58 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 329fb0dcaea36..9e5bfca7bc4ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ rustc-hex = "2.1.0" serde_json = "1.0.67" rpassword = "5.0.1" evm = "0.30.1" +rayon = "1.5.1" [patch.'crates-io'] ethabi = { git = "https://github.com/gakonst/ethabi/", branch = "patch-1" } diff --git a/src/dapp.rs b/src/dapp.rs index f70a0d42c21a5..a94d40ba2fde2 100644 --- a/src/dapp.rs +++ b/src/dapp.rs @@ -2,7 +2,7 @@ use ethers::{ abi::{self, Detokenize, Function, FunctionExt, Tokenize}, prelude::{decode_function_data, encode_function_data}, types::*, - utils::{id, CompiledContract, Solc}, + utils::{CompiledContract, Solc}, }; use evm::backend::{MemoryAccount, MemoryBackend, MemoryVicinity}; @@ -14,11 +14,12 @@ use std::collections::{BTreeMap, HashMap}; use eyre::Result; use crate::utils::get_func; +use rayon::prelude::*; // TODO: Check if we can implement this as the base layer of an ethers-provider // Middleware stack instead of doing RPC calls. pub struct Executor<'a, S> { - executor: StackExecutor<'a, S>, + executor: std::sync::Mutex>, gas_limit: u64, } @@ -40,7 +41,7 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { let executor = StackExecutor::new(state, &config); Self { - executor, + executor: std::sync::Mutex::new(executor), gas_limit, } } @@ -55,7 +56,7 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { /// Runs the selected function pub fn call( - &mut self, + &self, from: Address, to: Address, func: &Function, @@ -64,9 +65,13 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { ) -> Result<(D, ExitReason)> { let data = encode_function_data(&func, args)?; - let (status, retdata) = - self.executor - .transact_call(from, to, value, data.to_vec(), self.gas_limit); + let (status, retdata) = self.executor.lock().unwrap().transact_call( + from, + to, + value, + data.to_vec(), + self.gas_limit, + ); let retdata = decode_function_data(&func, retdata, false)?; @@ -114,20 +119,20 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { } #[derive(Clone, Debug)] -struct TestResult { +pub struct TestResult { success: bool, // TODO: Add gas consumption if possible? } -struct ContractRunner<'a, S> { - executor: &'a mut Executor<'a, S>, +pub struct ContractRunner<'a, S> { + executor: &'a Executor<'a, S>, contract: &'a CompiledContract, address: Address, } impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { /// Runs the `setUp()` function call to initiate the contract's state - fn setup(&mut self) -> Result<()> { + fn setup(&self) -> Result<()> { let (_, status) = self.executor.call::<(), _>( Address::zero(), self.address, @@ -140,16 +145,18 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { } /// runs all tests under a contract - pub fn test(&mut self) -> Result> { + pub fn test(&self) -> Result> { let test_fns = self .contract .abi .functions() .into_iter() - .filter(|func| func.name.starts_with("test")); + .filter(|func| func.name.starts_with("test")) + .collect::>(); // run all tests let map = test_fns + .par_iter() .map(|func| { // call the setup function in each test to reset the test's state. // if we did this outside the map, we'd not have test isolation @@ -164,7 +171,7 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { Ok(map) } - pub fn test_func(&mut self, func: &Function) -> TestResult { + pub fn test_func(&self, func: &Function) -> TestResult { // the expected result depends on the function name let expected = if func.name.contains("testFail") { ExitReason::Revert(ExitRevert::Reverted) @@ -174,7 +181,7 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { // set the selector & execute the call let data = func.selector().to_vec(); - let (result, _) = self.executor.executor.transact_call( + let (result, _) = self.executor.executor.lock().unwrap().transact_call( Address::zero(), self.address, 0.into(), @@ -188,13 +195,14 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { } } -fn decode_revert(error: &[u8]) -> Result { +pub fn decode_revert(error: &[u8]) -> Result { Ok(abi::decode(&[abi::ParamType::String], &error[4..])?[0].to_string()) } #[cfg(test)] mod tests { use super::*; + use ethers::utils::id; #[test] fn can_call_vm_directly() { @@ -211,7 +219,7 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); - let mut dapp = Executor::new(12_000_000, &cfg, &backend); + let dapp = Executor::new(12_000_000, &cfg, &backend); let (_, status) = dapp .call::<(), _>( @@ -251,7 +259,7 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); - let mut dapp = Executor::new(12_000_000, &cfg, &backend); + let dapp = Executor::new(12_000_000, &cfg, &backend); // call the setup function to deploy the contracts inside the test let (_, status) = dapp @@ -291,9 +299,9 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); - let mut dapp = Executor::new(12_000_000, &cfg, &backend); + let dapp = Executor::new(12_000_000, &cfg, &backend); - let (status, res) = dapp.executor.transact_call( + let (status, res) = dapp.executor.lock().unwrap().transact_call( Address::zero(), addr, 0.into(), @@ -318,7 +326,7 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); - let mut dapp = Executor::new(12_000_000, &cfg, &backend); + let dapp = Executor::new(12_000_000, &cfg, &backend); // call the setup function to deploy the contracts inside the test let (_, status) = dapp @@ -332,7 +340,7 @@ mod tests { .unwrap(); assert_eq!(status, ExitReason::Succeed(ExitSucceed::Stopped)); - let (status, res) = dapp.executor.transact_call( + let (status, res) = dapp.executor.lock().unwrap().transact_call( Address::zero(), addr, 0.into(), @@ -359,10 +367,10 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); - let mut dapp = Executor::new(12_000_000, &cfg, &backend); + let dapp = Executor::new(12_000_000, &cfg, &backend); - let mut runner = ContractRunner { - executor: &mut dapp, + let runner = ContractRunner { + executor: &dapp, contract: compiled, address: addr, }; From eacf767099a73fe6d8c0047a54b65860b5b36ee4 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 12 Sep 2021 16:48:32 +0300 Subject: [PATCH 2/3] gate rayon behind feature flag --- Cargo.toml | 5 ++- src/dapp.rs | 93 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e5bfca7bc4ab..f43ebf3c1fd3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,10 @@ rustc-hex = "2.1.0" serde_json = "1.0.67" rpassword = "5.0.1" evm = "0.30.1" -rayon = "1.5.1" +rayon = { version = "1.5.1", optional = true } [patch.'crates-io'] ethabi = { git = "https://github.com/gakonst/ethabi/", branch = "patch-1" } + +[features] +parallel = ['rayon'] diff --git a/src/dapp.rs b/src/dapp.rs index a94d40ba2fde2..aacd04591a31f 100644 --- a/src/dapp.rs +++ b/src/dapp.rs @@ -6,7 +6,7 @@ use ethers::{ }; use evm::backend::{MemoryAccount, MemoryBackend, MemoryVicinity}; -use evm::executor::{MemoryStackState, StackExecutor, StackSubstateMetadata}; +use evm::executor::{self, MemoryStackState, StackSubstateMetadata}; use evm::Config; use evm::{ExitReason, ExitRevert, ExitSucceed}; use std::collections::{BTreeMap, HashMap}; @@ -14,15 +14,72 @@ use std::collections::{BTreeMap, HashMap}; use eyre::Result; use crate::utils::get_func; -use rayon::prelude::*; // TODO: Check if we can implement this as the base layer of an ethers-provider // Middleware stack instead of doing RPC calls. pub struct Executor<'a, S> { - executor: std::sync::Mutex>, + executor: StackExecutor<'a, S>, gas_limit: u64, } +#[cfg(feature = "parallel")] +use rayon::prelude::*; + +use stack_executor::StackExecutor; + +mod stack_executor { + use evm::executor::StackState; + + use super::*; + + #[cfg(feature = "parallel")] + use std::sync::Mutex; + + /// Thread-safe wrapper around the StackExecutor which can be triggered with a + /// `parallel` feature flag to compare parallel/serial performance + pub struct StackExecutor<'a, S> { + #[cfg(feature = "parallel")] + executor: Mutex>, + #[cfg(not(feature = "parallel"))] + executor: executor::StackExecutor<'a, S>, + } + + impl<'a, S: StackState<'a>> StackExecutor<'a, S> { + pub fn new(state: S, config: &'a Config) -> Self { + let executor = executor::StackExecutor::new(state, config); + #[cfg(feature = "parallel")] + let executor = Mutex::new(executor); + Self { executor } + } + + #[cfg(not(feature = "parallel"))] + pub fn transact_call( + &mut self, + caller: H160, + address: H160, + value: U256, + data: Vec, + gas_limit: u64, + ) -> (ExitReason, Vec) { + self.executor + .transact_call(caller, address, value, data.to_vec(), gas_limit) + } + + #[cfg(feature = "parallel")] + pub fn transact_call( + &self, + caller: H160, + address: H160, + value: U256, + data: Vec, + gas_limit: u64, + ) -> (ExitReason, Vec) { + let mut executor = self.executor.lock().unwrap(); + executor.transact_call(caller, address, value, data.to_vec(), gas_limit) + } + } +} + type MemoryState = BTreeMap; impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { @@ -41,7 +98,7 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { let executor = StackExecutor::new(state, &config); Self { - executor: std::sync::Mutex::new(executor), + executor, gas_limit, } } @@ -56,7 +113,7 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { /// Runs the selected function pub fn call( - &self, + &mut self, from: Address, to: Address, func: &Function, @@ -65,13 +122,9 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { ) -> Result<(D, ExitReason)> { let data = encode_function_data(&func, args)?; - let (status, retdata) = self.executor.lock().unwrap().transact_call( - from, - to, - value, - data.to_vec(), - self.gas_limit, - ); + let (status, retdata) = + self.executor + .transact_call(from, to, value, data.to_vec(), self.gas_limit); let retdata = decode_function_data(&func, retdata, false)?; @@ -125,14 +178,14 @@ pub struct TestResult { } pub struct ContractRunner<'a, S> { - executor: &'a Executor<'a, S>, + executor: &'a mut Executor<'a, S>, contract: &'a CompiledContract, address: Address, } impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { /// Runs the `setUp()` function call to initiate the contract's state - fn setup(&self) -> Result<()> { + fn setup(&mut self) -> Result<()> { let (_, status) = self.executor.call::<(), _>( Address::zero(), self.address, @@ -145,7 +198,7 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { } /// runs all tests under a contract - pub fn test(&self) -> Result> { + pub fn test(&mut self) -> Result> { let test_fns = self .contract .abi @@ -154,9 +207,13 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { .filter(|func| func.name.starts_with("test")) .collect::>(); + #[cfg(feature = "parallel")] + let test_fns = test_fns.par_iter(); + #[cfg(not(feature = "parallel"))] + let test_fns = test_fns.iter(); + // run all tests let map = test_fns - .par_iter() .map(|func| { // call the setup function in each test to reset the test's state. // if we did this outside the map, we'd not have test isolation @@ -171,7 +228,7 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { Ok(map) } - pub fn test_func(&self, func: &Function) -> TestResult { + pub fn test_func(&mut self, func: &Function) -> TestResult { // the expected result depends on the function name let expected = if func.name.contains("testFail") { ExitReason::Revert(ExitRevert::Reverted) @@ -181,7 +238,7 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { // set the selector & execute the call let data = func.selector().to_vec(); - let (result, _) = self.executor.executor.lock().unwrap().transact_call( + let (result, _) = self.executor.executor.transact_call( Address::zero(), self.address, 0.into(), From a0066209325c7466789874b21133379058439002 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 12 Sep 2021 16:55:27 +0300 Subject: [PATCH 3/3] make both serial and parallel work --- src/dapp.rs | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/src/dapp.rs b/src/dapp.rs index aacd04591a31f..a01219b172bb3 100644 --- a/src/dapp.rs +++ b/src/dapp.rs @@ -112,6 +112,27 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { } /// Runs the selected function + #[cfg(feature = "parallel")] + pub fn call( + &self, + from: Address, + to: Address, + func: &Function, + args: T, // derive arbitrary for Tokenize? + value: U256, + ) -> Result<(D, ExitReason)> { + let data = encode_function_data(&func, args)?; + + let (status, retdata) = + self.executor + .transact_call(from, to, value, data.to_vec(), self.gas_limit); + + let retdata = decode_function_data(&func, retdata, false)?; + + Ok((retdata, status)) + } + + #[cfg(not(feature = "parallel"))] pub fn call( &mut self, from: Address, @@ -178,11 +199,16 @@ pub struct TestResult { } pub struct ContractRunner<'a, S> { + #[cfg(feature = "parallel")] + executor: &'a Executor<'a, S>, + #[cfg(not(feature = "parallel"))] executor: &'a mut Executor<'a, S>, + contract: &'a CompiledContract, address: Address, } +#[cfg(not(feature = "parallel"))] impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { /// Runs the `setUp()` function call to initiate the contract's state fn setup(&mut self) -> Result<()> { @@ -252,6 +278,76 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { } } +#[cfg(feature = "parallel")] +impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> { + /// Runs the `setUp()` function call to initiate the contract's state + fn setup(&self) -> Result<()> { + let (_, status) = self.executor.call::<(), _>( + Address::zero(), + self.address, + &get_func("function setUp() external").unwrap(), + (), + 0.into(), + )?; + debug_assert_eq!(status, ExitReason::Succeed(ExitSucceed::Stopped)); + Ok(()) + } + + /// runs all tests under a contract + pub fn test(&self) -> Result> { + let test_fns = self + .contract + .abi + .functions() + .into_iter() + .filter(|func| func.name.starts_with("test")) + .collect::>(); + + #[cfg(feature = "parallel")] + let test_fns = test_fns.par_iter(); + #[cfg(not(feature = "parallel"))] + let test_fns = test_fns.iter(); + + // run all tests + let map = test_fns + .map(|func| { + // call the setup function in each test to reset the test's state. + // if we did this outside the map, we'd not have test isolation + self.setup()?; + + let result = self.test_func(func); + println!("{:?}, got {:?}", func.name, result); + Ok((func.name.clone(), result)) + }) + .collect::>>()?; + + Ok(map) + } + + pub fn test_func(&self, func: &Function) -> TestResult { + // the expected result depends on the function name + let expected = if func.name.contains("testFail") { + ExitReason::Revert(ExitRevert::Reverted) + } else { + ExitReason::Succeed(ExitSucceed::Stopped) + }; + + // set the selector & execute the call + let data = func.selector().to_vec(); + let (result, _) = self.executor.executor.transact_call( + Address::zero(), + self.address, + 0.into(), + data.to_vec(), + self.executor.gas_limit, + ); + + TestResult { + success: expected == result, + } + } +} + pub fn decode_revert(error: &[u8]) -> Result { Ok(abi::decode(&[abi::ParamType::String], &error[4..])?[0].to_string()) } @@ -277,6 +373,8 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); let dapp = Executor::new(12_000_000, &cfg, &backend); + #[cfg(not(feature = "parallel"))] + let mut dapp = dapp; let (_, status) = dapp .call::<(), _>( @@ -317,6 +415,8 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); let dapp = Executor::new(12_000_000, &cfg, &backend); + #[cfg(not(feature = "parallel"))] + let mut dapp = dapp; // call the setup function to deploy the contracts inside the test let (_, status) = dapp @@ -357,8 +457,10 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); let dapp = Executor::new(12_000_000, &cfg, &backend); + #[cfg(not(feature = "parallel"))] + let mut dapp = dapp; - let (status, res) = dapp.executor.lock().unwrap().transact_call( + let (status, res) = dapp.executor.transact_call( Address::zero(), addr, 0.into(), @@ -384,6 +486,8 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); let dapp = Executor::new(12_000_000, &cfg, &backend); + #[cfg(not(feature = "parallel"))] + let mut dapp = dapp; // call the setup function to deploy the contracts inside the test let (_, status) = dapp @@ -397,7 +501,7 @@ mod tests { .unwrap(); assert_eq!(status, ExitReason::Succeed(ExitSucceed::Stopped)); - let (status, res) = dapp.executor.lock().unwrap().transact_call( + let (status, res) = dapp.executor.transact_call( Address::zero(), addr, 0.into(), @@ -425,12 +529,19 @@ mod tests { let vicinity = Executor::new_vicinity(); let backend = Executor::new_backend(&vicinity, state); let dapp = Executor::new(12_000_000, &cfg, &backend); + #[cfg(not(feature = "parallel"))] + let mut dapp = dapp; let runner = ContractRunner { + #[cfg(feature = "parallel")] executor: &dapp, + #[cfg(not(feature = "parallel"))] + executor: &mut dapp, contract: compiled, address: addr, }; + #[cfg(not(feature = "parallel"))] + let mut runner = runner; let res = runner.test().unwrap(); assert!(res.iter().all(|(_, result)| result.success == true));