1use super::{UintStrategy, state::FuzzStateReader};
2use crate::{
3 invariant::SenderFilters,
4 strategies::mutators::{
5 BitMutator, GaussianNoiseMutator, IncrementDecrementMutator, InterestingWordMutator,
6 },
7};
8use alloy_dyn_abi::{DynSolType, DynSolValue, Word};
9use alloy_primitives::{Address, B256, I256, U256};
10use proptest::{prelude::*, strategy::ValueTree, test_runner::TestRunner};
11use rand::{SeedableRng, prelude::IndexedMutRandom, rngs::StdRng};
12use std::mem::replace;
13
14const MAX_ARRAY_LEN: usize = 256;
16
17pub fn fuzz_param(param: &DynSolType) -> BoxedStrategy<DynSolValue> {
21 fuzz_param_inner(param, None)
22}
23
24pub fn fuzz_param_with_fixtures(
40 param: &DynSolType,
41 fixtures: Option<&[DynSolValue]>,
42 name: &str,
43) -> BoxedStrategy<DynSolValue> {
44 fuzz_param_inner(param, fixtures.map(|f| (f, name)))
45}
46
47fn fuzz_param_inner(
48 param: &DynSolType,
49 mut fuzz_fixtures: Option<(&[DynSolValue], &str)>,
50) -> BoxedStrategy<DynSolValue> {
51 if let Some((fixtures, name)) = fuzz_fixtures
52 && !fixtures.iter().all(|f| f.matches(param))
53 {
54 error!("fixtures for {name:?} do not match type {param}");
55 fuzz_fixtures = None;
56 }
57 let fuzz_fixtures = fuzz_fixtures.map(|(f, _)| f);
58
59 let value = || {
60 let default_strategy = DynSolValue::type_strategy(param);
61 if let Some(fixtures) = fuzz_fixtures {
62 proptest::prop_oneof![
63 50 => {
64 let fixtures = fixtures.to_vec();
65 any::<prop::sample::Index>()
66 .prop_map(move |index| index.get(&fixtures).clone())
67 },
68 50 => default_strategy,
69 ]
70 .boxed()
71 } else {
72 default_strategy.boxed()
73 }
74 };
75
76 match *param {
77 DynSolType::Address => value(),
78 DynSolType::Int(n @ 8..=256) => super::IntStrategy::new(n, fuzz_fixtures)
79 .prop_map(move |x| DynSolValue::Int(x, n))
80 .boxed(),
81 DynSolType::Uint(n @ 8..=256) => super::UintStrategy::new(n, fuzz_fixtures)
82 .prop_map(move |x| DynSolValue::Uint(x, n))
83 .boxed(),
84 DynSolType::Function | DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
85 DynSolType::Bytes => value(),
86 DynSolType::FixedBytes(_size @ 1..=32) => value(),
87 DynSolType::String => value()
88 .prop_map(move |value| {
89 DynSolValue::String(
90 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
91 )
92 })
93 .boxed(),
94 DynSolType::Tuple(ref params) => params
95 .iter()
96 .map(|param| fuzz_param_inner(param, None))
97 .collect::<Vec<_>>()
98 .prop_map(DynSolValue::Tuple)
99 .boxed(),
100 DynSolType::FixedArray(ref param, size) => {
101 proptest::collection::vec(fuzz_param_inner(param, None), size)
102 .prop_map(DynSolValue::FixedArray)
103 .boxed()
104 }
105 DynSolType::Array(ref param) => {
106 proptest::collection::vec(fuzz_param_inner(param, None), 0..MAX_ARRAY_LEN)
107 .prop_map(DynSolValue::Array)
108 .boxed()
109 }
110 _ => panic!("unsupported fuzz param type: {param}"),
111 }
112}
113
114pub fn fuzz_param_from_state(
119 param: &DynSolType,
120 state: &impl FuzzStateReader,
121) -> BoxedStrategy<DynSolValue> {
122 let value = || {
124 let state = state.clone();
125 let param = param.clone();
126 any::<(bool, prop::sample::Index)>().prop_map(move |(bias, index)| {
130 state.with_dictionary(|dict| {
131 let values = if bias { dict.samples(¶m) } else { None }
132 .unwrap_or_else(|| dict.values())
133 .as_slice();
134 values[index.index(values.len())]
135 })
136 })
137 };
138
139 match *param {
141 DynSolType::Address => {
142 let deployed_libs = state.deployed_libs().to_vec();
143 value()
144 .prop_map(move |value| {
145 let mut fuzzed_addr = Address::from_word(value);
146 if deployed_libs.contains(&fuzzed_addr) {
147 let mut rng = StdRng::seed_from_u64(0x1337); loop {
155 fuzzed_addr.randomize_with(&mut rng);
156 if !deployed_libs.contains(&fuzzed_addr) {
157 break;
158 }
159 }
160 }
161 DynSolValue::Address(fuzzed_addr)
162 })
163 .boxed()
164 }
165 DynSolType::Function => value()
166 .prop_map(move |value| {
167 DynSolValue::Function(alloy_primitives::Function::from_word(value))
168 })
169 .boxed(),
170 DynSolType::FixedBytes(size @ 1..=32) => value()
171 .prop_map(move |mut v| {
172 v[size..].fill(0);
173 DynSolValue::FixedBytes(B256::from(v), size)
174 })
175 .boxed(),
176 DynSolType::Bool => DynSolValue::type_strategy(param).boxed(),
177 DynSolType::String => {
178 let state = state.clone();
179 (proptest::bool::weighted(0.3), any::<prop::sample::Index>())
180 .prop_flat_map(move |(use_ast, select_index)| {
181 if let Some(value) = state.with_dictionary(|dict| {
182 let ast_strings = dict.ast_strings();
184 if use_ast && !ast_strings.is_empty() {
185 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
186 return Some(DynSolValue::String(s.clone()));
187 }
188 None
189 }) {
190 return Just(value).boxed();
191 }
192
193 DynSolValue::type_strategy(&DynSolType::String)
195 .prop_map(|value| {
196 DynSolValue::String(
197 value.as_str().unwrap().trim().trim_end_matches('\0').to_string(),
198 )
199 })
200 .boxed()
201 })
202 .boxed()
203 }
204 DynSolType::Bytes => {
205 let state_clone = state.clone();
206 (
207 value(),
208 proptest::bool::weighted(0.1),
209 proptest::bool::weighted(0.2),
210 any::<prop::sample::Index>(),
211 )
212 .prop_map(move |(word, use_ast_string, use_ast_bytes, select_index)| {
213 if let Some(value) = state_clone.with_dictionary(|dict| {
214 let ast_strings = dict.ast_strings();
216 if use_ast_string && !ast_strings.is_empty() {
217 let s = &ast_strings.as_slice()[select_index.index(ast_strings.len())];
218 return Some(DynSolValue::Bytes(s.as_bytes().to_vec()));
219 }
220
221 let ast_bytes = dict.ast_bytes();
223 if use_ast_bytes && !ast_bytes.is_empty() {
224 let bytes = &ast_bytes.as_slice()[select_index.index(ast_bytes.len())];
225 return Some(DynSolValue::Bytes(bytes.to_vec()));
226 }
227 None
228 }) {
229 return value;
230 }
231
232 DynSolValue::Bytes(word.0.into())
234 })
235 .boxed()
236 }
237 DynSolType::Int(n @ 8..=256) => match n / 8 {
238 32 => value()
239 .prop_map(move |value| DynSolValue::Int(I256::from_raw(value.into()), 256))
240 .boxed(),
241 1..=31 => value()
242 .prop_map(move |value| {
243 let uint_n = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
245 let sign_bit = U256::from(1) << (n - 1);
247 let num = if uint_n >= sign_bit {
248 let modulus = U256::from(1) << n;
250 I256::from_raw(uint_n.wrapping_sub(modulus))
251 } else {
252 I256::from_raw(uint_n)
254 };
255
256 DynSolValue::Int(num, n)
257 })
258 .boxed(),
259 _ => unreachable!(),
260 },
261 DynSolType::Uint(n @ 8..=256) => match n / 8 {
262 32 => value()
263 .prop_map(move |value| DynSolValue::Uint(U256::from_be_bytes(value.0), 256))
264 .boxed(),
265 1..=31 => value()
266 .prop_map(move |value| {
267 let uint = U256::from_be_bytes(value.0) % U256::from(1).wrapping_shl(n);
268 DynSolValue::Uint(uint, n)
269 })
270 .boxed(),
271 _ => unreachable!(),
272 },
273 DynSolType::Tuple(ref params) => params
274 .iter()
275 .map(|p| fuzz_param_from_state(p, state))
276 .collect::<Vec<_>>()
277 .prop_map(DynSolValue::Tuple)
278 .boxed(),
279 DynSolType::FixedArray(ref param, size) => {
280 proptest::collection::vec(fuzz_param_from_state(param, state), size)
281 .prop_map(DynSolValue::FixedArray)
282 .boxed()
283 }
284 DynSolType::Array(ref param) => {
285 proptest::collection::vec(fuzz_param_from_state(param, state), 0..MAX_ARRAY_LEN)
286 .prop_map(DynSolValue::Array)
287 .boxed()
288 }
289 _ => panic!("unsupported fuzz param type: {param}"),
290 }
291}
292
293fn select_random_address(
300 current: Address,
301 test_runner: &mut TestRunner,
302 state: &impl FuzzStateReader,
303 senders: Option<&SenderFilters>,
304) -> Option<Address> {
305 if let Some(senders) = senders {
306 if !senders.targeted.is_empty() {
307 let index = test_runner.rng().random_range(0..senders.targeted.len());
309 let addr = senders.targeted[index];
310 return (addr != current).then_some(addr);
311 }
312
313 state.with_dictionary(|dict| {
315 let values = dict.values();
316 if values.is_empty() {
317 return None;
318 }
319
320 for _ in 0..10 {
322 let index = test_runner.rng().random_range(0..values.len());
323 let addr = Address::from_word(values[index]);
324 if addr != current && !senders.excluded.contains(&addr) {
325 return Some(addr);
326 }
327 }
328 None
329 })
330 } else {
331 state.with_dictionary(|dict| {
333 let values = dict.values();
334 if values.is_empty() {
335 None
336 } else {
337 let index = test_runner.rng().random_range(0..values.len());
338 let addr = Address::from_word(values[index]);
339 (addr != current).then_some(addr)
340 }
341 })
342 }
343}
344
345pub fn mutate_param_value(
347 param: &DynSolType,
348 value: DynSolValue,
349 test_runner: &mut TestRunner,
350 state: &impl FuzzStateReader,
351) -> DynSolValue {
352 mutate_param_value_inner(param, value, test_runner, state, None)
353}
354
355pub fn mutate_param_value_with_senders(
360 param: &DynSolType,
361 value: DynSolValue,
362 test_runner: &mut TestRunner,
363 state: &impl FuzzStateReader,
364 senders: &SenderFilters,
365) -> DynSolValue {
366 mutate_param_value_inner(param, value, test_runner, state, Some(senders))
367}
368
369fn mutate_param_value_inner(
370 param: &DynSolType,
371 value: DynSolValue,
372 test_runner: &mut TestRunner,
373 state: &impl FuzzStateReader,
374 senders: Option<&SenderFilters>,
375) -> DynSolValue {
376 let new_value = |param: &DynSolType, test_runner: &mut TestRunner| {
377 fuzz_param_from_state(param, state)
378 .new_tree(test_runner)
379 .expect("Could not generate case")
380 .current()
381 };
382
383 match value {
384 DynSolValue::Bool(val) => {
385 trace!(target: "mutator", "Bool flip {val}");
387 Some(DynSolValue::Bool(!val))
388 }
389 DynSolValue::Uint(val, size) => match test_runner.rng().random_range(0..=6) {
390 0 => U256::increment_decrement(val, size, test_runner),
391 1 => U256::flip_random_bit(val, size, test_runner),
392 2 => U256::mutate_interesting_byte(val, size, test_runner),
393 3 => U256::mutate_interesting_word(val, size, test_runner),
394 4 => U256::mutate_interesting_dword(val, size, test_runner),
395 5 => U256::mutate_with_gaussian_noise(val, size, test_runner),
396 6 => None,
397 _ => unreachable!(),
398 }
399 .map(|v| DynSolValue::Uint(v, size)),
400 DynSolValue::Int(val, size) => match test_runner.rng().random_range(0..=6) {
401 0 => I256::increment_decrement(val, size, test_runner),
402 1 => I256::flip_random_bit(val, size, test_runner),
403 2 => I256::mutate_interesting_byte(val, size, test_runner),
404 3 => I256::mutate_interesting_word(val, size, test_runner),
405 4 => I256::mutate_interesting_dword(val, size, test_runner),
406 5 => I256::mutate_with_gaussian_noise(val, size, test_runner),
407 6 => None,
408 _ => unreachable!(),
409 }
410 .map(|v| DynSolValue::Int(v, size)),
411 DynSolValue::Address(val) => match test_runner.rng().random_range(0..=5) {
412 0 => Address::flip_random_bit(val, 20, test_runner),
413 1 => Address::mutate_interesting_byte(val, 20, test_runner),
414 2 => Address::mutate_interesting_word(val, 20, test_runner),
415 3 => Address::mutate_interesting_dword(val, 20, test_runner),
416 4 => select_random_address(val, test_runner, state, senders),
418 5 => None,
419 _ => unreachable!(),
420 }
421 .map(DynSolValue::Address),
422 DynSolValue::Array(mut values) => {
423 if let DynSolType::Array(param_type) = param
424 && !values.is_empty()
425 {
426 match test_runner.rng().random_range(0..=2) {
427 0 => {
429 values.remove(test_runner.rng().random_range(0..values.len()));
430 }
431 1 => values.push(new_value(param_type, test_runner)),
433 2 => mutate_random_array_value(
435 &mut values,
436 param_type,
437 test_runner,
438 state,
439 senders,
440 ),
441 _ => unreachable!(),
442 }
443 Some(DynSolValue::Array(values))
444 } else {
445 None
446 }
447 }
448 DynSolValue::FixedArray(mut values) => {
449 if let DynSolType::FixedArray(param_type, _size) = param
450 && !values.is_empty()
451 {
452 mutate_random_array_value(&mut values, param_type, test_runner, state, senders);
453 Some(DynSolValue::FixedArray(values))
454 } else {
455 None
456 }
457 }
458 DynSolValue::FixedBytes(word, size) => match test_runner.rng().random_range(0..=4) {
459 0 => Word::flip_random_bit(word, size, test_runner),
460 1 => Word::mutate_interesting_byte(word, size, test_runner),
461 2 => Word::mutate_interesting_word(word, size, test_runner),
462 3 => Word::mutate_interesting_dword(word, size, test_runner),
463 4 => None,
464 _ => unreachable!(),
465 }
466 .map(|word| DynSolValue::FixedBytes(word, size)),
467 DynSolValue::CustomStruct { name, prop_names, tuple: mut values } => {
468 if let DynSolType::CustomStruct { name: _, prop_names: _, tuple: tuple_types }
469 | DynSolType::Tuple(tuple_types) = param
470 && !values.is_empty()
471 {
472 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state, senders);
474 Some(DynSolValue::CustomStruct { name, prop_names, tuple: values })
475 } else {
476 None
477 }
478 }
479 DynSolValue::Tuple(mut values) => {
480 if let DynSolType::Tuple(tuple_types) = param
481 && !values.is_empty()
482 {
483 mutate_random_tuple_value(&mut values, tuple_types, test_runner, state, senders);
485 Some(DynSolValue::Tuple(values))
486 } else {
487 None
488 }
489 }
490 _ => None,
491 }
492 .unwrap_or_else(|| new_value(param, test_runner))
493}
494
495fn mutate_random_tuple_value(
497 tuple_values: &mut [DynSolValue],
498 tuple_types: &[DynSolType],
499 test_runner: &mut TestRunner,
500 state: &impl FuzzStateReader,
501 senders: Option<&SenderFilters>,
502) {
503 let id = test_runner.rng().random_range(0..tuple_values.len());
504 let param_type = &tuple_types[id];
505 let old_val = replace(&mut tuple_values[id], DynSolValue::Bool(false));
506 let new_val = mutate_param_value_inner(param_type, old_val, test_runner, state, senders);
507 tuple_values[id] = new_val;
508}
509
510fn mutate_random_array_value(
512 array_values: &mut [DynSolValue],
513 element_type: &DynSolType,
514 test_runner: &mut TestRunner,
515 state: &impl FuzzStateReader,
516 senders: Option<&SenderFilters>,
517) {
518 let elem = array_values.choose_mut(&mut test_runner.rng()).unwrap();
519 let old_val = replace(elem, DynSolValue::Bool(false));
520 let new_val = mutate_param_value_inner(element_type, old_val, test_runner, state, senders);
521 *elem = new_val;
522}
523
524const PAYABLE_VALUE_PROB: u32 = 15;
526
527pub fn fuzz_msg_value() -> impl Strategy<Value = Option<U256>> {
533 proptest::prop_oneof![
534 100 - PAYABLE_VALUE_PROB => proptest::strategy::Just(None),
535 PAYABLE_VALUE_PROB => UintStrategy::new(256, None).prop_map(Some),
536 ]
537}
538
539pub fn generate_msg_value(test_runner: &mut TestRunner) -> U256 {
545 UintStrategy::new(256, None)
546 .new_tree(test_runner)
547 .expect("UintStrategy::new_tree is infallible")
548 .current()
549}
550
551#[cfg(test)]
552mod tests {
553 use crate::{
554 FuzzFixtures,
555 strategies::{EvmFuzzState, fuzz_calldata, fuzz_calldata_from_state},
556 };
557 use alloy_primitives::B256;
558 use foundry_common::abi::get_func;
559 use std::collections::HashSet;
560
561 #[test]
562 fn can_fuzz_array() {
563 let f = "testArray(uint64[2] calldata values)";
564 let func = get_func(f).unwrap();
565 let state = EvmFuzzState::test();
566 let strategy = proptest::prop_oneof![
567 60 => fuzz_calldata(func.clone(), &FuzzFixtures::default()),
568 40 => fuzz_calldata_from_state(func, &state),
569 ];
570 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
571 let mut runner = proptest::test_runner::TestRunner::new(cfg);
572 let _ = runner.run(&strategy, |_| Ok(()));
573 }
574
575 #[test]
576 fn can_fuzz_string_and_bytes_with_ast_literals_and_hashes() {
577 use super::fuzz_param_from_state;
578 use crate::strategies::LiteralMaps;
579 use alloy_dyn_abi::DynSolType;
580 use alloy_primitives::keccak256;
581 use proptest::strategy::Strategy;
582
583 let mut literals = LiteralMaps::default();
585 literals.strings.insert("hello".to_string());
586 literals.strings.insert("world".to_string());
587 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("hello"));
588 literals.words.entry(DynSolType::FixedBytes(32)).or_default().insert(keccak256("world"));
589
590 let mut state = EvmFuzzState::test();
591 state.seed_literals(literals);
592
593 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
594 let mut runner = proptest::test_runner::TestRunner::new(cfg);
595
596 let mut generated_bytes = HashSet::new();
598 let mut generated_hashes = HashSet::new();
599 let mut generated_strings = HashSet::new();
600 let bytes_strategy = fuzz_param_from_state(&DynSolType::Bytes, &state);
601 let string_strategy = fuzz_param_from_state(&DynSolType::String, &state);
602 let bytes32_strategy = fuzz_param_from_state(&DynSolType::FixedBytes(32), &state);
603
604 for _ in 0..256 {
605 let tree = bytes_strategy.new_tree(&mut runner).unwrap();
606 if let Some(bytes) = tree.current().as_bytes()
607 && let Ok(s) = std::str::from_utf8(bytes)
608 {
609 generated_bytes.insert(s.to_string());
610 }
611
612 let tree = string_strategy.new_tree(&mut runner).unwrap();
613 if let Some(s) = tree.current().as_str() {
614 generated_strings.insert(s.to_string());
615 }
616
617 let tree = bytes32_strategy.new_tree(&mut runner).unwrap();
618 if let Some((bytes, size)) = tree.current().as_fixed_bytes()
619 && size == 32
620 {
621 generated_hashes.insert(B256::from_slice(bytes));
622 }
623 }
624
625 assert!(generated_bytes.contains("hello"));
626 assert!(generated_bytes.contains("world"));
627 assert!(generated_strings.contains("hello"));
628 assert!(generated_strings.contains("world"));
629 assert!(generated_hashes.contains(&keccak256("hello")));
630 assert!(generated_hashes.contains(&keccak256("world")));
631 }
632
633 #[test]
634 fn mutate_address_can_select_from_dictionary() {
635 use super::mutate_param_value;
636 use alloy_dyn_abi::{DynSolType, DynSolValue};
637 use alloy_primitives::Address;
638
639 let mut state = EvmFuzzState::test();
640
641 let addr1 = Address::repeat_byte(0x11);
643 let addr2 = Address::repeat_byte(0x22);
644 let addr3 = Address::repeat_byte(0x33);
645 state.collect_values([addr1.into_word(), addr2.into_word(), addr3.into_word()]);
646
647 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
648 let mut runner = proptest::test_runner::TestRunner::new(cfg);
649
650 let original = Address::repeat_byte(0xff);
652 let mut got_addr1 = false;
653 let mut got_addr2 = false;
654 let mut got_addr3 = false;
655
656 for _ in 0..1000 {
657 let mutated = mutate_param_value(
658 &DynSolType::Address,
659 DynSolValue::Address(original),
660 &mut runner,
661 &state,
662 );
663 if let DynSolValue::Address(addr) = mutated {
664 if addr == addr1 {
665 got_addr1 = true;
666 }
667 if addr == addr2 {
668 got_addr2 = true;
669 }
670 if addr == addr3 {
671 got_addr3 = true;
672 }
673 }
674 if got_addr1 && got_addr2 && got_addr3 {
675 break;
676 }
677 }
678
679 assert!(
681 got_addr1 || got_addr2 || got_addr3,
682 "Address mutation should select addresses from dictionary"
683 );
684 }
685
686 #[test]
687 fn mutate_address_prefers_targeted_senders() {
688 use super::select_random_address;
689 use crate::invariant::SenderFilters;
690 use alloy_primitives::Address;
691
692 let mut state = EvmFuzzState::test();
693
694 let dict_addr = Address::repeat_byte(0xdd);
696 state.collect_values([dict_addr.into_word()]);
697
698 let targeted1 = Address::repeat_byte(0x11);
700 let targeted2 = Address::repeat_byte(0x22);
701 let senders = SenderFilters::new(vec![targeted1, targeted2], vec![]);
702
703 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
704 let mut runner = proptest::test_runner::TestRunner::new(cfg);
705
706 let original = Address::repeat_byte(0xff);
708 let mut got_targeted1 = false;
709 let mut got_targeted2 = false;
710 let mut got_dict = false;
711
712 for _ in 0..100 {
713 if let Some(addr) = select_random_address(original, &mut runner, &state, Some(&senders))
714 {
715 if addr == targeted1 {
716 got_targeted1 = true;
717 }
718 if addr == targeted2 {
719 got_targeted2 = true;
720 }
721 if addr == dict_addr {
722 got_dict = true;
723 }
724 }
725 }
726
727 assert!(
729 got_targeted1 || got_targeted2,
730 "select_random_address should select from targeted senders"
731 );
732 assert!(
733 !got_dict,
734 "select_random_address should not select from dictionary when targeted senders are set"
735 );
736 }
737
738 #[test]
739 fn mutate_address_respects_excluded_senders() {
740 use super::select_random_address;
741 use crate::invariant::SenderFilters;
742 use alloy_primitives::Address;
743
744 let mut state = EvmFuzzState::test();
745
746 let addr1 = Address::repeat_byte(0x11);
748 let addr2 = Address::repeat_byte(0x22);
749 let excluded_addr = Address::repeat_byte(0xee);
750 state.collect_values([addr1.into_word(), addr2.into_word(), excluded_addr.into_word()]);
751
752 let senders = SenderFilters::new(vec![], vec![excluded_addr]);
754
755 let cfg = proptest::test_runner::Config { failure_persistence: None, ..Default::default() };
756 let mut runner = proptest::test_runner::TestRunner::new(cfg);
757
758 let original = Address::repeat_byte(0xff);
760 let mut got_excluded = false;
761 let mut got_valid = false;
762
763 for _ in 0..100 {
764 if let Some(addr) = select_random_address(original, &mut runner, &state, Some(&senders))
765 {
766 if addr == excluded_addr {
767 got_excluded = true;
768 break;
769 }
770 if addr == addr1 || addr == addr2 {
771 got_valid = true;
772 }
773 }
774 }
775
776 assert!(!got_excluded, "select_random_address should not select excluded addresses");
777 assert!(got_valid, "select_random_address should select valid (non-excluded) addresses");
778 }
779}