Skip to content

Commit

Permalink
resolve conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
0xalpharush committed Aug 26, 2024
2 parents 2abb619 + ff587c3 commit 1f47c3f
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 49 deletions.
2 changes: 2 additions & 0 deletions compilation/platforms/crytic_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/crytic/medusa/compilation/types"
"github.com/crytic/medusa/logging"
"github.com/crytic/medusa/utils"
)

Expand Down Expand Up @@ -114,6 +115,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error)

// Get main command and set working directory
cmd := exec.Command("crytic-compile", args...)
logging.GlobalLogger.Info("Running command:\n", cmd.String())

// Install a specific `solc` version if requested in the config
if c.SolcVersion != "" {
Expand Down
90 changes: 65 additions & 25 deletions fuzzing/coverage/coverage_maps.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package coverage

import (
"bytes"
"golang.org/x/exp/slices"

compilationTypes "github.com/crytic/medusa/compilation/types"
"github.com/crytic/medusa/utils"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -168,8 +169,8 @@ func (cm *CoverageMaps) Update(coverageMaps *CoverageMaps) (bool, bool, error) {
return successCoverageChanged, revertedCoverageChanged, nil
}

// SetAt sets the coverage state of a given program counter location within code coverage data.
func (cm *CoverageMaps) SetAt(codeAddress common.Address, codeLookupHash common.Hash, codeSize int, pc uint64) (bool, error) {
// UpdateAt updates the hit count of a given program counter location within code coverage data.
func (cm *CoverageMaps) UpdateAt(codeAddress common.Address, codeLookupHash common.Hash, codeSize int, pc uint64) (bool, error) {
// If the code size is zero, do nothing
if codeSize == 0 {
return false, nil
Expand Down Expand Up @@ -210,7 +211,8 @@ func (cm *CoverageMaps) SetAt(codeAddress common.Address, codeLookupHash common.
}

// Set our coverage in the map and return our change state
changedInMap, err = coverageMap.setCoveredAt(codeSize, pc)
changedInMap, err = coverageMap.updateCoveredAt(codeSize, pc)

return addedNewMap || changedInMap, err
}

Expand Down Expand Up @@ -242,6 +244,37 @@ func (cm *CoverageMaps) RevertAll() (bool, error) {
return revertedCoverageChanged, nil
}

// UniquePCs is a function that returns the total number of unique program counters (PCs)
func (cm *CoverageMaps) UniquePCs() uint64 {
uniquePCs := uint64(0)
// Iterate across each contract deployment
for _, mapsByAddress := range cm.maps {
for _, contractCoverageMap := range mapsByAddress {
// TODO: Note we are not checking for nil dereference here because we are guaranteed that the successful
// coverage and reverted coverage arrays have been instantiated if we are iterating over it

// Iterate across each PC in the successful coverage array
// We do not separately iterate over the reverted coverage array because if there is no data about a
// successful PC execution, then it is not possible for that PC to have ever reverted either
for i, hits := range contractCoverageMap.successfulCoverage.executedFlags {
// If we hit the PC at least once, we have a unique PC hit
if hits != 0 {
uniquePCs++

// Do not count both success and revert
continue
}

// This is only executed if the PC was not executed successfully
if contractCoverageMap.revertedCoverage.executedFlags != nil && contractCoverageMap.revertedCoverage.executedFlags[i] != 0 {
uniquePCs++
}
}
}
}
return uniquePCs
}

// ContractCoverageMap represents a data structure used to identify instruction execution coverage of a contract.
type ContractCoverageMap struct {
// successfulCoverage represents coverage for the contract bytecode, which did not encounter a revert and was
Expand All @@ -267,7 +300,7 @@ func (cm *ContractCoverageMap) Equal(b *ContractCoverageMap) bool {
return cm.successfulCoverage.Equal(b.successfulCoverage) && cm.revertedCoverage.Equal(b.revertedCoverage)
}

// update creates updates the current ContractCoverageMap with the provided one.
// update updates the current ContractCoverageMap with the provided one.
// Returns two booleans indicating whether successful or reverted coverage changed, or an error if one was encountered.
func (cm *ContractCoverageMap) update(coverageMap *ContractCoverageMap) (bool, bool, error) {
// Update our success coverage data
Expand All @@ -285,18 +318,18 @@ func (cm *ContractCoverageMap) update(coverageMap *ContractCoverageMap) (bool, b
return successfulCoverageChanged, revertedCoverageChanged, nil
}

// setCoveredAt sets the coverage state at a given program counter location within a ContractCoverageMap used for
// updateCoveredAt updates the hit counter at a given program counter location within a ContractCoverageMap used for
// "successful" coverage (non-reverted).
// Returns a boolean indicating whether new coverage was achieved, or an error if one occurred.
func (cm *ContractCoverageMap) setCoveredAt(codeSize int, pc uint64) (bool, error) {
func (cm *ContractCoverageMap) updateCoveredAt(codeSize int, pc uint64) (bool, error) {
// Set our coverage data for the successful path.
return cm.successfulCoverage.setCoveredAt(codeSize, pc)
return cm.successfulCoverage.updateCoveredAt(codeSize, pc)
}

// CoverageMapBytecodeData represents a data structure used to identify instruction execution coverage of some init
// or runtime bytecode.
type CoverageMapBytecodeData struct {
executedFlags []byte
executedFlags []uint
}

// Reset resets the bytecode coverage map data to be empty.
Expand All @@ -310,27 +343,29 @@ func (cm *CoverageMapBytecodeData) Equal(b *CoverageMapBytecodeData) bool {
// Return an equality comparison on the data, ignoring size checks by stopping at the end of the shortest slice.
// We do this to avoid comparing arbitrary length constructor arguments appended to init bytecode.
smallestSize := utils.Min(len(cm.executedFlags), len(b.executedFlags))
return bytes.Equal(cm.executedFlags[:smallestSize], b.executedFlags[:smallestSize])
// TODO: Currently we are checking equality by making sure the two maps have the same hit counts
// it may make sense to just check that both of them are greater than zero
return slices.Equal(cm.executedFlags[:smallestSize], b.executedFlags[:smallestSize])
}

// IsCovered checks if a given program counter location is covered by the map.
// Returns a boolean indicating if the program counter was executed on this map.
func (cm *CoverageMapBytecodeData) IsCovered(pc int) bool {
// HitCount returns the number of times that the provided program counter (PC) has been hit. If zero is returned, then
// the PC has not been hit, the map is empty, or the PC is out-of-bounds
func (cm *CoverageMapBytecodeData) HitCount(pc int) uint {
// If the coverage map bytecode data is nil, this is not covered.
if cm == nil {
return false
return 0
}

// If this map has no execution data or is out of bounds, it is not covered.
if cm.executedFlags == nil || len(cm.executedFlags) <= pc {
return false
return 0
}

// Otherwise, return the execution flag
return cm.executedFlags[pc] != 0
// Otherwise, return the hit count
return cm.executedFlags[pc]
}

// update creates updates the current CoverageMapBytecodeData with the provided one.
// update updates the hit count of the current CoverageMapBytecodeData with the provided one.
// Returns a boolean indicating whether new coverage was achieved, or an error if one was encountered.
func (cm *CoverageMapBytecodeData) update(coverageMap *CoverageMapBytecodeData) (bool, error) {
// If the coverage map execution data provided is nil, exit early
Expand All @@ -347,28 +382,33 @@ func (cm *CoverageMapBytecodeData) update(coverageMap *CoverageMapBytecodeData)
// Update each byte which represents a position in the bytecode which was covered.
changed := false
for i := 0; i < len(cm.executedFlags) && i < len(coverageMap.executedFlags); i++ {
// Only update the map if we haven't seen this coverage before
if cm.executedFlags[i] == 0 && coverageMap.executedFlags[i] != 0 {
cm.executedFlags[i] = 1
cm.executedFlags[i] += coverageMap.executedFlags[i]
changed = true
}
}
return changed, nil
}

// setCoveredAt sets the coverage state at a given program counter location within a CoverageMapBytecodeData.
// updateCoveredAt updates the hit count at a given program counter location within a CoverageMapBytecodeData.
// Returns a boolean indicating whether new coverage was achieved, or an error if one occurred.
func (cm *CoverageMapBytecodeData) setCoveredAt(codeSize int, pc uint64) (bool, error) {
func (cm *CoverageMapBytecodeData) updateCoveredAt(codeSize int, pc uint64) (bool, error) {
// If the execution flags don't exist, create them for this code size.
if cm.executedFlags == nil {
cm.executedFlags = make([]byte, codeSize)
cm.executedFlags = make([]uint, codeSize)
}

// If our program counter is in range, determine if we achieved new coverage for the first time, and update it.
// If our program counter is in range, determine if we achieved new coverage for the first time or increment the hit counter.
if pc < uint64(len(cm.executedFlags)) {
if cm.executedFlags[pc] == 0 {
cm.executedFlags[pc] = 1
// Increment the hit counter
cm.executedFlags[pc] += 1

// This is the first time we have hit this PC, so return true
if cm.executedFlags[pc] == 1 {
return true, nil
}
// We have seen this PC before, return false
return false, nil
}

Expand Down
2 changes: 1 addition & 1 deletion fuzzing/coverage/coverage_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (t *CoverageTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tr
}

// Record coverage for this location in our map.
_, coverageUpdateErr := callFrameState.pendingCoverageMap.SetAt(address, *callFrameState.lookupHash, codeSize, pc)
_, coverageUpdateErr := callFrameState.pendingCoverageMap.UpdateAt(address, *callFrameState.lookupHash, codeSize, pc)
if coverageUpdateErr != nil {
logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map while tracing state", coverageUpdateErr)
}
Expand Down
4 changes: 2 additions & 2 deletions fuzzing/coverage/report_template.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,12 @@
{{/* Output two cells for the reverted/non-reverted execution status */}}
<td class="row-reverted-status unselectable">
{{if $line.IsCovered}}
<div title="The source line executed without reverting.">√</div>
<div title="The source line executed without reverting.">√ {{$line.SuccessHitCount}}</div>
{{end}}
</td>
<td class="row-reverted-status unselectable">
{{if $line.IsCoveredReverted}}
<div title="The source line executed, but was reverted.">⟳</div>
<div title="The source line executed, but was reverted.">⟳ {{$line.RevertHitCount}}</div>
{{end}}
</td>

Expand Down
24 changes: 16 additions & 8 deletions fuzzing/coverage/source_analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ type SourceLineAnalysis struct {
// IsCovered indicates whether the source line has been executed without reverting.
IsCovered bool

// SuccessHitCount describes how many times this line was executed successfully
SuccessHitCount uint

// RevertHitCount describes how many times this line reverted during execution
RevertHitCount uint

// IsCoveredReverted indicates whether the source line has been executed before reverting.
IsCoveredReverted bool
}
Expand Down Expand Up @@ -214,12 +220,12 @@ func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis
continue
}

// Check if the source map element was executed.
sourceMapElementCovered := false
sourceMapElementCoveredReverted := false
// Capture the hit count of the source map element.
succHitCount := uint(0)
revertHitCount := uint(0)
if contractCoverageData != nil {
sourceMapElementCovered = contractCoverageData.successfulCoverage.IsCovered(instructionOffsetLookup[sourceMapElement.Index])
sourceMapElementCoveredReverted = contractCoverageData.revertedCoverage.IsCovered(instructionOffsetLookup[sourceMapElement.Index])
succHitCount = contractCoverageData.successfulCoverage.HitCount(instructionOffsetLookup[sourceMapElement.Index])
revertHitCount = contractCoverageData.revertedCoverage.HitCount(instructionOffsetLookup[sourceMapElement.Index])
}

// Obtain the source file this element maps to.
Expand All @@ -232,9 +238,11 @@ func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis
// Mark the line active/executable.
sourceLine.IsActive = true

// Set its coverage state
sourceLine.IsCovered = sourceLine.IsCovered || sourceMapElementCovered
sourceLine.IsCoveredReverted = sourceLine.IsCoveredReverted || sourceMapElementCoveredReverted
// Set its coverage state and increment hit counts
sourceLine.SuccessHitCount += succHitCount
sourceLine.RevertHitCount += revertHitCount
sourceLine.IsCovered = sourceLine.IsCovered || sourceLine.SuccessHitCount > 0
sourceLine.IsCoveredReverted = sourceLine.IsCoveredReverted || sourceLine.RevertHitCount > 0

// Indicate we matched a source line, so when we stop matching sequentially, we know we can exit
// early.
Expand Down
24 changes: 18 additions & 6 deletions fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,13 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) {
if fuzzer.config.Compilation != nil {
// Compile the targets specified in the compilation config
fuzzer.logger.Info("Compiling targets with ", colors.Bold, fuzzer.config.Compilation.Platform, colors.Reset)
start := time.Now()
compilations, _, err := (*fuzzer.config.Compilation).Compile()
if err != nil {
fuzzer.logger.Error("Failed to compile target", err)
return nil, err
}
fuzzer.logger.Info("Finished compiling targets in ", time.Since(start).Round(time.Second))

// Add our compilation targets
fuzzer.AddCompilationTargets(compilations)
Expand All @@ -191,8 +193,6 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) {
attachAssertionTestCaseProvider(fuzzer)
}
if fuzzer.config.Fuzzing.Testing.OptimizationTesting.Enabled {
// TODO: Remove this warning when call sequence shrinking is improved
fuzzer.logger.Warn("Currently, optimization mode's call sequence shrinking is inefficient; this may lead to minor performance issues")
attachOptimizationTestCaseProvider(fuzzer)
}
return fuzzer, nil
Expand Down Expand Up @@ -744,7 +744,7 @@ func (f *Fuzzer) Start() error {
}

// Set it up with our deployment/setup strategy defined by the fuzzer.
f.logger.Info("Setting up base chain")
f.logger.Info("Setting up test chain")
trace, err := f.Hooks.ChainSetupFunc(f, baseTestChain)
if err != nil {
if trace != nil {
Expand All @@ -754,11 +754,18 @@ func (f *Fuzzer) Start() error {
}
return err
}
f.logger.Info("Finished setting up test chain")

// Initialize our coverage maps by measuring the coverage we get from the corpus.
var corpusActiveSequences, corpusTotalSequences int
f.logger.Info("Initializing and validating corpus call sequences")
if f.corpus.CallSequenceEntryCount(true, true, true) > 0 {
f.logger.Info("Running call sequences in the corpus...")
}
startTime := time.Now()
corpusActiveSequences, corpusTotalSequences, err = f.corpus.Initialize(baseTestChain, f.contractDefinitions)
if corpusTotalSequences > 0 {
f.logger.Info("Finished running call sequences in the corpus in ", time.Since(startTime).Round(time.Second))
}
if err != nil {
f.logger.Error("Failed to initialize the corpus", err)
return err
Expand Down Expand Up @@ -857,12 +864,14 @@ func (f *Fuzzer) printMetricsLoop() {
lastCallsTested := big.NewInt(0)
lastSequencesTested := big.NewInt(0)
lastWorkerStartupCount := big.NewInt(0)
lastGasUsed := big.NewInt(0)

lastPrintedTime := time.Time{}
for !utils.CheckContextDone(f.ctx) {
// Obtain our metrics
callsTested := f.metrics.CallsTested()
sequencesTested := f.metrics.SequencesTested()
gasUsed := f.metrics.GasUsed()
failedSequences := f.metrics.FailedSequences()
workerStartupCount := f.metrics.WorkerStartupCount()
workersShrinking := f.metrics.WorkersShrinkingCount()
Expand All @@ -882,10 +891,12 @@ func (f *Fuzzer) printMetricsLoop() {
logBuffer.Append("elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset)
logBuffer.Append(", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset)
logBuffer.Append(", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset)
logBuffer.Append(", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset)
logBuffer.Append(", shrinking: ", colors.Bold, fmt.Sprintf("%v", workersShrinking), colors.Reset)
logBuffer.Append(", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.CoverageMaps().UniquePCs()), colors.Reset)
logBuffer.Append(", corpus: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset)
logBuffer.Append(", failures: ", colors.Bold, fmt.Sprintf("%d/%d", failedSequences, sequencesTested), colors.Reset)
logBuffer.Append(", gas/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(gasUsed, lastGasUsed).Uint64())/secondsSinceLastUpdate)), colors.Reset)
if f.logger.Level() <= zerolog.DebugLevel {
logBuffer.Append(", shrinking: ", colors.Bold, fmt.Sprintf("%v", workersShrinking), colors.Reset)
logBuffer.Append(", mem: ", colors.Bold, fmt.Sprintf("%v/%v MB", memoryUsedMB, memoryTotalMB), colors.Reset)
logBuffer.Append(", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset)
}
Expand All @@ -895,6 +906,7 @@ func (f *Fuzzer) printMetricsLoop() {
lastPrintedTime = time.Now()
lastCallsTested = callsTested
lastSequencesTested = sequencesTested
lastGasUsed = gasUsed
lastWorkerStartupCount = workerStartupCount

// If we reached our transaction threshold, halt
Expand Down
Loading

0 comments on commit 1f47c3f

Please sign in to comment.