Skip to content

Commit

Permalink
Add support for optimization mode (#75)
Browse files Browse the repository at this point in the history
integrate optimization mode into fuzzer
  • Loading branch information
tarunbhm authored Jun 21, 2023
1 parent fca903b commit c444be9
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 10 deletions.
12 changes: 12 additions & 0 deletions fuzzing/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ type TestingConfig struct {

// PropertyTesting describes the configuration used for property testing.
PropertyTesting PropertyTestConfig `json:"propertyTesting"`

// OptimizationTesting describes the configuration used for optimization testing.
OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"`
}

// AssertionTestingConfig describes the configuration options used for assertion testing
Expand All @@ -124,6 +127,15 @@ type PropertyTestConfig struct {
TestPrefixes []string `json:"testPrefixes"`
}

// OptimizationTestingConfig describes the configuration options used for optimization testing
type OptimizationTestingConfig struct {
// Enabled describes whether testing is enabled.
Enabled bool `json:"enabled"`

// TestPrefixes dictates what method name prefixes will determine if a contract method is an optimization test.
TestPrefixes []string `json:"testPrefixes"`
}

// ReadProjectConfigFromFile reads a JSON-serialized ProjectConfig from a provided file path.
// Returns the ProjectConfig if it succeeds, or an error if one occurs.
func ReadProjectConfigFromFile(path string) (*ProjectConfig, error) {
Expand Down
6 changes: 6 additions & 0 deletions fuzzing/config/config_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) {
"fuzz_",
},
},
OptimizationTesting: OptimizationTestingConfig{
Enabled: false,
TestPrefixes: []string{
"optimize_",
},
},
},
TestChainConfig: *chainConfig,
},
Expand Down
5 changes: 5 additions & 0 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) {
if fuzzer.config.Fuzzing.Testing.AssertionTesting.Enabled {
attachAssertionTestCaseProvider(fuzzer)
}
if fuzzer.config.Fuzzing.Testing.OptimizationTesting.Enabled {
// TODO: Make this is a warning in the logging PR
fmt.Printf("warning: currently optimization mode's call sequence shrinking is inefficient. this may lead to minor performance issues")
attachOptimizationTestCaseProvider(fuzzer)
}
return fuzzer, nil
}

Expand Down
34 changes: 34 additions & 0 deletions fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/crytic/medusa/fuzzing/calls"
"github.com/crytic/medusa/fuzzing/valuegeneration"
"github.com/crytic/medusa/utils"
"math/big"
"math/rand"
"testing"

Expand Down Expand Up @@ -127,6 +128,39 @@ func TestAssertionsAndProperties(t *testing.T) {
})
}

// TestOptimizationsSolving runs a test to ensure that optimization mode works as expected
func TestOptimizationsSolving(t *testing.T) {
filePaths := []string{
"testdata/contracts/optimizations/optimize.sol",
}
for _, filePath := range filePaths {
runFuzzerTest(t, &fuzzerSolcFileTest{
filePath: filePath,
configUpdates: func(config *config.ProjectConfig) {
config.Fuzzing.DeploymentOrder = []string{"TestContract"}
config.Fuzzing.Testing.PropertyTesting.Enabled = false
config.Fuzzing.Testing.AssertionTesting.Enabled = false
config.Fuzzing.Testing.OptimizationTesting.Enabled = true
config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly.
},
method: func(f *fuzzerTestContext) {
// Start the fuzzer
err := f.fuzzer.Start()
assert.NoError(t, err)

// Check the value found for optimization test
var testCases = f.fuzzer.TestCasesWithStatus(TestCaseStatusPassed)
switch v := testCases[0].(type) {
case *OptimizationTestCase:
assert.EqualValues(t, v.Value().Cmp(big.NewInt(4241)), 0)
default:
t.Errorf("invalid test case found %T", v)
}
},
})
}
}

// TestChainBehaviour runs tests to ensure the chain behaves as expected.
func TestChainBehaviour(t *testing.T) {
// Run a test to simulate out of gas errors to make sure its handled well by the Chain and does not panic.
Expand Down
10 changes: 7 additions & 3 deletions fuzzing/test_case_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ import (

// AssertionTestCase describes a test being run by a AssertionTestCaseProvider.
type AssertionTestCase struct {
status TestCaseStatus
// status describes the status of the test case
status TestCaseStatus
// targetContract describes the target contract where the test case was found
targetContract *fuzzerTypes.Contract
targetMethod abi.Method
callSequence *calls.CallSequence
// targetMethod describes the target method for the test case
targetMethod abi.Method
// callSequence describes the call sequence that broke the assertion
callSequence *calls.CallSequence
}

// Status describes the TestCaseStatus used to define the current state of the test.
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/test_case_assertion_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (t *AssertionTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent)
return nil
}

// onFuzzerStarting is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers
// onFuzzerStopping is the event handler triggered when the Fuzzer is stopping the fuzzing campaign and all workers
// have been destroyed. It clears state tracked for each FuzzerWorker and sets test cases in "running" states to
// "passed".
func (t *AssertionTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error {
Expand Down
77 changes: 77 additions & 0 deletions fuzzing/test_case_optimization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package fuzzing

import (
"fmt"
"github.com/crytic/medusa/fuzzing/calls"
"github.com/crytic/medusa/fuzzing/contracts"
"github.com/crytic/medusa/fuzzing/executiontracer"
"github.com/ethereum/go-ethereum/accounts/abi"
"math/big"
"strings"
"sync"
)

// OptimizationTestCase describes a test being run by a OptimizationTestCaseProvider.
type OptimizationTestCase struct {
// status describes the status of the test case
status TestCaseStatus
// targetContract describes the target contract where the test case was found
targetContract *contracts.Contract
// targetMethod describes the target method for the test case
targetMethod abi.Method
// callSequence describes the call sequence that maximized the value
callSequence *calls.CallSequence
// value is used to store the maximum value returned by the test method
value *big.Int
// valueLock is used for thread-synchronization when updating the value
valueLock sync.Mutex
// optimizationTestTrace describes the execution trace when running the callSequence
optimizationTestTrace *executiontracer.ExecutionTrace
}

// Status describes the TestCaseStatus used to define the current state of the test.
func (t *OptimizationTestCase) Status() TestCaseStatus {
return t.status
}

// CallSequence describes the calls.CallSequence of calls sent to the EVM which resulted in this TestCase result.
// This should be nil if the result is not related to the CallSequence.
func (t *OptimizationTestCase) CallSequence() *calls.CallSequence {
return t.callSequence
}

// Name describes the name of the test case.
func (t *OptimizationTestCase) Name() string {
return fmt.Sprintf("Optimization Test: %s.%s", t.targetContract.Name(), t.targetMethod.Sig)
}

// Message obtains a text-based printable message which describes the test result.
func (t *OptimizationTestCase) Message() string {
// We print final value in case the test case passed for optimization test
if t.Status() == TestCaseStatusPassed {
msg := fmt.Sprintf(
"Optimization test \"%s.%s\" resulted in the maximum value: %s with the following sequence:\n%s",
t.targetContract.Name(),
t.targetMethod.Sig,
t.value,
t.CallSequence().String(),
)
// If an execution trace is attached then add it to the message
if t.optimizationTestTrace != nil {
// TODO: Improve formatting in logging PR
msg += fmt.Sprintf("\nOptimization test execution trace:\n%s", t.optimizationTestTrace.String())
}
return msg
}
return ""
}

// ID obtains a unique identifier for a test result.
func (t *OptimizationTestCase) ID() string {
return strings.Replace(fmt.Sprintf("OPTIMIZATION-%s-%s", t.targetContract.Name(), t.targetMethod.Sig), "_", "-", -1)
}

// Value obtains the maximum value returned by the test method found till now
func (t *OptimizationTestCase) Value() *big.Int {
return t.value
}
Loading

0 comments on commit c444be9

Please sign in to comment.