Skip to content
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

Fix corpus call method resolution bug, improve startup logging #308

Merged
merged 3 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions fuzzing/calls/call_message_abi_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/crytic/medusa/fuzzing/valuegeneration"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)

// CallMessageDataAbiValues describes a CallMessage Data field which is represented by ABI input argument values.
Expand All @@ -22,8 +23,17 @@ type CallMessageDataAbiValues struct {

// methodName stores the name of Method when decoding from JSON. The Method will be resolved using this internal
// reference when Resolve is called.
//
// TODO: Note, this field is deprecated and should be removed after methodSignature is adopted for some time.
// This will help transition old corpuses in the meantime.
methodName string

// methodSignature stores the function prototype which is used to calculate the method ID. This is human-readable,
// and easily editable, so it is used in favor of the method ID derived from it.
//
// The Method will be resolved using this internal reference when Resolve is called.
methodSignature string

// encodedInputValues stores the raw encoded input values when decoding from JSON. The actual InputValues will be
// decoded using this and the resolved Method once Resolve is called.
encodedInputValues []any
Expand All @@ -32,7 +42,8 @@ type CallMessageDataAbiValues struct {
// callMessageDataAbiValuesMarshal is used as an internal struct to represent JSON serialized data for
// CallMessageDataAbiValues.
type callMessageDataAbiValuesMarshal struct {
MethodName string `json:"methodName"`
MethodName string `json:"methodName,omitempty"`
MethodSignature string `json:"methodSignature"`
EncodedInputValues []any `json:"inputValues"`
}

Expand All @@ -43,6 +54,7 @@ func (m *CallMessageDataAbiValues) Clone() (*CallMessageDataAbiValues, error) {
Method: m.Method,
InputValues: nil, // set lower
methodName: m.methodName,
methodSignature: m.methodSignature,
encodedInputValues: m.encodedInputValues,
}

Expand All @@ -65,17 +77,32 @@ func (m *CallMessageDataAbiValues) Clone() (*CallMessageDataAbiValues, error) {
// Resolve takes a previously unmarshalled CallMessageDataAbiValues and resolves all internal data needed for it to be
// used at runtime by resolving the abi.Method it references from the provided contract ABI.
func (d *CallMessageDataAbiValues) Resolve(contractAbi abi.ABI) error {
// Try to resolve the method from our contract ABI.
if resolvedMethod, ok := contractAbi.Methods[d.methodName]; ok {
d.Method = &resolvedMethod
} else {
return fmt.Errorf("could not resolve method '%v' from the given contract ABI", d.methodName)
// If we have a method signature, try to resolve it by calculating a method ID from this.
d.Method = nil
if d.methodSignature != "" {
methodId := crypto.Keccak256([]byte(d.methodSignature))[:4]
if resolvedMethod, err := contractAbi.MethodById(methodId); err == nil {
d.Method = resolvedMethod
} else {
return fmt.Errorf("could not resolve method signature '%v'", d.methodSignature)
}
}

// TODO: Deprecated old way of resolving methods. This is left for compatibility with old corpuses, but should be
// removed at a later date in favor of methodSignature resolution. It resolves a method by name if it has not been.
if d.Method == nil {
if resolvedMethod, ok := contractAbi.Methods[d.methodName]; ok {
d.Method = &resolvedMethod
} else {
return fmt.Errorf("could not resolve method name '%v'", d.methodName)
}
}
d.methodSignature = d.Method.Sig

// Now that we've resolved the method, decode our encoded input values.
decodedArguments, err := valuegeneration.DecodeJSONArgumentsFromSlice(d.Method.Inputs, d.encodedInputValues, make(map[string]common.Address))
if err != nil {
return err
return fmt.Errorf("error decoding arguments for method '%v': %v", d.methodSignature, err)
}

// If we've decoded arguments successfully, set them and clear our encoded arguments as they're no longer needed.
Expand Down Expand Up @@ -132,7 +159,7 @@ func (d *CallMessageDataAbiValues) MarshalJSON() ([]byte, error) {

// Now create our outer struct and marshal all the data and return it.
marshalData := callMessageDataAbiValuesMarshal{
MethodName: d.Method.Name,
MethodSignature: d.Method.Sig,
EncodedInputValues: inputValuesEncoded,
}
return json.Marshal(marshalData)
Expand All @@ -150,6 +177,7 @@ func (d *CallMessageDataAbiValues) UnmarshalJSON(b []byte) error {

// Set our data in our actual structure now
d.methodName = marshalData.MethodName
d.methodSignature = marshalData.MethodSignature
d.encodedInputValues = marshalData.EncodedInputValues
return nil
}
12 changes: 11 additions & 1 deletion fuzzing/calls/call_sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,17 @@ func (cse *CallSequenceElement) Method() (*abi.Method, error) {
if cse.Contract == nil {
return nil, nil
}
return cse.Contract.CompiledContract().Abi.MethodById(cse.Call.Data)

// If we have a method resolved, return it.
if cse.Call != nil && cse.Call.DataAbiValues != nil {
if cse.Call.DataAbiValues.Method != nil {
return cse.Call.DataAbiValues.Method, nil
}
}

// Try to resolve the method by ID from the call data.
method, err := cse.Contract.CompiledContract().Abi.MethodById(cse.Call.Data)
return method, err
}

// String returns a displayable string representing the CallSequenceElement.
Expand Down
21 changes: 14 additions & 7 deletions fuzzing/corpus/corpus.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe
if callAbiValues != nil {
sequenceInvalidError = callAbiValues.Resolve(currentSequenceElement.Contract.CompiledContract().Abi)
if sequenceInvalidError != nil {
sequenceInvalidError = fmt.Errorf("error resolving method in contract '%v': %v", currentSequenceElement.Contract.Name(), sequenceInvalidError)
return nil, nil
}
}
Expand Down Expand Up @@ -236,7 +237,9 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe

// Initialize initializes any runtime data needed for a Corpus on startup. Call sequences are replayed on the post-setup
// (deployment) test chain to calculate coverage, while resolving references to compiled contracts.
func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions contracts.Contracts) error {
// Returns the active number of corpus items, total number of corpus items, or an error if one occurred. If an error
// is returned, then the corpus counts returned will always be zero.
func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions contracts.Contracts) (int, int, error) {
// Acquire our call sequences lock during the duration of this method.
c.callSequencesLock.Lock()
defer c.callSequencesLock.Unlock()
Expand Down Expand Up @@ -273,7 +276,7 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions
return nil
})
if err != nil {
return fmt.Errorf("failed to initialize coverage maps, base test chain cloning encountered error: %v", err)
return 0, 0, fmt.Errorf("failed to initialize coverage maps, base test chain cloning encountered error: %v", err)
}

// Set our coverage maps to those collected when replaying all blocks when cloning.
Expand All @@ -283,7 +286,7 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions
covMaps := coverage.GetCoverageTracerResults(messageResults)
_, _, covErr := c.coverageMaps.Update(covMaps)
if covErr != nil {
return err
return 0, 0, err
}
}
}
Expand All @@ -292,18 +295,22 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions
// are added to the corpus for mutations, re-execution, etc.
err = c.initializeSequences(c.mutableSequenceFiles, testChain, deployedContracts, true)
if err != nil {
return err
return 0, 0, err
}
err = c.initializeSequences(c.immutableSequenceFiles, testChain, deployedContracts, false)
if err != nil {
return err
return 0, 0, err
}
err = c.initializeSequences(c.testResultSequenceFiles, testChain, deployedContracts, false)
if err != nil {
return err
return 0, 0, err
}

return nil
// Calculate corpus health metrics
corpusSequencesTotal := len(c.mutableSequenceFiles.files) + len(c.immutableSequenceFiles.files) + len(c.testResultSequenceFiles.files)
corpusSequencesActive := len(c.unexecutedCallSequences)

return corpusSequencesActive, corpusSequencesTotal, nil
}

// addCallSequence adds a call sequence to the corpus in a given corpus directory.
Expand Down
21 changes: 18 additions & 3 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,8 +513,7 @@ func (f *Fuzzer) spawnWorkersLoop(baseTestChain *chain.TestChain) error {
// Define a flag that indicates whether we have not cancelled o
working := !utils.CheckContextDone(f.ctx)

// Log that we are about to create the workers and start fuzzing
f.logger.Info("Creating ", colors.Bold, f.config.Fuzzing.Workers, colors.Reset, " workers...")
// Create workers and start fuzzing.
var err error
for err == nil && working {
// Send an item into our channel to queue up a spot. This will block us if we hit capacity until a worker
Expand Down Expand Up @@ -617,6 +616,7 @@ func (f *Fuzzer) Start() error {
}

// Set up the corpus
f.logger.Info("Initializing corpus")
f.corpus, err = corpus.NewCorpus(f.config.Fuzzing.CorpusDirectory)
if err != nil {
f.logger.Error("Failed to create the corpus", err)
Expand All @@ -640,19 +640,34 @@ func (f *Fuzzer) Start() error {
}

// Set it up with our deployment/setup strategy defined by the fuzzer.
f.logger.Info("Setting up base chain")
err = f.Hooks.ChainSetupFunc(f, baseTestChain)
if err != nil {
f.logger.Error("Failed to initialize the test chain", err)
return err
}

// Initialize our coverage maps by measuring the coverage we get from the corpus.
err = f.corpus.Initialize(baseTestChain, f.contractDefinitions)
var corpusActiveSequences, corpusTotalSequences int
f.logger.Info("Initializing and validating corpus call sequences")
corpusActiveSequences, corpusTotalSequences, err = f.corpus.Initialize(baseTestChain, f.contractDefinitions)
if err != nil {
f.logger.Error("Failed to initialize the corpus", err)
return err
}

// Log corpus health statistics, if we have any existing sequences.
if corpusTotalSequences > 0 {
f.logger.Info(
colors.Bold, "corpus: ", colors.Reset,
"health: ", colors.Bold, int(float32(corpusActiveSequences)/float32(corpusTotalSequences)*100.0), "%", colors.Reset, ", ",
"sequences: ", colors.Bold, corpusTotalSequences, " (", corpusActiveSequences, " valid, ", corpusTotalSequences-corpusActiveSequences, " invalid)", colors.Reset,
)
}

// Log the start of our fuzzing campaign.
f.logger.Info("Fuzzing with ", colors.Bold, f.config.Fuzzing.Workers, colors.Reset, " workers")

// Start our printing loop now that we're about to begin fuzzing.
go f.printMetricsLoop()

Expand Down
Loading