Strategies
Often in code, we operate on some assumptions about the possible values of variables. For example, we can perform greater than zero validations early in the code, and we can assume that variables are greater than zero in the subsequent code. Fuzzer allows providing such assumptions to avoid testing against values which are not covered by the code.
Cheatcode given
,
instructs the fuzzer to adopt a specific fuzzing strategy for input parameters.
Such strategies are provided declaratively, by assigning strategies to the input parameters as on the example below.
@external
func setup_integers() {
%{
given(
a = strategy.integers(10, 20),
b = strategy.integers(30, 40),
)
%}
return ();
}
@external
func test_integers{syscall_ptr : felt*, range_check_ptr}(a : felt, b : felt) {
assert_le(a, b);
return ();
}
This document is a guide to what strategies are available for generating examples and how to build them.
Core strategies
All core strategies are contained in the strategy
cheatcode-namespace.
strategy.felts
def felts(*, rc_bound: bool = False) -> Strategy: ...
By default, explores all possible felt values.
If keyword argument rc_bound
is True
, explores felts which can be passed to the range_check
builtin.
This narrows the range of explored values according to the parameters of Cairo runtime.
Use this functionality, if fuzzed values will be passed to any of the assert_*
functions from
starkware.cairo.common.math
module.
strategy.integers
def integers(
min_value: Optional[int] = None,
max_value: Optional[int] = None,
) -> Strategy: ...
Generates integer values, possibly bounded by provided range.
strategy.integers(0, 100)
strategy.integers(max_value=3000)
strategy.integers(11)
Fuzzer picks integers from provided range and then converts them to felts.
If min_value
is not None
then all values will be greater than or equal to min_value
,
and if max_value
is not None
then all values will be less than or equal to max_value
.
strategy.short_strings
def short_strings() -> Strategy: ...
Generates strings with ASCII characters of length that passes the condition 0 <= length <= 31
.
Max size 31
comes from the docs.
strategy.uint256
def uint256() -> Strategy: ...
Generates UInt256's. They can be used by map
and filter
as (named) tuples.
strategy.uint256().map(lambda x: (x.low // 30, x.high)).filter(lambda x: x[1] > 0)
Adapting strategies
Often it is the case that a strategy does not produce exactly what is desired, and it is necessary to further
adapt the generated values.
Although this could be done in tests directly, this hurts because fuzzer does not know about the adaptation and may repeatedly test the same values.
The assume
and reject
cheatcodes provide simple interfaces to adapt an advanced strategy.
Those are not very good considering the performance.
Fuzzer can execute tests on rejected data anyway and will just ignore failure when it happens.
Protostar provides ways to build strategies by transforming other ones.
Mapping
class Strategy:
def map(self, mapping_function: Callable[[int], int]) -> Strategy: ...
Applies provided mapping function to all searched inputs values.
strategy.felts().map(lambda x: x // 2)
Filtering
class Strategy:
def filter(self, filter_function: Callable[[int], bool]) -> Strategy: ...
Rejects examples not matching a condition.
strategy.felts().filter(lambda x: x not in [3, 5, 8])
The outcome is similar to using the assume
or reject
cheatcodes, but filter
does not
require executing tested Cairo function and thus is more performant.
Try to use filter
only to avoid unwanted corner cases rather than attempting to cut out a large
portion of the searched input values.
Fuzzer draws random data from the original strategy and only afterwards checks if it passes filter conditions. If too many variables are restricted, fuzzer will reject test execution.
Combining
def one_of(*strategies: Strategy) -> Strategy: ...
Returns a strategy which generates values from any of the argument strategies.
strategy.one_of(
strategy.integers(0, 100),
strategy.integers(1000, 1200),
)