Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ rustc-hex = "2.1.0"
serde_json = "1.0.67"
rpassword = "5.0.1"
evm = "0.30.1"
rayon = { version = "1.5.1", optional = true }

[patch.'crates-io']
ethabi = { git = "https://github.com/gakonst/ethabi/", branch = "patch-1" }

[features]
parallel = ['rayon']
202 changes: 189 additions & 13 deletions src/dapp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ 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};
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};
Expand All @@ -22,6 +22,64 @@ pub struct Executor<'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<executor::StackExecutor<'a, S>>,
#[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<u8>,
gas_limit: u64,
) -> (ExitReason, Vec<u8>) {
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<u8>,
gas_limit: u64,
) -> (ExitReason, Vec<u8>) {
let mut executor = self.executor.lock().unwrap();
executor.transact_call(caller, address, value, data.to_vec(), gas_limit)
}
}
}

type MemoryState = BTreeMap<Address, MemoryAccount>;

impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> {
Expand Down Expand Up @@ -54,6 +112,27 @@ impl<'a> Executor<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> {
}

/// Runs the selected function
#[cfg(feature = "parallel")]
pub fn call<D: Detokenize, T: Tokenize>(
&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<D: Detokenize, T: Tokenize>(
&mut self,
from: Address,
Expand Down Expand Up @@ -114,17 +193,22 @@ 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> {
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<()> {
Expand All @@ -146,7 +230,13 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> {
.abi
.functions()
.into_iter()
.filter(|func| func.name.starts_with("test"));
.filter(|func| func.name.starts_with("test"))
.collect::<Vec<_>>();

#[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
Expand Down Expand Up @@ -188,13 +278,84 @@ impl<'a> ContractRunner<'a, MemoryStackState<'a, 'a, MemoryBackend<'a>>> {
}
}

fn decode_revert(error: &[u8]) -> Result<String> {
#[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<HashMap<String, TestResult>> {
let test_fns = self
.contract
.abi
.functions()
.into_iter()
.filter(|func| func.name.starts_with("test"))
.collect::<Vec<_>>();

#[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::<Result<HashMap<_, _>>>()?;

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<String> {
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() {
Expand All @@ -211,7 +372,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);
#[cfg(not(feature = "parallel"))]
let mut dapp = dapp;

let (_, status) = dapp
.call::<(), _>(
Expand Down Expand Up @@ -251,7 +414,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);
#[cfg(not(feature = "parallel"))]
let mut dapp = dapp;

// call the setup function to deploy the contracts inside the test
let (_, status) = dapp
Expand Down Expand Up @@ -291,7 +456,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);
#[cfg(not(feature = "parallel"))]
let mut dapp = dapp;

let (status, res) = dapp.executor.transact_call(
Address::zero(),
Expand All @@ -318,7 +485,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);
#[cfg(not(feature = "parallel"))]
let mut dapp = dapp;

// call the setup function to deploy the contracts inside the test
let (_, status) = dapp
Expand Down Expand Up @@ -359,13 +528,20 @@ 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 mut runner = ContractRunner {
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));
Expand Down