-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(fuzz): ast-seeded dictionary #12015
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
Changes from 16 commits
7f0d2ba
2809ba5
9bea99a
83542a4
2a6c8c4
738a7f6
951e0cd
07a5dca
7316df5
dc063ca
fc4f3d4
3440429
5ea2f8d
bf49a49
6477f11
58e45f9
e4e5b4d
6aaa729
cba4f57
b56e2ae
c7ff115
935c435
bc9504e
7ed7476
9a05ffb
5813a5a
e7d3159
544d7ff
7907e97
a0fe257
172b63a
8f854be
aa48772
587038c
c212567
6475317
959314f
34d280e
4d9ee25
a4d0c1c
15856c8
5e7847b
a65342c
4094e81
5f80af8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -170,27 +170,74 @@ pub fn fuzz_param_from_state( | |
| }) | ||
| .boxed(), | ||
| DynSolType::Bool => DynSolValue::type_strategy(param).boxed(), | ||
| DynSolType::String => DynSolValue::type_strategy(param) | ||
| .prop_map(move |value| { | ||
| DynSolValue::String( | ||
| value.as_str().unwrap().trim().trim_end_matches('\0').to_string(), | ||
| ) | ||
| }) | ||
| .boxed(), | ||
| DynSolType::String => { | ||
| let state_clone = state.clone(); | ||
0xrusowsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| (any::<prop::sample::Index>(), any::<prop::sample::Index>()) | ||
| .prop_flat_map(move |(use_ast_index, select_index)| { | ||
|
||
| let dict = state_clone.dictionary_read(); | ||
|
|
||
| // AST string literals available: use 30/70 allocation | ||
0xrusowsky marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let ast_strings = dict.ast_strings(); | ||
| if !ast_strings.is_empty() && use_ast_index.index(10) < 3 { | ||
| let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())]; | ||
| return Just(DynSolValue::String(s.clone())).boxed(); | ||
| } | ||
|
|
||
| // Fallback to random string generation | ||
| DynSolValue::type_strategy(&DynSolType::String) | ||
| .prop_map(|value| { | ||
| DynSolValue::String( | ||
| value.as_str().unwrap().trim().trim_end_matches('\0').to_string(), | ||
| ) | ||
| }) | ||
| .boxed() | ||
| }) | ||
| .boxed() | ||
| } | ||
| DynSolType::Bytes => { | ||
| value().prop_map(move |value| DynSolValue::Bytes(value.0.into())).boxed() | ||
| let state_clone = state.clone(); | ||
| (value(), any::<prop::sample::Index>(), any::<prop::sample::Index>()) | ||
| .prop_map(move |(word, use_ast_index, select_index)| { | ||
| let dict = state_clone.dictionary_read(); | ||
|
|
||
| // Try string literals as bytes (10% chance) | ||
| let ast_strings = dict.ast_strings(); | ||
| if !ast_strings.is_empty() && use_ast_index.index(10) < 1 { | ||
| let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())]; | ||
| return DynSolValue::Bytes(s.as_bytes().to_vec()); | ||
| } | ||
|
|
||
| // Prefer hex literals (20% chance) | ||
| let ast_bytes = dict.ast_bytes(); | ||
| if !ast_bytes.is_empty() && use_ast_index.index(10) < 3 { | ||
| let bytes = &ast_bytes.as_slice()[select_index.index(ast_bytes.len())]; | ||
| return DynSolValue::Bytes(bytes.to_vec()); | ||
| } | ||
|
|
||
| // Fallback to the generated word from the dictionary (70% chance) | ||
| DynSolValue::Bytes(word.0.into()) | ||
| }) | ||
| .boxed() | ||
| } | ||
| DynSolType::Int(n @ 8..=256) => match n / 8 { | ||
| 32 => value() | ||
| .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256)) | ||
| .boxed(), | ||
| 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_be_bytes(value.0) % U256::from(1).wrapping_shl(n); | ||
| let max_int_plus1 = U256::from(1).wrapping_shl(n - 1); | ||
| let num = I256::from_raw(uint.wrapping_sub(max_int_plus1)); | ||
| // Extract lower N bits | ||
| let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n); | ||
grandizzy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Interpret as signed int (two's complement) --> check sign bit (bit N-1). | ||
| let sign_bit = U256::from(1) << (n - 1); | ||
| let num = if uint_n >= sign_bit { | ||
| // Negative number in two's complement | ||
| let modulus = U256::from(1) << n; | ||
| I256::from_raw(uint_n.wrapping_sub(modulus)) | ||
| } else { | ||
| // Positive number | ||
| I256::from_raw(uint_n) | ||
| }; | ||
|
|
||
| DynSolValue::Int(num, n) | ||
| }) | ||
| .boxed(), | ||
|
|
@@ -379,16 +426,18 @@ mod tests { | |
| FuzzFixtures, | ||
| strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state}, | ||
| }; | ||
| use alloy_primitives::B256; | ||
| use foundry_common::abi::get_func; | ||
| use foundry_config::FuzzDictionaryConfig; | ||
| use revm::database::{CacheDB, EmptyDB}; | ||
| use std::collections::HashSet; | ||
|
|
||
| #[test] | ||
| fn can_fuzz_array() { | ||
| let f = "testArray(uint64[2] calldata values)"; | ||
| let func = get_func(f).unwrap(); | ||
| let db = CacheDB::new(EmptyDB::default()); | ||
| let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[]); | ||
| let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None); | ||
| let strategy = proptest::prop_oneof![ | ||
| 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()), | ||
| 40 => fuzz_calldata_from_state(func, &state), | ||
|
|
@@ -397,4 +446,63 @@ mod tests { | |
| let mut runner = proptest::test_runner::TestRunner::new(cfg); | ||
| let _ = runner.run(&strategy, |_| Ok(())); | ||
| } | ||
|
|
||
| #[test] | ||
| fn can_fuzz_string_and_bytes_with_ast_literals_and_hashes() { | ||
| use super::fuzz_param_from_state; | ||
| use crate::strategies::state::LiteralMaps; | ||
| use alloy_dyn_abi::DynSolType; | ||
| use alloy_primitives::keccak256; | ||
| use proptest::strategy::Strategy; | ||
|
|
||
| // Seed dict with string values and their hashes --> mimic `CheatcodeAnalysis` behavior. | ||
| let mut literals = LiteralMaps::default(); | ||
| literals.strings.insert("hello".to_string()); | ||
| literals.strings.insert("world".to_string()); | ||
| literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello")); | ||
| literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world")); | ||
|
|
||
| let db = CacheDB::new(EmptyDB::default()); | ||
| let state = EvmFuzzState::new(&db, FuzzDictionaryConfig::default(), &[], None, None); | ||
| state.seed_literals(literals); | ||
|
|
||
| let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() }; | ||
| let mut runner = proptest::test_runner::TestRunner::new(cfg); | ||
|
|
||
| // Verify strategies generates the seeded AST literals | ||
| let mut generated_bytes = HashSet::new(); | ||
| let mut generated_hashes = HashSet::new(); | ||
| let mut generated_strings = HashSet::new(); | ||
| let bytes_strategy = fuzz_param_from_state(&DynSolType::Bytes, &state); | ||
| let string_strategy = fuzz_param_from_state(&DynSolType::String, &state); | ||
| let bytes32_strategy = fuzz_param_from_state(&DynSolType::FixedBytes(32), &state); | ||
|
|
||
| for _ in 0..256 { | ||
| let tree = bytes_strategy.new_tree(&mut runner).unwrap(); | ||
| if let Some(bytes) = tree.current().as_bytes() | ||
| && let Ok(s) = std::str::from_utf8(bytes) | ||
| { | ||
| generated_bytes.insert(s.to_string()); | ||
| } | ||
|
|
||
| let tree = string_strategy.new_tree(&mut runner).unwrap(); | ||
| if let Some(s) = tree.current().as_str() { | ||
| generated_strings.insert(s.to_string()); | ||
| } | ||
|
|
||
| let tree = bytes32_strategy.new_tree(&mut runner).unwrap(); | ||
| if let Some((bytes, size)) = tree.current().as_fixed_bytes() | ||
| && size == 32 | ||
| { | ||
| generated_hashes.insert(B256::from_slice(bytes)); | ||
| } | ||
| } | ||
|
|
||
| assert!(generated_bytes.contains("hello")); | ||
| assert!(generated_bytes.contains("world")); | ||
| assert!(generated_strings.contains("hello")); | ||
| assert!(generated_strings.contains("world")); | ||
| assert!(generated_hashes.contains(&keccak256("hello"))); | ||
| assert!(generated_hashes.contains(&keccak256("world"))); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.