From ae3c3af0f9104788484138d6d7ffaa0824b65864 Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Sat, 19 Mar 2022 02:54:48 +0100 Subject: [PATCH 1/6] feat: fuzz dictionary Co-authored-by: brockelmore <31553173+brockelmore@users.noreply.github.com> --- evm/src/executor/mod.rs | 13 ++-- evm/src/fuzz/mod.rs | 18 +++++- evm/src/fuzz/strategies/calldata.rs | 16 +++-- evm/src/fuzz/strategies/mod.rs | 5 +- evm/src/fuzz/strategies/param.rs | 96 +++++++++++++++++++++++++++- evm/src/fuzz/strategies/state.rs | 99 +++++++++++++++++++++++++++++ 6 files changed, 229 insertions(+), 18 deletions(-) create mode 100644 evm/src/fuzz/strategies/state.rs diff --git a/evm/src/executor/mod.rs b/evm/src/executor/mod.rs index d2b7c735d401f..62a9f098e9bd3 100644 --- a/evm/src/executor/mod.rs +++ b/evm/src/executor/mod.rs @@ -41,6 +41,9 @@ use revm::{ }; use std::collections::BTreeMap; +/// A mapping of addresses to their changed state. +pub type StateChangeset = HashMap; + #[derive(thiserror::Error, Debug)] pub enum EvmError { /// Error which occurred during execution of a transaction @@ -54,7 +57,7 @@ pub enum EvmError { traces: Option, debug: Option, labels: BTreeMap, - state_changeset: HashMap, + state_changeset: StateChangeset, }, /// Error which occurred during ABI encoding/decoding #[error(transparent)] @@ -102,7 +105,7 @@ pub struct CallResult { /// /// This is only present if the changed state was not committed to the database (i.e. if you /// used `call` and `call_raw` not `call_committing` or `call_raw_committing`). - pub state_changeset: HashMap, + pub state_changeset: StateChangeset, } /// The result of a raw call. @@ -130,7 +133,7 @@ pub struct RawCallResult { /// /// This is only present if the changed state was not committed to the database (i.e. if you /// used `call` and `call_raw` not `call_committing` or `call_raw_committing`). - pub state_changeset: HashMap, + pub state_changeset: StateChangeset, } impl Default for RawCallResult { @@ -145,7 +148,7 @@ impl Default for RawCallResult { labels: BTreeMap::new(), traces: None, debug: None, - state_changeset: HashMap::new(), + state_changeset: StateChangeset::new(), } } } @@ -411,7 +414,7 @@ where &self, address: Address, reverted: bool, - state_changeset: HashMap, + state_changeset: StateChangeset, should_fail: bool, ) -> bool { // Construct a new VM with the state changeset diff --git a/evm/src/fuzz/mod.rs b/evm/src/fuzz/mod.rs index fdfa1f64046a5..feff1a7977ba7 100644 --- a/evm/src/fuzz/mod.rs +++ b/evm/src/fuzz/mod.rs @@ -14,7 +14,9 @@ use proptest::test_runner::{TestCaseError, TestError, TestRunner}; use revm::db::DatabaseRef; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, collections::BTreeMap, fmt}; -use strategies::fuzz_calldata; +use strategies::{ + collect_state_from_changeset, fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState, +}; /// Magic return code for the `assume` cheatcode pub const ASSUME_MAGIC_RETURN_CODE: &[u8] = "FOUNDRY::ASSUME".as_bytes(); @@ -54,14 +56,21 @@ where should_fail: bool, errors: Option<&Abi>, ) -> FuzzTestResult { - let strat = fuzz_calldata(func); - // Stores the consumed gas and calldata of every successful fuzz call let cases: RefCell> = RefCell::new(Default::default()); // Stores the result of the last call let call: RefCell = RefCell::new(Default::default()); + // Stores fuzz state for use with [fuzz_calldata_from_state] + let state: EvmFuzzState = EvmFuzzState::default(); + + // TODO: We should have a `FuzzerOpts` struct where we can configure the fuzzer. When we + // have that, we should add a way to configure strategy weights + let strat = proptest::strategy::Union::new_weighted(vec![ + (60, fuzz_calldata(func.clone())), + (40, fuzz_calldata_from_state(func.clone(), state.clone())), + ]); tracing::debug!(func = ?func.name, should_fail, "fuzzing"); let run_result = self.runner.clone().run(&strat, |calldata| { *call.borrow_mut() = self @@ -70,6 +79,9 @@ where .expect("could not make raw evm call"); let call = call.borrow(); + // Build fuzzer state + collect_state_from_changeset(&call.state_changeset, state.clone()); + // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME" if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE { return Err(TestCaseError::reject("ASSUME: Too many rejects")) diff --git a/evm/src/fuzz/strategies/calldata.rs b/evm/src/fuzz/strategies/calldata.rs index f3c3321388092..be9e24a7289bd 100644 --- a/evm/src/fuzz/strategies/calldata.rs +++ b/evm/src/fuzz/strategies/calldata.rs @@ -1,16 +1,18 @@ use super::fuzz_param; use ethers::{abi::Function, types::Bytes}; -use proptest::prelude::Strategy; +use proptest::prelude::{BoxedStrategy, Strategy}; -/// Given a function, it returns a proptest strategy which generates valid abi-encoded calldata +/// Given a function, it returns a strategy which generates valid calldata /// for that function's input types. -pub fn fuzz_calldata(func: &Function) -> impl Strategy + '_ { +pub fn fuzz_calldata(func: Function) -> BoxedStrategy { // We need to compose all the strategies generated for each parameter in all // possible combinations let strats = func.inputs.iter().map(|input| fuzz_param(&input.kind)).collect::>(); - strats.prop_map(move |tokens| { - tracing::trace!(input = ?tokens); - func.encode_input(&tokens).unwrap().into() - }) + strats + .prop_map(move |tokens| { + tracing::trace!(input = ?tokens); + func.encode_input(&tokens).unwrap().into() + }) + .boxed() } diff --git a/evm/src/fuzz/strategies/mod.rs b/evm/src/fuzz/strategies/mod.rs index 10a4a3bb5578b..735a9cf141013 100644 --- a/evm/src/fuzz/strategies/mod.rs +++ b/evm/src/fuzz/strategies/mod.rs @@ -2,7 +2,10 @@ mod uint; pub use uint::UintStrategy; mod param; -pub use param::fuzz_param; +pub use param::{fuzz_param, fuzz_param_from_state}; mod calldata; pub use calldata::fuzz_calldata; + +mod state; +pub use state::{collect_state_from_changeset, fuzz_calldata_from_state, EvmFuzzState}; diff --git a/evm/src/fuzz/strategies/param.rs b/evm/src/fuzz/strategies/param.rs index cc4376c8045dc..ab47a37f3fc8c 100644 --- a/evm/src/fuzz/strategies/param.rs +++ b/evm/src/fuzz/strategies/param.rs @@ -4,11 +4,14 @@ use ethers::{ }; use proptest::prelude::*; +use super::state::EvmFuzzState; + /// The max length of arrays we fuzz for is 256. pub const MAX_ARRAY_LEN: usize = 256; -/// Given an ethabi parameter type, returns a proptest strategy for generating values for that -/// datatype. Works with ABI Encoder v2 tuples. +/// Given a parameter type, returns a strategy for generating values for that type. +/// +/// Works with ABI Encoder v2 tuples. pub fn fuzz_param(param: &ParamType) -> impl Strategy { match param { ParamType::Address => { @@ -63,3 +66,92 @@ pub fn fuzz_param(param: &ParamType) -> impl Strategy { } } } + +/// Given a parameter type, returns a strategy for generating values for that type, given some EVM +/// fuzz state. +/// +/// Works with ABI Encoder v2 tuples. +pub fn fuzz_param_from_state(param: &ParamType, state: EvmFuzzState) -> BoxedStrategy { + let selectors = any::(); + match param { + ParamType::Address => selectors + .prop_map(move |selector| { + Address::from_slice(&selector.select(&*state.borrow())[12..]).into_token() + }) + .boxed(), + ParamType::Bytes => selectors + .prop_map(move |selector| Bytes::from(*selector.select(&*state.borrow())).into_token()) + .boxed(), + ParamType::Int(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + I256::from_raw(U256::from(*selector.select(&*state.borrow()))).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + // Generate a uintN in the correct range, then shift it to the range of intN + // by subtracting 2^(N-1) + let uint = U256::from(*selector.select(&*state.borrow())) % + U256::from(2usize).pow(U256::from(y * 8)); + let max_int_plus1 = U256::from(2usize).pow(U256::from(y * 8 - 1)); + let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); + num.into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type int{}", n), + }, + ParamType::Uint(n) => match n / 8 { + 32 => selectors + .prop_map(move |selector| { + U256::from(*selector.select(&*state.borrow())).into_token() + }) + .boxed(), + y @ 1..=31 => selectors + .prop_map(move |selector| { + (U256::from(*selector.select(&*state.borrow())) % + (U256::from(2usize).pow(U256::from(y * 8)))) + .into_token() + }) + .boxed(), + _ => panic!("unsupported solidity type uint{}", n), + }, + ParamType::Bool => selectors + .prop_map(move |selector| Token::Bool(selector.select(&*state.borrow())[31] == 1)) + .boxed(), + ParamType::String => selectors + .prop_map(move |selector| { + Token::String(unsafe { + std::str::from_utf8_unchecked(selector.select(&*state.borrow())).to_string() + }) + }) + .boxed(), + ParamType::Array(param) => { + proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN) + .prop_map(Token::Array) + .boxed() + } + ParamType::FixedBytes(size) => { + let size = *size; + selectors + .prop_map(move |selector| { + let val = selector.select(&*state.borrow())[32 - size..].to_vec(); + Token::FixedBytes(val) + }) + .boxed() + } + ParamType::FixedArray(param, size) => (0..*size as u64) + .map(|_| { + fuzz_param_from_state(param, state.clone()).prop_map(|param| param.into_token()) + }) + .collect::>() + .prop_map(Token::FixedArray) + .boxed(), + ParamType::Tuple(params) => params + .iter() + .map(|p| fuzz_param_from_state(p, state.clone())) + .collect::>() + .prop_map(Token::Tuple) + .boxed(), + } +} diff --git a/evm/src/fuzz/strategies/state.rs b/evm/src/fuzz/strategies/state.rs new file mode 100644 index 0000000000000..88b0878efa922 --- /dev/null +++ b/evm/src/fuzz/strategies/state.rs @@ -0,0 +1,99 @@ +use super::fuzz_param_from_state; +use crate::executor::StateChangeset; +use bytes::Bytes; +use ethers::{ + abi::Function, + types::{H256, U256}, +}; +use proptest::prelude::{BoxedStrategy, Strategy}; +use revm::{opcode, spec_opcode_gas, SpecId}; +use std::{cell::RefCell, collections::HashSet, io::Write, rc::Rc}; + +/// A set of arbitrary 32 byte data from the VM used to generate values for the strategy. +/// +/// Wrapped in a shareable container. +pub type EvmFuzzState = Rc>>; + +/// Given a function and some state, it returns a strategy which generated valid calldata for the +/// given function's input types, based on state taken from the EVM. +pub fn fuzz_calldata_from_state( + func: Function, + state: EvmFuzzState, +) -> BoxedStrategy { + let strats = func + .inputs + .iter() + .map(|input| fuzz_param_from_state(&input.kind, state.clone())) + .collect::>(); + + strats + .prop_map(move |tokens| { + tracing::trace!(input = ?tokens); + func.encode_input(&tokens).unwrap().into() + }) + .no_shrink() + .boxed() +} + +/// Collects state changes from a [StateChangeset] into an [EvmFuzzState]. +pub fn collect_state_from_changeset(state_changeset: &StateChangeset, state: EvmFuzzState) { + let state = &mut *state.borrow_mut(); + + // TODO: Old values + // TODO: Logs + for (address, account) in state_changeset { + // Insert basic account information + state.insert(H256::from(*address).into()); + state.insert(u256_to_h256(account.info.balance).into()); + state.insert(u256_to_h256(U256::from(account.info.nonce)).into()); + + // Insert storage + for (slot, value) in &account.storage { + state.insert(u256_to_h256(*slot).into()); + state.insert(u256_to_h256(*value).into()); + } + + // Insert push bytes + if let Some(code) = &account.info.code { + for push_byte in collect_push_bytes(code.clone()) { + state.insert(push_byte); + } + } + } +} + +/// Collects all push bytes from the given bytecode. +fn collect_push_bytes(code: Bytes) -> Vec<[u8; 32]> { + let mut bytes: Vec<[u8; 32]> = Vec::new(); + + // We use [SpecId::LATEST] since we do not really care what spec it is - we are not interested + // in gas costs. + let opcode_infos = spec_opcode_gas(SpecId::LATEST); + + let mut i = 0; + while i < code.len() { + let op = code[i]; + if opcode_infos[op as usize].is_push { + let push_size = (op - opcode::PUSH1 + 1) as usize; + let push_start = i + 1; + let push_end = push_start + push_size; + + let mut buffer: [u8; 32] = [0; 32]; + let _ = (&mut buffer[..]) + .write(&code[push_start..push_end]) + .expect("push was larger than 32 bytes"); + bytes.push(buffer); + i += push_size; + } + i += 1; + } + + bytes +} + +/// Small helper function to convert [U256] into [H256]. +fn u256_to_h256(u: U256) -> H256 { + let mut h = H256::default(); + u.to_little_endian(h.as_mut()); + h +} From 55500a797ec5ccf254a982b091590047cc218255 Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Sat, 19 Mar 2022 03:15:32 +0100 Subject: [PATCH 2/6] fix: handle malformed bytecode --- evm/src/fuzz/strategies/state.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/evm/src/fuzz/strategies/state.rs b/evm/src/fuzz/strategies/state.rs index 88b0878efa922..45bf5e2ca3c83 100644 --- a/evm/src/fuzz/strategies/state.rs +++ b/evm/src/fuzz/strategies/state.rs @@ -78,6 +78,12 @@ fn collect_push_bytes(code: Bytes) -> Vec<[u8; 32]> { let push_start = i + 1; let push_end = push_start + push_size; + // As a precaution, if a fuzz test deploys malformed bytecode (such as using `CREATE2`) + // this will terminate the loop early. + if push_start > code.len() || push_end > code.len() { + return bytes + } + let mut buffer: [u8; 32] = [0; 32]; let _ = (&mut buffer[..]) .write(&code[push_start..push_end]) From 35a362c7f6162334ba5a62600c0c090779aefe44 Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Sun, 20 Mar 2022 20:14:52 +0100 Subject: [PATCH 3/6] fix: limit search for push bytes --- evm/src/fuzz/strategies/state.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/evm/src/fuzz/strategies/state.rs b/evm/src/fuzz/strategies/state.rs index 45bf5e2ca3c83..52b86f4a1c6ae 100644 --- a/evm/src/fuzz/strategies/state.rs +++ b/evm/src/fuzz/strategies/state.rs @@ -62,6 +62,12 @@ pub fn collect_state_from_changeset(state_changeset: &StateChangeset, state: Evm } } +/// The maximum number of bytes we will look at in bytecodes to find push bytes (24 KiB). +/// +/// This is to limit the performance impact of fuzz tests that might deploy arbitrarily sized +/// bytecode (as is the case with Solmate). +const PUSH_BYTE_ANALYSIS_LIMIT: usize = 24 * 1024; + /// Collects all push bytes from the given bytecode. fn collect_push_bytes(code: Bytes) -> Vec<[u8; 32]> { let mut bytes: Vec<[u8; 32]> = Vec::new(); @@ -71,7 +77,7 @@ fn collect_push_bytes(code: Bytes) -> Vec<[u8; 32]> { let opcode_infos = spec_opcode_gas(SpecId::LATEST); let mut i = 0; - while i < code.len() { + while i < code.len().min(PUSH_BYTE_ANALYSIS_LIMIT) { let op = code[i]; if opcode_infos[op as usize].is_push { let push_size = (op - opcode::PUSH1 + 1) as usize; From 81ed3a22f304a477483ec801ee0b1270623dbcf1 Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Sun, 20 Mar 2022 20:21:03 +0100 Subject: [PATCH 4/6] feat: collect fuzz state from logs --- evm/src/fuzz/mod.rs | 6 ++---- evm/src/fuzz/strategies/mod.rs | 2 +- evm/src/fuzz/strategies/state.rs | 25 +++++++++++++++++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/evm/src/fuzz/mod.rs b/evm/src/fuzz/mod.rs index feff1a7977ba7..fe41c0c6dd06e 100644 --- a/evm/src/fuzz/mod.rs +++ b/evm/src/fuzz/mod.rs @@ -14,9 +14,7 @@ use proptest::test_runner::{TestCaseError, TestError, TestRunner}; use revm::db::DatabaseRef; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, collections::BTreeMap, fmt}; -use strategies::{ - collect_state_from_changeset, fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState, -}; +use strategies::{collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState}; /// Magic return code for the `assume` cheatcode pub const ASSUME_MAGIC_RETURN_CODE: &[u8] = "FOUNDRY::ASSUME".as_bytes(); @@ -80,7 +78,7 @@ where let call = call.borrow(); // Build fuzzer state - collect_state_from_changeset(&call.state_changeset, state.clone()); + collect_state_from_call(&call.logs, &call.state_changeset, state.clone()); // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME" if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE { diff --git a/evm/src/fuzz/strategies/mod.rs b/evm/src/fuzz/strategies/mod.rs index 735a9cf141013..cbfabf7a7a190 100644 --- a/evm/src/fuzz/strategies/mod.rs +++ b/evm/src/fuzz/strategies/mod.rs @@ -8,4 +8,4 @@ mod calldata; pub use calldata::fuzz_calldata; mod state; -pub use state::{collect_state_from_changeset, fuzz_calldata_from_state, EvmFuzzState}; +pub use state::{collect_state_from_call, fuzz_calldata_from_state, EvmFuzzState}; diff --git a/evm/src/fuzz/strategies/state.rs b/evm/src/fuzz/strategies/state.rs index 52b86f4a1c6ae..caddaa0678b80 100644 --- a/evm/src/fuzz/strategies/state.rs +++ b/evm/src/fuzz/strategies/state.rs @@ -2,7 +2,7 @@ use super::fuzz_param_from_state; use crate::executor::StateChangeset; use bytes::Bytes; use ethers::{ - abi::Function, + abi::{Function, RawLog}, types::{H256, U256}, }; use proptest::prelude::{BoxedStrategy, Strategy}; @@ -35,12 +35,15 @@ pub fn fuzz_calldata_from_state( .boxed() } -/// Collects state changes from a [StateChangeset] into an [EvmFuzzState]. -pub fn collect_state_from_changeset(state_changeset: &StateChangeset, state: EvmFuzzState) { +/// Collects state changes from a [StateChangeset] and logs into an [EvmFuzzState]. +pub fn collect_state_from_call( + logs: &[RawLog], + state_changeset: &StateChangeset, + state: EvmFuzzState, +) { let state = &mut *state.borrow_mut(); // TODO: Old values - // TODO: Logs for (address, account) in state_changeset { // Insert basic account information state.insert(H256::from(*address).into()); @@ -59,6 +62,20 @@ pub fn collect_state_from_changeset(state_changeset: &StateChangeset, state: Evm state.insert(push_byte); } } + + // Insert log topics and data + for log in logs { + log.topics.iter().for_each(|topic| { + state.insert(topic.0); + }); + log.data.chunks(32).for_each(|chunk| { + let mut buffer: [u8; 32] = [0; 32]; + let _ = (&mut buffer[..]) + .write(chunk) + .expect("log data chunk was larger than 32 bytes"); + state.insert(buffer); + }); + } } } From f7ebc026572f9b2c83bd1fafb931299c9d108d97 Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Sun, 20 Mar 2022 21:06:43 +0100 Subject: [PATCH 5/6] feat: build initial fuzz state from db --- evm/src/executor/mod.rs | 2 +- evm/src/fuzz/mod.rs | 7 +++++-- evm/src/fuzz/strategies/mod.rs | 4 +++- evm/src/fuzz/strategies/state.rs | 27 +++++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/evm/src/executor/mod.rs b/evm/src/executor/mod.rs index 62a9f098e9bd3..231d633b58c32 100644 --- a/evm/src/executor/mod.rs +++ b/evm/src/executor/mod.rs @@ -162,7 +162,7 @@ pub struct Executor { // Also, if we stored the VM here we would still need to // take `&mut self` when we are not committing to the database, since // we need to set `evm.env`. - db: CacheDB, + pub(crate) db: CacheDB, env: Env, inspector_config: InspectorStackConfig, } diff --git a/evm/src/fuzz/mod.rs b/evm/src/fuzz/mod.rs index fe41c0c6dd06e..ad358dcbbb133 100644 --- a/evm/src/fuzz/mod.rs +++ b/evm/src/fuzz/mod.rs @@ -14,7 +14,10 @@ use proptest::test_runner::{TestCaseError, TestError, TestRunner}; use revm::db::DatabaseRef; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, collections::BTreeMap, fmt}; -use strategies::{collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState}; +use strategies::{ + build_initial_state, collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state, + EvmFuzzState, +}; /// Magic return code for the `assume` cheatcode pub const ASSUME_MAGIC_RETURN_CODE: &[u8] = "FOUNDRY::ASSUME".as_bytes(); @@ -61,7 +64,7 @@ where let call: RefCell = RefCell::new(Default::default()); // Stores fuzz state for use with [fuzz_calldata_from_state] - let state: EvmFuzzState = EvmFuzzState::default(); + let state: EvmFuzzState = build_initial_state(&self.executor.db); // TODO: We should have a `FuzzerOpts` struct where we can configure the fuzzer. When we // have that, we should add a way to configure strategy weights diff --git a/evm/src/fuzz/strategies/mod.rs b/evm/src/fuzz/strategies/mod.rs index cbfabf7a7a190..4c5d31d6835b4 100644 --- a/evm/src/fuzz/strategies/mod.rs +++ b/evm/src/fuzz/strategies/mod.rs @@ -8,4 +8,6 @@ mod calldata; pub use calldata::fuzz_calldata; mod state; -pub use state::{collect_state_from_call, fuzz_calldata_from_state, EvmFuzzState}; +pub use state::{ + build_initial_state, collect_state_from_call, fuzz_calldata_from_state, EvmFuzzState, +}; diff --git a/evm/src/fuzz/strategies/state.rs b/evm/src/fuzz/strategies/state.rs index caddaa0678b80..2ef824cb832f8 100644 --- a/evm/src/fuzz/strategies/state.rs +++ b/evm/src/fuzz/strategies/state.rs @@ -6,7 +6,10 @@ use ethers::{ types::{H256, U256}, }; use proptest::prelude::{BoxedStrategy, Strategy}; -use revm::{opcode, spec_opcode_gas, SpecId}; +use revm::{ + db::{CacheDB, DatabaseRef}, + opcode, spec_opcode_gas, SpecId, +}; use std::{cell::RefCell, collections::HashSet, io::Write, rc::Rc}; /// A set of arbitrary 32 byte data from the VM used to generate values for the strategy. @@ -35,6 +38,27 @@ pub fn fuzz_calldata_from_state( .boxed() } +/// Builds the initial [EvmFuzzState] from a database. +pub fn build_initial_state(db: &CacheDB) -> EvmFuzzState { + let mut state: HashSet<[u8; 32]> = HashSet::new(); + for (address, storage) in db.storage() { + let info = db.basic(*address); + + // Insert basic account information + state.insert(H256::from(*address).into()); + state.insert(u256_to_h256(info.balance).into()); + state.insert(u256_to_h256(U256::from(info.nonce)).into()); + + // Insert storage + for (slot, value) in storage { + state.insert(u256_to_h256(*slot).into()); + state.insert(u256_to_h256(*value).into()); + } + } + + Rc::new(RefCell::new(state)) +} + /// Collects state changes from a [StateChangeset] and logs into an [EvmFuzzState]. pub fn collect_state_from_call( logs: &[RawLog], @@ -43,7 +67,6 @@ pub fn collect_state_from_call( ) { let state = &mut *state.borrow_mut(); - // TODO: Old values for (address, account) in state_changeset { // Insert basic account information state.insert(H256::from(*address).into()); From d8c60bb92c8de69573c54f12483775828b5e321a Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Sun, 20 Mar 2022 21:58:46 +0100 Subject: [PATCH 6/6] perf: use `Index` instead of `Selector` --- evm/src/fuzz/strategies/param.rs | 81 +++++++++++++------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/evm/src/fuzz/strategies/param.rs b/evm/src/fuzz/strategies/param.rs index ab47a37f3fc8c..e86b15e85c86e 100644 --- a/evm/src/fuzz/strategies/param.rs +++ b/evm/src/fuzz/strategies/param.rs @@ -72,28 +72,30 @@ pub fn fuzz_param(param: &ParamType) -> impl Strategy { /// /// Works with ABI Encoder v2 tuples. pub fn fuzz_param_from_state(param: &ParamType, state: EvmFuzzState) -> BoxedStrategy { - let selectors = any::(); + // These are to comply with lifetime requirements + let state_len = state.borrow().len(); + let s = state.clone(); + + // Select a value from the state + let value = any::() + .prop_map(move |index| index.index(state_len)) + .prop_map(move |index| *s.borrow().iter().nth(index).unwrap()); + + // Convert the value based on the parameter type match param { - ParamType::Address => selectors - .prop_map(move |selector| { - Address::from_slice(&selector.select(&*state.borrow())[12..]).into_token() - }) - .boxed(), - ParamType::Bytes => selectors - .prop_map(move |selector| Bytes::from(*selector.select(&*state.borrow())).into_token()) - .boxed(), + ParamType::Address => { + value.prop_map(move |value| Address::from_slice(&value[12..]).into_token()).boxed() + } + ParamType::Bytes => value.prop_map(move |value| Bytes::from(value).into_token()).boxed(), ParamType::Int(n) => match n / 8 { - 32 => selectors - .prop_map(move |selector| { - I256::from_raw(U256::from(*selector.select(&*state.borrow()))).into_token() - }) - .boxed(), - y @ 1..=31 => selectors - .prop_map(move |selector| { + 32 => { + value.prop_map(move |value| I256::from_raw(U256::from(value)).into_token()).boxed() + } + y @ 1..=31 => value + .prop_map(move |value| { // Generate a uintN in the correct range, then shift it to the range of intN // by subtracting 2^(N-1) - let uint = U256::from(*selector.select(&*state.borrow())) % - U256::from(2usize).pow(U256::from(y * 8)); + let uint = U256::from(value) % U256::from(2usize).pow(U256::from(y * 8)); let max_int_plus1 = U256::from(2usize).pow(U256::from(y * 8 - 1)); let num = I256::from_raw(uint.overflowing_sub(max_int_plus1).0); num.into_token() @@ -102,28 +104,18 @@ pub fn fuzz_param_from_state(param: &ParamType, state: EvmFuzzState) -> BoxedStr _ => panic!("unsupported solidity type int{}", n), }, ParamType::Uint(n) => match n / 8 { - 32 => selectors - .prop_map(move |selector| { - U256::from(*selector.select(&*state.borrow())).into_token() - }) - .boxed(), - y @ 1..=31 => selectors - .prop_map(move |selector| { - (U256::from(*selector.select(&*state.borrow())) % - (U256::from(2usize).pow(U256::from(y * 8)))) - .into_token() + 32 => value.prop_map(move |value| U256::from(value).into_token()).boxed(), + y @ 1..=31 => value + .prop_map(move |value| { + (U256::from(value) % (U256::from(2usize).pow(U256::from(y * 8)))).into_token() }) .boxed(), _ => panic!("unsupported solidity type uint{}", n), }, - ParamType::Bool => selectors - .prop_map(move |selector| Token::Bool(selector.select(&*state.borrow())[31] == 1)) - .boxed(), - ParamType::String => selectors - .prop_map(move |selector| { - Token::String(unsafe { - std::str::from_utf8_unchecked(selector.select(&*state.borrow())).to_string() - }) + ParamType::Bool => value.prop_map(move |value| Token::Bool(value[31] == 1)).boxed(), + ParamType::String => value + .prop_map(move |value| { + Token::String(unsafe { std::str::from_utf8_unchecked(&value[..]).to_string() }) }) .boxed(), ParamType::Array(param) => { @@ -133,20 +125,13 @@ pub fn fuzz_param_from_state(param: &ParamType, state: EvmFuzzState) -> BoxedStr } ParamType::FixedBytes(size) => { let size = *size; - selectors - .prop_map(move |selector| { - let val = selector.select(&*state.borrow())[32 - size..].to_vec(); - Token::FixedBytes(val) - }) + value.prop_map(move |value| Token::FixedBytes(value[32 - size..].to_vec())).boxed() + } + ParamType::FixedArray(param, size) => { + proptest::collection::vec(fuzz_param_from_state(param, state), 0..*size) + .prop_map(Token::FixedArray) .boxed() } - ParamType::FixedArray(param, size) => (0..*size as u64) - .map(|_| { - fuzz_param_from_state(param, state.clone()).prop_map(|param| param.into_token()) - }) - .collect::>() - .prop_map(Token::FixedArray) - .boxed(), ParamType::Tuple(params) => params .iter() .map(|p| fuzz_param_from_state(p, state.clone()))