-
Notifications
You must be signed in to change notification settings - Fork 2.4k
REVM fuzz dictionary #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
REVM fuzz dictionary #985
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ae3c3af
feat: fuzz dictionary
onbjerg 55500a7
fix: handle malformed bytecode
onbjerg 35a362c
fix: limit search for push bytes
onbjerg 81ed3a2
feat: collect fuzz state from logs
onbjerg f7ebc02
feat: build initial fuzz state from db
onbjerg d8c60bb
perf: use `Index` instead of `Selector`
onbjerg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Value = Bytes> + '_ { | ||
| pub fn fuzz_calldata(func: Function) -> BoxedStrategy<Bytes> { | ||
| // 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::<Vec<_>>(); | ||
|
|
||
| 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() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| use super::fuzz_param_from_state; | ||
| use crate::executor::StateChangeset; | ||
| use bytes::Bytes; | ||
| use ethers::{ | ||
| abi::{Function, RawLog}, | ||
| types::{H256, U256}, | ||
| }; | ||
| use proptest::prelude::{BoxedStrategy, Strategy}; | ||
| 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. | ||
| /// | ||
| /// Wrapped in a shareable container. | ||
| pub type EvmFuzzState = Rc<RefCell<HashSet<[u8; 32]>>>; | ||
|
|
||
| /// 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<ethers::types::Bytes> { | ||
| let strats = func | ||
| .inputs | ||
| .iter() | ||
| .map(|input| fuzz_param_from_state(&input.kind, state.clone())) | ||
| .collect::<Vec<_>>(); | ||
|
|
||
| strats | ||
| .prop_map(move |tokens| { | ||
| tracing::trace!(input = ?tokens); | ||
| func.encode_input(&tokens).unwrap().into() | ||
| }) | ||
| .no_shrink() | ||
| .boxed() | ||
| } | ||
|
|
||
| /// Builds the initial [EvmFuzzState] from a database. | ||
| pub fn build_initial_state<DB: DatabaseRef>(db: &CacheDB<DB>) -> 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], | ||
| state_changeset: &StateChangeset, | ||
| state: EvmFuzzState, | ||
| ) { | ||
| let state = &mut *state.borrow_mut(); | ||
|
|
||
| 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()); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it even make sense that we save the nonce? |
||
|
|
||
| // 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); | ||
| } | ||
| } | ||
|
|
||
| // 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); | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// 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]> { | ||
gakonst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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().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; | ||
| 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]) | ||
| .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 | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decided to not make this configurable yet since we're probably going to change the signature of
FuzzedExecutor::newwhen we introduce aFuzzerOptsstruct