From b7ea61bd76cbdb0520a5c4d34c7f1a8dc139f54e Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Tue, 2 May 2023 20:13:04 +0200 Subject: [PATCH 001/109] Create CODEOWNERS (#141) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..a0108738 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @Xenomega @anishnaik From 184905fd6d97607a87b24ad761a4bd028ac776c6 Mon Sep 17 00:00:00 2001 From: Jaime Iglesias <22358726+Jaime-Iglesias@users.noreply.github.com> Date: Mon, 22 May 2023 09:01:11 +0100 Subject: [PATCH 002/109] Coverage report generation (#111) * Added support for source coverage analysis and coverage report generation * Added support for parsing source maps, translating instruction index->offset * Added support for reverted coverage collection * Changed corpus to save different call sequences to different folders. * Improved init bytecode and runtime bytecode matching * Fix corpus initialization not measuring initial deployment coverage on fuzzer startup * Miscellaneous fixes and improvements --------- Co-authored-by: David Pokora Co-authored-by: anishnaik --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 1 + README.md | 4 +- compilation/platforms/crytic_compile.go | 6 +- compilation/platforms/crytic_compile_test.go | 38 +- compilation/platforms/solc.go | 13 + compilation/platforms/solc_test.go | 15 +- .../testdata/solc/SimpleContract.sol | 20 - .../{ => bad}/FailedCompilationContract.sol | 0 .../testdata/solc/basic/DerivedContract.sol | 9 + .../testdata/solc/basic/SimpleContract.sol | 26 ++ .../contracts/SimpleContract.sol | 20 - .../migrations/1_initial_migration.js | 5 - .../truffle/basic_project/test/.gitkeep | 0 .../truffle/basic_project/truffle-config.js | 116 ------ compilation/platforms/truffle.go | 141 ------- compilation/platforms/truffle_test.go | 24 -- compilation/supported_platforms.go | 1 - compilation/types/compilation.go | 50 ++- compilation/types/contract_metadata.go | 17 + compilation/types/source_maps.go | 183 ++++++++ fuzzing/contracts/contract.go | 11 +- fuzzing/corpus/corpus.go | 373 +++++++++-------- fuzzing/corpus/corpus_files.go | 187 +++++++++ fuzzing/corpus/corpus_test.go | 14 +- fuzzing/coverage/coverage_maps.go | 392 ++++++++++++------ fuzzing/coverage/coverage_tracer.go | 78 ++-- fuzzing/coverage/report_generation.go | 106 +++++ fuzzing/coverage/report_template.gohtml | 243 +++++++++++ fuzzing/coverage/source_analysis.go | 313 ++++++++++++++ fuzzing/fuzzer.go | 34 +- fuzzing/fuzzer_test.go | 8 +- fuzzing/fuzzer_test_methods_test.go | 7 +- fuzzing/fuzzer_worker.go | 6 +- fuzzing/fuzzer_worker_sequence_generator.go | 14 +- .../execution_tracing/proxy_call.sol | 2 +- utils/fs_utils.go | 5 +- 37 files changed, 1762 insertions(+), 722 deletions(-) delete mode 100644 compilation/platforms/testdata/solc/SimpleContract.sol rename compilation/platforms/testdata/solc/{ => bad}/FailedCompilationContract.sol (100%) create mode 100644 compilation/platforms/testdata/solc/basic/DerivedContract.sol create mode 100644 compilation/platforms/testdata/solc/basic/SimpleContract.sol delete mode 100644 compilation/platforms/testdata/truffle/basic_project/contracts/SimpleContract.sol delete mode 100644 compilation/platforms/testdata/truffle/basic_project/migrations/1_initial_migration.js delete mode 100644 compilation/platforms/testdata/truffle/basic_project/test/.gitkeep delete mode 100644 compilation/platforms/testdata/truffle/basic_project/truffle-config.js delete mode 100644 compilation/platforms/truffle.go delete mode 100644 compilation/platforms/truffle_test.go create mode 100644 compilation/types/source_maps.go create mode 100644 fuzzing/corpus/corpus_files.go create mode 100644 fuzzing/coverage/report_generation.go create mode 100644 fuzzing/coverage/report_template.gohtml create mode 100644 fuzzing/coverage/source_analysis.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2814442..437885c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,7 +105,7 @@ jobs: node-version: 18.15 - name: Install Node dependencies - run: npm install -g hardhat truffle + run: npm install -g hardhat - name: Install Python dependencies run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c1bdddf..7df2230d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,7 @@ To better understand how to make responsible open source contributions, consider When introducing changes to the project, note the following requirements: - All changes to the main branch should be introduced via pull requests. +- All branches created for pull requests should follow the `dev/*` naming convention, e.g. `dev/coverage-reports`. - Every pull request **must** be reviewed by at least one other peer prior to being merged into the main branch. - Code **must** be supported on Linux, macOS, and Windows. - Code **must** be sufficiently commented: diff --git a/README.md b/README.md index 65809192..9b4fc3fa 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It provides parallelized fuzz testing of smart contracts through CLI, or its Go ### Precompiled binaries -To use `medusa`, first ensure you have [crytic-compile](https://github.com/crytic/crytic-compile) and a suitable compilation framework (e.g. `solc`, `truffle`, `hardhat`) installed on your machine. +To use `medusa`, first ensure you have [crytic-compile](https://github.com/crytic/crytic-compile) and a suitable compilation framework (e.g. `solc`, `hardhat`) installed on your machine. You can then fetch the latest binaries for your platform from our [GitHub Releases](https://github.com/crytic/medusa/releases) page. @@ -85,7 +85,7 @@ This will use the `medusa.json` configuration in the current directory and begin ## Running Unit Tests -First, install [crytic-compile](https://github.com/crytic/crytic-compile), [solc-select](https://github.com/crytic/solc-select), and ensure you have `solc` (version >=0.8.7), `truffle`, and `hardhat` available on your system. +First, install [crytic-compile](https://github.com/crytic/crytic-compile), [solc-select](https://github.com/crytic/solc-select), and ensure you have `solc` (version >=0.8.7), and `hardhat` available on your system. - From the root of the repository, invoke `go test -v ./...` on through command-line to run tests from all packages at or below the root. - Or enter each package directory to run `go test -v .` to test the immediate package. diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index de01873c..377d1a6d 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -154,8 +154,9 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) BinRuntime string `json:"bin-runtime"` } type solcExportData struct { - Sources map[string]solcExportSource `json:"sources"` - Contracts map[string]solcExportContract `json:"contracts"` + Sources map[string]solcExportSource `json:"sources"` + Contracts map[string]solcExportContract `json:"contracts"` + SourceList []string `json:"sourceList"` } // Loop through each .json file for compilation units. @@ -175,6 +176,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) // Create a compilation object that will store the contracts and source information. compilation := types.NewCompilation() + compilation.SourceList = solcExport.SourceList // Loop through all sources and parse them into our types. for sourcePath, source := range solcExport.Sources { diff --git a/compilation/platforms/crytic_compile_test.go b/compilation/platforms/crytic_compile_test.go index 2b5e27c4..3c4ac420 100644 --- a/compilation/platforms/crytic_compile_test.go +++ b/compilation/platforms/crytic_compile_test.go @@ -39,7 +39,7 @@ func testCryticGetCompiledSourceByBaseName(sources map[string]types.CompiledSour // file path. func TestCryticSingleFileAbsolutePath(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/basic/SimpleContract.sol") // Execute our tests in the given test path testutils.ExecuteInDirectory(t, contractPath, func() { @@ -67,7 +67,7 @@ func TestCryticSingleFileAbsolutePath(t *testing.T) { // file path in the working directory. func TestCryticSingleFileRelativePathSameDirectory(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/basic/SimpleContract.sol") contractName := filepath.Base(contractPath) // Execute our tests in the given test path @@ -96,7 +96,7 @@ func TestCryticSingleFileRelativePathSameDirectory(t *testing.T) { // file path in a child directory of the working directory. func TestCryticSingleFileRelativePathChildDirectory(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/basic/SimpleContract.sol") // Move it to a subdirectory contractDirectory := filepath.Dir(contractPath) @@ -137,7 +137,7 @@ func TestCryticSingleFileRelativePathChildDirectory(t *testing.T) { // a relative path provided. func TestCryticSingleFileBuildDirectoryArgRelativePath(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/basic/SimpleContract.sol") // Execute our tests in the given test path testutils.ExecuteInDirectory(t, contractPath, func() { @@ -172,7 +172,7 @@ func TestCryticSingleFileBuildDirectoryArgRelativePath(t *testing.T) { // (e.g. export-dir, export-format) func TestCryticSingleFileBadArgs(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/basic/SimpleContract.sol") // Execute our tests in the given test path testutils.ExecuteInDirectory(t, contractPath, func() { @@ -198,6 +198,34 @@ func TestCryticSingleFileBadArgs(t *testing.T) { }) } +// TestCryticMultipleFiles tests compilation of a single target that inherits from another file. +func TestCryticMultipleFiles(t *testing.T) { + // Copy our testdata over to our testing directory + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/basic/") + + // Execute our tests in the given test path + testutils.ExecuteInDirectory(t, contractPath, func() { + // Create our platform configuration + config := NewCryticCompilationConfig("DerivedContract.sol") + + // Compile the file + compilations, _, err := config.Compile() + assert.NoError(t, err) + + // Verify there is one compilation object + assert.EqualValues(t, 1, len(compilations)) + // Verify there are two sources + assert.EqualValues(t, 2, len(compilations[0].Sources)) + + // Verify there are three contracts + contractCount := 0 + for _, source := range compilations[0].Sources { + contractCount += len(source.Contracts) + } + assert.EqualValues(t, 3, contractCount) + }) +} + // TestCryticDirectoryNoArgs tests compilation of a hardhat directory with no addition arguments provided func TestCryticDirectoryNoArgs(t *testing.T) { // Copy our testdata over to our testing directory diff --git a/compilation/platforms/solc.go b/compilation/platforms/solc.go index 989a7e53..283d8611 100644 --- a/compilation/platforms/solc.go +++ b/compilation/platforms/solc.go @@ -104,6 +104,18 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { // Create a compilation unit out of this. compilation := types.NewCompilation() + if sourceList, ok := results["sourceList"]; ok { + if sourceListCasted, ok := sourceList.([]any); ok { + compilation.SourceList = make([]string, len(sourceListCasted)) + for i := 0; i < len(sourceListCasted); i++ { + compilation.SourceList[i] = sourceListCasted[i].(string) + } + } else { + return nil, "", fmt.Errorf("could not parse compiled source artifact because 'sourcesList' was not a []string type") + } + } else { + return nil, "", fmt.Errorf("could not parse compiled source artifact because 'sourcesList' did not exist") + } // Parse our sources from solc output if sources, ok := results["sources"]; ok { @@ -135,6 +147,7 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { if err != nil { return nil, "", err } + for name, contract := range contracts { // Split our name which should be of form "filename:contractname" nameSplit := strings.Split(name, ":") diff --git a/compilation/platforms/solc_test.go b/compilation/platforms/solc_test.go index 95ad967d..0ed59ecc 100644 --- a/compilation/platforms/solc_test.go +++ b/compilation/platforms/solc_test.go @@ -19,12 +19,12 @@ func TestSolcVersion(t *testing.T) { // with an absolute target path in our platform config. func TestSimpleSolcCompilationAbsolutePath(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") + contractDirectory := testutils.CopyToTestDirectory(t, "testdata/solc/basic/") // Execute our tests in the given test path - testutils.ExecuteInDirectory(t, contractPath, func() { + testutils.ExecuteInDirectory(t, contractDirectory, func() { // Create a solc provider - solc := NewSolcCompilationConfig(contractPath) + solc := NewSolcCompilationConfig(filepath.Join(contractDirectory, "DerivedContract.sol")) // Obtain our compilations and ensure we didn't encounter an error compilations, _, err := solc.Compile() @@ -37,13 +37,12 @@ func TestSimpleSolcCompilationAbsolutePath(t *testing.T) { // with a relative target path in our platform config. func TestSimpleSolcCompilationRelativePath(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/SimpleContract.sol") - contractName := filepath.Base(contractPath) + contractDirectory := testutils.CopyToTestDirectory(t, "testdata/solc/basic/") // Execute our tests in the given test path - testutils.ExecuteInDirectory(t, contractPath, func() { + testutils.ExecuteInDirectory(t, contractDirectory, func() { // Create a solc provider - solc := NewSolcCompilationConfig(contractName) + solc := NewSolcCompilationConfig("DerivedContract.sol") // Obtain our solc version and ensure we didn't encounter an error compilations, _, err := solc.Compile() @@ -55,7 +54,7 @@ func TestSimpleSolcCompilationRelativePath(t *testing.T) { // TestFailedSolcCompilation tests that a single contract of invalid form should fail compilation. func TestFailedSolcCompilation(t *testing.T) { // Copy our testdata over to our testing directory - contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/FailedCompilationContract.sol") + contractPath := testutils.CopyToTestDirectory(t, "testdata/solc/bad/FailedCompilationContract.sol") // Execute our tests in the given test path testutils.ExecuteInDirectory(t, contractPath, func() { diff --git a/compilation/platforms/testdata/solc/SimpleContract.sol b/compilation/platforms/testdata/solc/SimpleContract.sol deleted file mode 100644 index d50df0a7..00000000 --- a/compilation/platforms/testdata/solc/SimpleContract.sol +++ /dev/null @@ -1,20 +0,0 @@ -contract SimpleContract { - uint x; - uint y; - - function setX(uint value) public { - x = value; - } - - function setY(uint value) public { - y = value; - } -} - -contract InheritedContract is SimpleContract { - uint z; - - function setZ(uint value) public { - z = value; - } -} diff --git a/compilation/platforms/testdata/solc/FailedCompilationContract.sol b/compilation/platforms/testdata/solc/bad/FailedCompilationContract.sol similarity index 100% rename from compilation/platforms/testdata/solc/FailedCompilationContract.sol rename to compilation/platforms/testdata/solc/bad/FailedCompilationContract.sol diff --git a/compilation/platforms/testdata/solc/basic/DerivedContract.sol b/compilation/platforms/testdata/solc/basic/DerivedContract.sol new file mode 100644 index 00000000..12f02015 --- /dev/null +++ b/compilation/platforms/testdata/solc/basic/DerivedContract.sol @@ -0,0 +1,9 @@ +import "./SimpleContract.sol"; + +contract DerivedContract is SimpleContract { + uint z; + + function setZ(uint value) public { + z = value; + } +} diff --git a/compilation/platforms/testdata/solc/basic/SimpleContract.sol b/compilation/platforms/testdata/solc/basic/SimpleContract.sol new file mode 100644 index 00000000..3cdcf6b9 --- /dev/null +++ b/compilation/platforms/testdata/solc/basic/SimpleContract.sol @@ -0,0 +1,26 @@ +contract SimpleContract { + uint x; + uint y; + + function setX(uint value) public { + x = value; + } + + function setY(uint value) public { + y = value; + } +} + +contract SimpleContract2 { + uint x; + uint y; + + function setX(uint value) public returns (bool) { + x = value; + return true; + } + + function setY(uint value) public { + y = value; + } +} \ No newline at end of file diff --git a/compilation/platforms/testdata/truffle/basic_project/contracts/SimpleContract.sol b/compilation/platforms/testdata/truffle/basic_project/contracts/SimpleContract.sol deleted file mode 100644 index d50df0a7..00000000 --- a/compilation/platforms/testdata/truffle/basic_project/contracts/SimpleContract.sol +++ /dev/null @@ -1,20 +0,0 @@ -contract SimpleContract { - uint x; - uint y; - - function setX(uint value) public { - x = value; - } - - function setY(uint value) public { - y = value; - } -} - -contract InheritedContract is SimpleContract { - uint z; - - function setZ(uint value) public { - z = value; - } -} diff --git a/compilation/platforms/testdata/truffle/basic_project/migrations/1_initial_migration.js b/compilation/platforms/testdata/truffle/basic_project/migrations/1_initial_migration.js deleted file mode 100644 index 16a7ba52..00000000 --- a/compilation/platforms/testdata/truffle/basic_project/migrations/1_initial_migration.js +++ /dev/null @@ -1,5 +0,0 @@ -const Migrations = artifacts.require("Migrations"); - -module.exports = function (deployer) { - deployer.deploy(Migrations); -}; diff --git a/compilation/platforms/testdata/truffle/basic_project/test/.gitkeep b/compilation/platforms/testdata/truffle/basic_project/test/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/compilation/platforms/testdata/truffle/basic_project/truffle-config.js b/compilation/platforms/testdata/truffle/basic_project/truffle-config.js deleted file mode 100644 index f10af854..00000000 --- a/compilation/platforms/testdata/truffle/basic_project/truffle-config.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Use this file to configure your truffle project. It's seeded with some - * common settings for different networks and features like migrations, - * compilation and testing. Uncomment the ones you need or modify - * them to suit your project as necessary. - * - * More information about configuration can be found at: - * - * trufflesuite.com/docs/advanced/configuration - * - * To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider) - * to sign your transactions before they're sent to a remote public node. Infura accounts - * are available for free at: infura.io/register. - * - * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate - * public/private key pairs. If you're publishing your code to GitHub make sure you load this - * phrase from a file you've .gitignored so it doesn't accidentally become public. - * - */ - -// const HDWalletProvider = require('@truffle/hdwallet-provider'); -// -// const fs = require('fs'); -// const mnemonic = fs.readFileSync(".secret").toString().trim(); - -module.exports = { - /** - * Networks define how you connect to your ethereum client and let you set the - * defaults web3 uses to send transactions. If you don't specify one truffle - * will spin up a development blockchain for you on port 9545 when you - * run `develop` or `test`. You can ask a truffle command to use a specific - * network from the command line, e.g - * - * $ truffle test --network - */ - - networks: { - // Useful for testing. The `development` name is special - truffle uses it by default - // if it's defined here and no other network is specified at the command line. - // You should run a client (like ganache-cli, geth or parity) in a separate terminal - // tab if you use this network and you must also set the `host`, `port` and `network_id` - // options below to some value. - // - // development: { - // host: "127.0.0.1", // Localhost (default: none) - // port: 8545, // Standard Ethereum port (default: none) - // network_id: "*", // Any network (default: none) - // }, - // Another network with more advanced options... - // advanced: { - // port: 8777, // Custom port - // network_id: 1342, // Custom network - // gas: 8500000, // Gas sent with each transaction (default: ~6700000) - // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) - // from:
, // Account to send txs from (default: accounts[0]) - // websocket: true // Enable EventEmitter interface for web3 (default: false) - // }, - // Useful for deploying to a public network. - // NB: It's important to wrap the provider as a function. - // ropsten: { - // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), - // network_id: 3, // Ropsten's id - // gas: 5500000, // Ropsten has a lower block limit than mainnet - // confirmations: 2, // # of confs to wait between deployments. (default: 0) - // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) - // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) - // }, - // Useful for private networks - // private: { - // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), - // network_id: 2111, // This network is yours, in the cloud. - // production: true // Treats this network as if it was a public net. (default: false) - // } - }, - - // Set default mocha options here, use special reporters etc. - mocha: { - // timeout: 100000 - }, - - // Configure your compilers - compilers: { - solc: { - // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) - // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) - // settings: { // See the solidity docs for advice about optimization and evmVersion - // optimizer: { - // enabled: false, - // runs: 200 - // }, - // evmVersion: "byzantium" - // } - } - }, - - // Truffle DB is currently disabled by default; to enable it, change enabled: - // false to enabled: true. The default storage location can also be - // overridden by specifying the adapter settings, as shown in the commented code below. - // - // NOTE: It is not possible to migrate your contracts to truffle DB and you should - // make a backup of your artifacts to a safe location before enabling this feature. - // - // After you backed up your artifacts you can utilize db by running migrate as follows: - // $ truffle migrate --reset --compile-all - // - // db: { - // enabled: false, - // host: "127.0.0.1", - // adapter: { - // name: "sqlite", - // settings: { - // directory: ".db" - // } - // } - // } -}; diff --git a/compilation/platforms/truffle.go b/compilation/platforms/truffle.go deleted file mode 100644 index d522a818..00000000 --- a/compilation/platforms/truffle.go +++ /dev/null @@ -1,141 +0,0 @@ -package platforms - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/crytic/medusa/compilation/types" -) - -type TruffleCompilationConfig struct { - Target string `json:"target"` - UseNpx bool `json:"useNpx"` - Command string `json:"command"` - BuildDirectory string `json:"buildDirectory"` -} - -func NewTruffleCompilationConfig(target string) *TruffleCompilationConfig { - return &TruffleCompilationConfig{ - Target: target, - UseNpx: true, - Command: "", - BuildDirectory: "", - } -} - -func (s *TruffleCompilationConfig) Platform() string { - return "truffle" -} - -// GetTarget returns the target for compilation -func (t *TruffleCompilationConfig) GetTarget() string { - return t.Target -} - -// SetTarget sets the new target for compilation -func (t *TruffleCompilationConfig) SetTarget(newTarget string) { - t.Target = newTarget -} - -func (s *TruffleCompilationConfig) Compile() ([]types.Compilation, string, error) { - // Determine the base command to use. - var baseCommandStr = "truffle" - if s.Command != "" { - baseCommandStr = s.Command - } - - // Execute solc to compile our target. - var cmd *exec.Cmd - if s.UseNpx { - cmd = exec.Command("npx", baseCommandStr, "compile", "--all") - } else { - cmd = exec.Command(baseCommandStr, "compile", "--all") - } - cmd.Dir = s.Target - out, err := cmd.CombinedOutput() - if err != nil { - return nil, "", fmt.Errorf("error while executing truffle:\nOUTPUT:\n%s\nERROR: %s\n", string(out), err.Error()) - } - - // Create a compilation unit out of this. - compilation := types.NewCompilation() - - // Find all the compiled truffle artifacts - buildDirectory := s.BuildDirectory - if buildDirectory == "" { - buildDirectory = filepath.Join(s.Target, "build", "contracts") - } - matches, err := filepath.Glob(filepath.Join(buildDirectory, "*.json")) - if err != nil { - return nil, "", err - } - - // Define our truffle structure to parse - type TruffleCompiledJson struct { - ContractName string `json:"contractName"` - Abi any `json:"abi"` - Bytecode string `json:"bytecode"` - DeployedBytecode string `json:"deployedBytecode"` - SourceMap string `json:"sourceMap"` - DeployedSourceMap string `json:"deployedSourceMap"` - Source string `json:"source"` - SourcePath string `json:"sourcePath"` - Ast any `json:"ast"` - } - - // Loop for each truffle artifact to parse our compilations. - for i := 0; i < len(matches); i++ { - // Read the compiled JSON file data - b, err := os.ReadFile(matches[i]) - if err != nil { - return nil, "", err - } - - // Parse the JSON - var compiledJson TruffleCompiledJson - err = json.Unmarshal(b, &compiledJson) - if err != nil { - return nil, "", err - } - - // Convert the abi structure to our parsed abi type - contractAbi, err := types.ParseABIFromInterface(compiledJson.Abi) - if err != nil { - continue - } - - // If we don't have a source for this file, create it. - if _, ok := compilation.Sources[compiledJson.SourcePath]; !ok { - compilation.Sources[compiledJson.SourcePath] = types.CompiledSource{ - Ast: compiledJson.Ast, - Contracts: make(map[string]types.CompiledContract), - } - } - - // Decode our init and runtime bytecode - initBytecode, err := hex.DecodeString(strings.TrimPrefix(compiledJson.Bytecode, "0x")) - if err != nil { - return nil, "", fmt.Errorf("unable to parse init bytecode for contract '%s'\n", compiledJson.ContractName) - } - runtimeBytecode, err := hex.DecodeString(strings.TrimPrefix(compiledJson.DeployedBytecode, "0x")) - if err != nil { - return nil, "", fmt.Errorf("unable to parse runtime bytecode for contract '%s'\n", compiledJson.ContractName) - } - - // Add our contract to the source - compilation.Sources[compiledJson.SourcePath].Contracts[compiledJson.ContractName] = types.CompiledContract{ - Abi: *contractAbi, - InitBytecode: initBytecode, - RuntimeBytecode: runtimeBytecode, - SrcMapsInit: compiledJson.SourceMap, - SrcMapsRuntime: compiledJson.DeployedSourceMap, - } - } - - return []types.Compilation{*compilation}, string(out), nil -} diff --git a/compilation/platforms/truffle_test.go b/compilation/platforms/truffle_test.go deleted file mode 100644 index 0def9e80..00000000 --- a/compilation/platforms/truffle_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package platforms - -import ( - "github.com/crytic/medusa/utils/testutils" - "github.com/stretchr/testify/assert" - "testing" -) - -// TestTruffleCompilationAbsolutePath tests compilation of a truffle project with an absolute project path. -func TestTruffleCompilationAbsolutePath(t *testing.T) { - // Copy our testdata over to our testing directory - truffleDirectory := testutils.CopyToTestDirectory(t, "testdata/truffle/basic_project/") - - // Execute our tests in the given test path - testutils.ExecuteInDirectory(t, truffleDirectory, func() { - // Create a solc provider - truffleConfig := NewTruffleCompilationConfig(truffleDirectory) - - // Obtain our solc version and ensure we didn't encounter an error - compilations, _, err := truffleConfig.Compile() - assert.NoError(t, err) - assert.True(t, len(compilations) > 0) - }) -} diff --git a/compilation/supported_platforms.go b/compilation/supported_platforms.go index 0fdc2b49..91dd6732 100644 --- a/compilation/supported_platforms.go +++ b/compilation/supported_platforms.go @@ -16,7 +16,6 @@ func init() { // Define a list of default platform config generators generators := []func() platforms.PlatformConfig{ func() platforms.PlatformConfig { return platforms.NewSolcCompilationConfig("contract.sol") }, - func() platforms.PlatformConfig { return platforms.NewTruffleCompilationConfig(".") }, func() platforms.PlatformConfig { return platforms.NewCryticCompilationConfig(".") }, } diff --git a/compilation/types/compilation.go b/compilation/types/compilation.go index d3352cd8..9958930b 100644 --- a/compilation/types/compilation.go +++ b/compilation/types/compilation.go @@ -1,19 +1,67 @@ package types +import ( + "errors" + "fmt" + "golang.org/x/exp/slices" + "os" +) + // Compilation represents the artifacts of a smart contract compilation. type Compilation struct { // Sources describes the CompiledSource objects provided in a compilation, housing information regarding source // files, mappings, ASTs, and contracts. Sources map[string]CompiledSource + + // SourceList describes the CompiledSource keys in Sources, in order. The file identifier used for a SourceMap + // corresponds to an index in this list. + SourceList []string + + // SourceCode is a lookup of a source file path from SourceList to source code. This is populated by + // CacheSourceCode. + SourceCode map[string][]byte } // NewCompilation returns a new, empty Compilation object. func NewCompilation() *Compilation { // Create our compilation compilation := &Compilation{ - Sources: make(map[string]CompiledSource), + Sources: make(map[string]CompiledSource), + SourceList: make([]string, 0), + SourceCode: make(map[string][]byte), } // Return the compilation. return compilation } + +// GetSourceFileId obtains the file identifier for a given source file path. This simply checks the index of the +// source file path in SourceList. +// Returns the identifier of the source file, or -1 if it could not be found. +func (c *Compilation) GetSourceFileId(sourcePath string) int { + return slices.Index(c.SourceList, sourcePath) +} + +// CacheSourceCode caches source code for each CompiledSource in the compilation in the CompiledSource.SourceCode field. +// This method will attempt to populate each CompiledSource.SourceCode which has not yet been populated (is nil) before +// returning an error, if one occurs. +func (c *Compilation) CacheSourceCode() error { + // Loop through each source file, try to read it, and collect errors in an aggregated string if we encounter any. + var errStr string + for sourcePath := range c.Sources { + if _, ok := c.SourceCode[sourcePath]; !ok { + sourceCodeBytes, sourceReadErr := os.ReadFile(sourcePath) + if sourceReadErr != nil { + errStr += fmt.Sprintf("source file '%v' could not be cached due to error: '%v'\n", sourcePath, sourceReadErr) + } + c.SourceCode[sourcePath] = sourceCodeBytes + } + } + + // If we have an error message, return an error encapsulating it. + if len(errStr) > 0 { + return errors.New(errStr) + } + + return nil +} diff --git a/compilation/types/contract_metadata.go b/compilation/types/contract_metadata.go index 0c3c885e..0a1e0d2b 100644 --- a/compilation/types/contract_metadata.go +++ b/compilation/types/contract_metadata.go @@ -2,6 +2,7 @@ package types import ( "bytes" + "github.com/fxamacker/cbor" ) @@ -46,6 +47,22 @@ func ExtractContractMetadata(bytecode []byte) *ContractMetadata { return nil } +// RemoveContractMetadata takes bytecode and attempts to detect contract metadata within it, splitting it where the +// metadata is found. +// If contract metadata could be located, this method returns the bytecode solely (no contract metadata, and no +// constructor arguments, which tend to follow). +// Otherwise, this method returns the provided input as-is. +func RemoveContractMetadata(bytecode []byte) []byte { + for _, metadataHashPrefix := range metadataHashPrefixes { + metadataOffset := bytes.LastIndex(bytecode, metadataHashPrefix[:]) + + if metadataOffset != -1 { + return bytecode[:metadataOffset-1] + } + } + return bytecode +} + // ExtractBytecodeHash extracts the bytecode hash from given contract metadata and returns the bytes representing the // hash. If it could not be detected or extracted, nil is returned. func (m ContractMetadata) ExtractBytecodeHash() []byte { diff --git a/compilation/types/source_maps.go b/compilation/types/source_maps.go new file mode 100644 index 00000000..9804733d --- /dev/null +++ b/compilation/types/source_maps.go @@ -0,0 +1,183 @@ +package types + +import ( + "fmt" + "github.com/ethereum/go-ethereum/core/vm" + "strconv" + "strings" +) + +// Reference: Source mapping is performed according to the rules specified in solidity documentation: +// https://docs.soliditylang.org/en/latest/internals/source_mappings.html + +// SourceMapJumpType describes the type of jump operation occurring within a SourceMapElement if the instruction +// is jumping. +type SourceMapJumpType string + +const ( + // SourceMapJumpTypeNone indicates no jump occurred. + SourceMapJumpTypeNone SourceMapJumpType = "" + + // SourceMapJumpTypeJumpIn indicates a jump into a function occurred. + SourceMapJumpTypeJumpIn SourceMapJumpType = "i" + + // SourceMapJumpTypeJumpOut indicates a return from a function occurred. + SourceMapJumpTypeJumpOut SourceMapJumpType = "o" + + // SourceMapJumpTypeJumpWithin indicates a jump occurred within the same function, e.g. for loops. + SourceMapJumpTypeJumpWithin SourceMapJumpType = "-" +) + +// SourceMap describes a list of elements which correspond to instruction indexes in compiled bytecode, describing +// which source files and the start/end range of the source code which the instruction maps to. +type SourceMap []SourceMapElement + +// SourceMapElement describes an individual element of a source mapping output by the compiler. +// The index of each element in a source map corresponds to an instruction index (not to be mistaken with offset). +// It describes portion of a source file the instruction references. +type SourceMapElement struct { + // Index refers to the index of the SourceMapElement within its parent SourceMap. This is not actually a field + // saved in the SourceMap, but is provided for convenience so the user may remove SourceMapElement objects during + // analysis. + Index int + + // Offset refers to the byte offset which marks the start of the source range the instruction maps to. + Offset int + + // Length refers to the byte length of the source range the instruction maps to. + Length int + + // FileID refers to an identifier for the CompiledSource file which houses the relevant source code. + FileID int + + // JumpType refers to the SourceMapJumpType which provides information about any type of jump that occurred. + JumpType SourceMapJumpType + + // ModifierDepth refers to the depth in which code has executed a modifier function. This is used to assist + // debuggers, e.g. understanding if the same modifier is re-used multiple times in a call. + ModifierDepth int +} + +// ParseSourceMap takes a source mapping string returned by the compiler and parses it into an array of +// SourceMapElement objects. +// Returns the list of SourceMapElement objects. +func ParseSourceMap(sourceMapStr string) (SourceMap, error) { + // Define our variables to store our results in + var ( + sourceMap SourceMap + err error + ) + + // If our provided source map string is empty, there is no work to be done. + if len(sourceMapStr) == 0 { + return sourceMap, nil + } + + // Separate all the individual source mapping elements + elements := strings.Split(sourceMapStr, ";") + + // We use this variable to store "the previous element" because the way + // the source mapping works when an element or field is "empty" + // the value of the previous element is used. + current := SourceMapElement{ + Index: -1, + Offset: -1, + Length: -1, + FileID: -1, + JumpType: "", + ModifierDepth: 0, + } + + // Iterate over all elements split from the source mapping + for _, element := range elements { + // Set the current index + current.Index = len(sourceMap) + + // If the element is empty, we use the previous one + if len(element) == 0 { + sourceMap = append(sourceMap, current) + continue + } + + // Split the element fields apart + fields := strings.Split(element, ":") + + // If the source range start offset exists, update our current element data. + if len(fields) > 0 && fields[0] != "" { + current.Offset, err = strconv.Atoi(fields[0]) + if err != nil { + return nil, err + } + } + + // If the source range length exists, update our current element data. + if len(fields) > 1 && fields[1] != "" { + current.Length, err = strconv.Atoi(fields[1]) + if err != nil { + return nil, err + } + } + + // If the source file identifier exists, update our current element data. + if len(fields) > 2 && fields[2] != "" { + current.FileID, err = strconv.Atoi(fields[2]) + if err != nil { + return nil, err + } + } + + // If the jump type information exists, update our current element data. + if len(fields) > 3 && fields[3] != "" { + current.JumpType = SourceMapJumpType(fields[3]) + } + + // If the modifier call depth exists, update our current element data. + if len(fields) > 4 && fields[4] != "" { + current.ModifierDepth, err = strconv.Atoi(fields[4]) + if err != nil { + return nil, err + } + } + + // Append our element to the map + sourceMap = append(sourceMap, current) + } + + // Return the resulting map + return sourceMap, nil +} + +// GetInstructionIndexToOffsetLookup obtains a slice where each index of the slice corresponds to an instruction index, +// and the element of the slice represents the instruction offset. +// Returns the slice lookup, or an error if one occurs. +func (s SourceMap) GetInstructionIndexToOffsetLookup(bytecode []byte) ([]int, error) { + // Create our resulting lookup + indexToOffsetLookup := make([]int, len(s)) + + // Loop through all byte code + currentOffset := 0 + for i := 0; i < len(indexToOffsetLookup); i++ { + // If we're going to read out of bounds, return an error. + if currentOffset >= len(bytecode) { + return nil, fmt.Errorf("failed to obtain a lookup of instruction indexes to offsets. instruction index: %v, current offset: %v, length: %v", i, currentOffset, len(bytecode)) + } + + // Obtain the indexed instruction and add the current offset to our lookup at this index. + op := vm.OpCode(bytecode[currentOffset]) + indexToOffsetLookup[i] = currentOffset + + // Next, calculate the length of data that follows this instruction. + operandCount := 0 + if op.IsPush() { + if op == vm.PUSH0 { + operandCount = 1 + } else { + operandCount = int(op) - int(vm.PUSH1) + 1 + } + } + + // Advance the offset past this instruction and its operands. + currentOffset += operandCount + 1 + } + return indexToOffsetLookup, nil +} diff --git a/fuzzing/contracts/contract.go b/fuzzing/contracts/contract.go index 30d4feb6..17ec9f66 100644 --- a/fuzzing/contracts/contract.go +++ b/fuzzing/contracts/contract.go @@ -32,14 +32,18 @@ type Contract struct { // compiledContract describes the compiled contract data. compiledContract *types.CompiledContract + + // compilation describes the compilation which contains the compiledContract. + compilation *types.Compilation } // NewContract returns a new Contract instance with the provided information. -func NewContract(name string, sourcePath string, compiledContract *types.CompiledContract) *Contract { +func NewContract(name string, sourcePath string, compiledContract *types.CompiledContract, compilation *types.Compilation) *Contract { return &Contract{ name: name, sourcePath: sourcePath, compiledContract: compiledContract, + compilation: compilation, } } @@ -57,3 +61,8 @@ func (c *Contract) SourcePath() string { func (c *Contract) CompiledContract() *types.CompiledContract { return c.compiledContract } + +// Compilation returns the compilation which contains the CompiledContract. +func (c *Contract) Compilation() *types.Compilation { + return c.compilation +} diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 0b78dbcd..28255579 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -2,21 +2,19 @@ package corpus import ( "bytes" - "encoding/json" "fmt" "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/coverage" - "github.com/crytic/medusa/utils" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" "math/big" - "os" "path/filepath" "sync" + "time" "github.com/crytic/medusa/fuzzing/contracts" - "github.com/google/uuid" ) // Corpus describes an archive of fuzzer-generated artifacts used to further fuzzing efforts. These artifacts are @@ -29,76 +27,69 @@ type Corpus struct { // coverageMaps describes the total code coverage known to be achieved across all corpus call sequences. coverageMaps *coverage.CoverageMaps - // callSequences is a list of call sequences that increased coverage or otherwise were found to be valuable - // to the fuzzer. - callSequences []*corpusFile[calls.CallSequence] + // mutableSequenceFiles represents a corpus directory with files which describe call sequences that should + // be used for mutations. + mutableSequenceFiles *corpusDirectory[calls.CallSequence] + + // immutableSequenceFiles represents a corpus directory with files which describe call sequences that should not be + // used for mutations. + immutableSequenceFiles *corpusDirectory[calls.CallSequence] + + // testResultSequenceFiles represents a corpus directory with files which describe call sequences that were flagged + // to be saved by a test case provider. These are not used in mutations. + testResultSequenceFiles *corpusDirectory[calls.CallSequence] // unexecutedCallSequences defines the callSequences which have not yet been executed by the fuzzer. As each item // is selected for execution by the fuzzer on startup, it is removed. This way, all call sequences loaded from disk // are executed to check for test failures. unexecutedCallSequences []calls.CallSequence - // weightedCallSequenceChooser is a provider that allows for weighted random selection of callSequences. If a + // mutationTargetSequenceChooser is a provider that allows for weighted random selection of callSequences. If a // call sequence was not found to be compatible with this run, it is not added to the chooser. - weightedCallSequenceChooser *randomutils.WeightedRandomChooser[calls.CallSequence] + mutationTargetSequenceChooser *randomutils.WeightedRandomChooser[calls.CallSequence] // callSequencesLock provides thread synchronization to prevent concurrent access errors into // callSequences. callSequencesLock sync.Mutex } -// corpusFile represents corpus data and its state on the filesystem. -type corpusFile[T any] struct { - // filePath describes the path the file should be written to. If blank, this indicates it has not yet been written. - filePath string - - // data describes an object whose data should be written to the file. - data T -} - // NewCorpus initializes a new Corpus object, reading artifacts from the provided directory. If the directory refers // to an empty path, artifacts will not be persistently stored. func NewCorpus(corpusDirectory string) (*Corpus, error) { + var err error corpus := &Corpus{ storageDirectory: corpusDirectory, coverageMaps: coverage.NewCoverageMaps(), - callSequences: make([]*corpusFile[calls.CallSequence], 0), + mutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""), + immutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""), + testResultSequenceFiles: newCorpusDirectory[calls.CallSequence](""), unexecutedCallSequences: make([]calls.CallSequence, 0), } - // If we have a corpus directory set, parse it. + // If we have a corpus directory set, parse our call sequences. if corpus.storageDirectory != "" { - // Read all call sequences discovered in the relevant corpus directory. - matches, err := filepath.Glob(filepath.Join(corpus.CallSequencesDirectory(), "*.json")) + // Read mutable call sequences. + corpus.mutableSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences", "mutable") + err = corpus.mutableSequenceFiles.readFiles("*.json") if err != nil { return nil, err } - for i := 0; i < len(matches); i++ { - // Alias our file path. - filePath := matches[i] - - // Read the call sequence data. - b, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - // Parse the call sequence data. - var seq calls.CallSequence - err = json.Unmarshal(b, &seq) - if err != nil { - return nil, err - } + // Read immutable call sequences. + corpus.immutableSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences", "immutable") + err = corpus.immutableSequenceFiles.readFiles("*.json") + if err != nil { + return nil, err + } - // Add entry to corpus - corpus.callSequences = append(corpus.callSequences, &corpusFile[calls.CallSequence]{ - filePath: filePath, - data: seq, - }) + // Read test case provider related call sequences (test failures, etc). + corpus.testResultSequenceFiles.path = filepath.Join(corpus.storageDirectory, "test_results") + err = corpus.testResultSequenceFiles.readFiles("*.json") + if err != nil { + return nil, err } } - // Initialize our weighted random chooser return corpus, nil } @@ -107,86 +98,59 @@ func (c *Corpus) CoverageMaps() *coverage.CoverageMaps { return c.coverageMaps } -// StorageDirectory returns the root directory path of the corpus. If this is empty, it indicates persistent storage -// will not be used. -func (c *Corpus) StorageDirectory() string { - return c.storageDirectory -} - -// CallSequencesDirectory returns the directory path where coverage increasing call sequences should be stored. -// This is a subdirectory of StorageDirectory. If StorageDirectory is empty, this is as well, indicating persistent -// storage will not be used. -func (c *Corpus) CallSequencesDirectory() string { - if c.storageDirectory == "" { - return "" +// CallSequenceEntryCount returns the total number of call sequences entries in the corpus, based on the provided filter +// flags. Some call sequences may not be valid for use if they fail validation when initializing the corpus. +// Returns the count of the requested call sequence entries. +func (c *Corpus) CallSequenceEntryCount(mutable bool, immutable bool, testResults bool) int { + count := 0 + if mutable { + count += len(c.mutableSequenceFiles.files) } - return filepath.Join(c.StorageDirectory(), "call_sequences") -} - -// CallSequenceCount returns the total number of call sequences in the corpus, some of which may be inactive/not in use. -func (c *Corpus) CallSequenceCount() int { - return len(c.callSequences) + if immutable { + count += len(c.immutableSequenceFiles.files) + } + if testResults { + count += len(c.testResultSequenceFiles.files) + } + return count } -// ActiveCallSequenceCount returns the count of call sequences recorded in the corpus which have been validated and are -// ready for use by RandomCallSequence. -func (c *Corpus) ActiveCallSequenceCount() int { - if c.weightedCallSequenceChooser == nil { +// ActiveMutableSequenceCount returns the count of call sequences recorded in the corpus which have been validated +// after Corpus initialization and are ready for use in mutations. +func (c *Corpus) ActiveMutableSequenceCount() int { + if c.mutationTargetSequenceChooser == nil { return 0 } - return c.weightedCallSequenceChooser.ChoiceCount() + return c.mutationTargetSequenceChooser.ChoiceCount() } -// 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 { - // Acquire our call sequences lock during the duration of this method. - c.callSequencesLock.Lock() - defer c.callSequencesLock.Unlock() - - // Initialize our call sequence structures. - c.weightedCallSequenceChooser = randomutils.NewWeightedRandomChooser[calls.CallSequence]() - c.unexecutedCallSequences = make([]calls.CallSequence, 0) - - // Create new coverage maps to track total coverage and a coverage tracer to do so. - c.coverageMaps = coverage.NewCoverageMaps() - coverageTracer := coverage.NewCoverageTracer() - - // Create our structure and event listeners to track deployed contracts - deployedContracts := make(map[common.Address]*contracts.Contract, 0) - - // Clone our test chain, adding listeners for contract deployment events from genesis. - testChain, err := baseTestChain.Clone(func(newChain *chain.TestChain) error { - // After genesis, prior to adding other blocks, we attach our coverage tracer - newChain.AddTracer(coverageTracer, true, false) - - // We also track any contract deployments, so we can resolve contract/method definitions for corpus call - // sequences. - newChain.Events.ContractDeploymentAddedEventEmitter.Subscribe(func(event chain.ContractDeploymentsAddedEvent) error { - matchedContract := contractDefinitions.MatchBytecode(event.Contract.InitBytecode, event.Contract.RuntimeBytecode) - if matchedContract != nil { - deployedContracts[event.Contract.Address] = matchedContract - } - return nil - }) - newChain.Events.ContractDeploymentRemovedEventEmitter.Subscribe(func(event chain.ContractDeploymentsRemovedEvent) error { - delete(deployedContracts, event.Contract.Address) - return nil - }) - return nil - }) - if err != nil { - return fmt.Errorf("failed to initialize coverage maps, base test chain cloning encountered error: %v", err) +// RandomMutationTargetSequence returns a weighted random call sequence from the Corpus, or an error if one occurs. +func (c *Corpus) RandomMutationTargetSequence() (calls.CallSequence, error) { + // If we didn't initialize a chooser, return an error + if c.mutationTargetSequenceChooser == nil { + return nil, fmt.Errorf("corpus could not return a random call sequence because the corpus was not initialized") } - // Next we replay every call sequence, checking its validity on this chain and measuring coverage. If the sequence - // is valid, we add it to our weighted list for future random selection. + // Pick a random call sequence, then clone it before returning it, so the original is untainted. + seq, err := c.mutationTargetSequenceChooser.Choose() + if seq == nil || err != nil { + return nil, err + } + return seq.Clone() +} +// initializeSequences is a helper method for Initialize. It validates a list of call sequence files on a given +// chain, using the map of deployed contracts (e.g. to check for non-existent method called, due to code changes). +// Valid call sequences are added to the list of un-executed sequences the fuzzer should execute first. +// If this sequence list being initialized is for use with mutations, it is added to the mutationTargetSequenceChooser. +// Returns an error if one occurs. +func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSequence], testChain *chain.TestChain, deployedContracts map[common.Address]*contracts.Contract, useInMutations bool) error { // Cache current HeadBlockNumber so that you can reset back to it after every sequence baseBlockNumber := testChain.HeadBlockNumber() // Loop for each sequence - for _, sequenceFileData := range c.callSequences { + var err error + for _, sequenceFileData := range sequenceFiles.files { // Unwrap the underlying sequence. sequence := sequenceFileData.data @@ -230,7 +194,7 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions // Update our coverage maps for each call executed in our sequence. lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] covMaps := coverage.GetCoverageTracerResults(lastExecutedSequenceElement.ChainReference.MessageResults()) - _, covErr := c.coverageMaps.Update(covMaps) + _, _, covErr := c.coverageMaps.Update(covMaps) if covErr != nil { return true, covErr } @@ -245,13 +209,14 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions return fmt.Errorf("failed to initialize coverage maps from corpus, encountered an error while executing call sequence: %v\n", err) } - // If the sequence was replayed successfully, we add a weighted choice for it, for future selection. If it was - // not, we simply exclude it from our chooser and print a warning. + // If the sequence was replayed successfully, we add it. If it was not, we exclude it with a warning. if sequenceInvalidError == nil { - c.weightedCallSequenceChooser.AddChoices(randomutils.NewWeightedRandomChoice[calls.CallSequence](sequence, big.NewInt(1))) + if useInMutations && c.mutationTargetSequenceChooser != nil { + c.mutationTargetSequenceChooser.AddChoices(randomutils.NewWeightedRandomChoice[calls.CallSequence](sequence, big.NewInt(1))) + } c.unexecutedCallSequences = append(c.unexecutedCallSequences, sequence) } else { - fmt.Printf("corpus item '%v' disabled due to error when replaying it: %v\n", sequenceFileData.filePath, sequenceInvalidError) + fmt.Printf("corpus item '%v' disabled due to error when replaying it: %v\n", sequenceFileData.fileName, sequenceInvalidError) } // Revert chain state to our starting point to test the next sequence. @@ -263,22 +228,96 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions return nil } -// AddCallSequence adds a call sequence to the corpus and returns an error in case of an issue -func (c *Corpus) AddCallSequence(seq calls.CallSequence, weight *big.Int, flushImmediately bool) error { +// 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 { + // Acquire our call sequences lock during the duration of this method. + c.callSequencesLock.Lock() + defer c.callSequencesLock.Unlock() + + // Initialize our call sequence structures. + c.mutationTargetSequenceChooser = randomutils.NewWeightedRandomChooser[calls.CallSequence]() + c.unexecutedCallSequences = make([]calls.CallSequence, 0) + + // Create a coverage tracer to track coverage across all blocks. + c.coverageMaps = coverage.NewCoverageMaps() + coverageTracer := coverage.NewCoverageTracer() + + // Create our structure and event listeners to track deployed contracts + deployedContracts := make(map[common.Address]*contracts.Contract, 0) + + // Clone our test chain, adding listeners for contract deployment events from genesis. + testChain, err := baseTestChain.Clone(func(newChain *chain.TestChain) error { + // After genesis, prior to adding other blocks, we attach our coverage tracer + newChain.AddTracer(coverageTracer, true, false) + + // We also track any contract deployments, so we can resolve contract/method definitions for corpus call + // sequences. + newChain.Events.ContractDeploymentAddedEventEmitter.Subscribe(func(event chain.ContractDeploymentsAddedEvent) error { + matchedContract := contractDefinitions.MatchBytecode(event.Contract.InitBytecode, event.Contract.RuntimeBytecode) + if matchedContract != nil { + deployedContracts[event.Contract.Address] = matchedContract + } + return nil + }) + newChain.Events.ContractDeploymentRemovedEventEmitter.Subscribe(func(event chain.ContractDeploymentsRemovedEvent) error { + delete(deployedContracts, event.Contract.Address) + return nil + }) + return nil + }) + if err != nil { + return 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. + c.coverageMaps = coverage.NewCoverageMaps() + for _, block := range testChain.CommittedBlocks() { + for _, messageResults := range block.MessageResults { + covMaps := coverage.GetCoverageTracerResults(messageResults) + _, _, covErr := c.coverageMaps.Update(covMaps) + if covErr != nil { + return err + } + } + } + + // Next we replay every call sequence, checking its validity on this chain and measuring coverage. Valid sequences + // are added to the corpus for mutations, re-execution, etc. + err = c.initializeSequences(c.mutableSequenceFiles, testChain, deployedContracts, true) + if err != nil { + return err + } + err = c.initializeSequences(c.immutableSequenceFiles, testChain, deployedContracts, false) + if err != nil { + return err + } + err = c.initializeSequences(c.testResultSequenceFiles, testChain, deployedContracts, false) + if err != nil { + return err + } + + return nil +} + +// addCallSequence adds a call sequence to the corpus in a given corpus directory. +// Returns an error, if one occurs. +func (c *Corpus) addCallSequence(sequenceFiles *corpusDirectory[calls.CallSequence], sequence calls.CallSequence, useInMutations bool, mutationChooserWeight *big.Int, flushImmediately bool) error { // Acquire a thread lock during modification of call sequence lists. c.callSequencesLock.Lock() // Check if call sequence has been added before, if so, exit without any action. - seqHash, err := seq.Hash() + seqHash, err := sequence.Hash() if err != nil { return err } // Verify no existing corpus item hash this same hash. - for _, existingSeq := range c.callSequences { + for _, existingSeq := range sequenceFiles.files { // Calculate the existing sequence hash existingSeqHash, err := existingSeq.data.Hash() if err != nil { + c.callSequencesLock.Unlock() return err } @@ -289,18 +328,19 @@ func (c *Corpus) AddCallSequence(seq calls.CallSequence, weight *big.Int, flushI } } - // Update our sequences with the new entry. - c.callSequences = append(c.callSequences, &corpusFile[calls.CallSequence]{ - filePath: "", - data: seq, - }) + // Update our corpus directory with the new entry. + fileName := fmt.Sprintf("%v-%v.json", time.Now().UnixNano(), uuid.New().String()) + err = sequenceFiles.addFile(fileName, sequence) + if err != nil { + return err + } - // If we have initialized a chooser, add our call sequence item to it. - if c.weightedCallSequenceChooser != nil { - if weight == nil { - weight = big.NewInt(1) + // If we want to use this sequence in mutations and initialized a chooser, add our call sequence item to it. + if useInMutations && c.mutationTargetSequenceChooser != nil { + if mutationChooserWeight == nil { + mutationChooserWeight = big.NewInt(1) } - c.weightedCallSequenceChooser.AddChoices(randomutils.NewWeightedRandomChoice[calls.CallSequence](seq, weight)) + c.mutationTargetSequenceChooser.AddChoices(randomutils.NewWeightedRandomChoice[calls.CallSequence](sequence, mutationChooserWeight)) } // Unlock now, as flushing will lock on its own. @@ -314,18 +354,26 @@ func (c *Corpus) AddCallSequence(seq calls.CallSequence, weight *big.Int, flushI } } -// AddCallSequenceIfCoverageChanged checks if the most recent call executed in the provided call sequence achieved +// AddTestResultCallSequence adds a call sequence recorded to the corpus due to a test case provider flagging it to be +// recorded. +// Returns an error, if one occurs. +func (c *Corpus) AddTestResultCallSequence(callSequence calls.CallSequence, mutationChooserWeight *big.Int, flushImmediately bool) error { + return c.addCallSequence(c.testResultSequenceFiles, callSequence, false, mutationChooserWeight, flushImmediately) +} + +// CheckSequenceCoverageAndUpdate checks if the most recent call executed in the provided call sequence achieved // coverage the Corpus did not with any of its call sequences. If it did, the call sequence is added to the corpus // and the Corpus coverage maps are updated accordingly. // Returns an error if one occurs. -func (c *Corpus) AddCallSequenceIfCoverageChanged(callSequence calls.CallSequence, weight *big.Int, flushImmediately bool) error { +func (c *Corpus) CheckSequenceCoverageAndUpdate(callSequence calls.CallSequence, mutationChooserWeight *big.Int, flushImmediately bool) error { // If we have coverage-guided fuzzing disabled or no calls in our sequence, there is nothing to do. if len(callSequence) == 0 { return nil } // Obtain our coverage maps for our last call. - lastCallChainReference := callSequence[len(callSequence)-1].ChainReference + lastCall := callSequence[len(callSequence)-1] + lastCallChainReference := lastCall.ChainReference lastMessageResult := lastCallChainReference.Block.MessageResults[lastCallChainReference.TransactionIndex] lastMessageCoverageMaps := coverage.GetCoverageTracerResults(lastMessageResult) @@ -338,13 +386,23 @@ func (c *Corpus) AddCallSequenceIfCoverageChanged(callSequence calls.CallSequenc coverage.RemoveCoverageTracerResults(lastMessageResult) // Merge the coverage maps into our total coverage maps and check if we had an update. - coverageUpdated, err := c.coverageMaps.Update(lastMessageCoverageMaps) + coverageUpdated, revertedCoverageUpdated, err := c.coverageMaps.Update(lastMessageCoverageMaps) if err != nil { return err } + + // If we had an increase in non-reverted or reverted coverage, we save the sequence. + // Note: We only want to save the sequence once. We're most interested if it can be used for mutations first. if coverageUpdated { - // New coverage has been found with this call sequence, so we add it to the corpus. - err = c.AddCallSequence(callSequence, weight, flushImmediately) + // If we achieved new non-reverting coverage, save this sequence for mutation purposes. + err = c.addCallSequence(c.mutableSequenceFiles, callSequence, true, mutationChooserWeight, flushImmediately) + if err != nil { + return err + } + } else if revertedCoverageUpdated { + // If we did not achieve new successful coverage, but achieved an increase in reverted coverage, save this + // sequence for non-mutation purposes. + err = c.addCallSequence(c.immutableSequenceFiles, callSequence, false, mutationChooserWeight, flushImmediately) if err != nil { return err } @@ -352,21 +410,6 @@ func (c *Corpus) AddCallSequenceIfCoverageChanged(callSequence calls.CallSequenc return nil } -// RandomCallSequence returns a weighted random call sequence from the Corpus, or an error if one occurs. -func (c *Corpus) RandomCallSequence() (calls.CallSequence, error) { - // If we didn't initialize a chooser, return an error - if c.weightedCallSequenceChooser == nil { - return nil, fmt.Errorf("corpus could not return a random call sequence because the corpus was not initialized") - } - - // Pick a random call sequence, then clone it before returning it, so the original is untainted. - seq, err := c.weightedCallSequenceChooser.Choose() - if seq == nil || err != nil { - return nil, err - } - return seq.Clone() -} - // UnexecutedCallSequence returns a call sequence loaded from disk which has not yet been returned by this method. // It is intended to be used by the fuzzer to run all un-executed call sequences (without mutations) to check for test // failures. If a call sequence is returned, it will not be returned by this method again. @@ -408,35 +451,23 @@ func (c *Corpus) Flush() error { c.callSequencesLock.Lock() defer c.callSequencesLock.Unlock() - // Ensure the corpus directories exists. - err := utils.MakeDirectory(c.storageDirectory) + // Write mutation target call sequences. + err := c.mutableSequenceFiles.writeFiles() if err != nil { return err } - err = utils.MakeDirectory(c.CallSequencesDirectory()) + + // Write test case provider related call sequences (test failures, etc). + err = c.testResultSequenceFiles.writeFiles() if err != nil { return err } - // Write all call sequences to disk - // TODO: This can be optimized by storing/indexing unwritten sequences separately and only iterating over those. - for _, sequenceFile := range c.callSequences { - if sequenceFile.filePath == "" { - // Determine the file path to write this to. - sequenceFile.filePath = filepath.Join(c.CallSequencesDirectory(), uuid.New().String()+".json") - - // Marshal the call sequence - jsonEncodedData, err := json.MarshalIndent(sequenceFile.data, "", " ") - if err != nil { - return err - } - - // Write the JSON encoded data. - err = os.WriteFile(sequenceFile.filePath, jsonEncodedData, os.ModePerm) - if err != nil { - return fmt.Errorf("An error occurred while writing call sequence to disk: %v\n", err) - } - } + // Write other call sequences. + err = c.immutableSequenceFiles.writeFiles() + if err != nil { + return err } + return nil } diff --git a/fuzzing/corpus/corpus_files.go b/fuzzing/corpus/corpus_files.go new file mode 100644 index 00000000..c1354f61 --- /dev/null +++ b/fuzzing/corpus/corpus_files.go @@ -0,0 +1,187 @@ +package corpus + +import ( + "encoding/json" + "fmt" + "github.com/crytic/medusa/utils" + "os" + "path/filepath" + "strings" + "sync" +) + +// corpusFile represents corpus data and its state on the filesystem. +type corpusFile[T any] struct { + // fileName describes the filename the file should be written with, in the corpusDirectory.path. + fileName string + + // data describes an object whose data should be written to the file. + data T + + // writtenToDisk indicates whether the corpus item has been flushed to disk yet. If this is false, it signals that + // the data should be written or overwritten on disk. + writtenToDisk bool +} + +// corpusDirectory is a provider for corpusFile items in a given directory, offering read/write operations to +// automatically JSON serialize/deserialize items of a given type to a directory. +type corpusDirectory[T any] struct { + // path signifies the directory to store corpusFile items within. If the path is an empty string, files + // will not be read from, or written to disk. + path string + + // files represents the corpusFile items stored/to be stored in the specified directory. + files []*corpusFile[T] + + // filesLock represents a thread lock used when editing files. + filesLock sync.Mutex +} + +// newCorpusDirectory returns a new corpusDirectory with the provided directory path set. +// If the directory path is an empty string, then files will not be read from, or written to disk. +func newCorpusDirectory[T any](path string) *corpusDirectory[T] { + return &corpusDirectory[T]{ + path: path, + files: make([]*corpusFile[T], 0), + } +} + +// addFile adds a given file to the file list (to later be written to the directory if a path was provided). +// If a corpusFile exists with the provided file name, it is overwritten in the list (but not yet flushed to disk). +// If a corpusFile does not exist with the provided file name, it is added. +// Returns an error, if one occurred. +func (cd *corpusDirectory[T]) addFile(fileName string, data T) error { + // Lock to avoid concurrency issues when accessing the files list + cd.filesLock.Lock() + defer cd.filesLock.Unlock() + + // First we make sure this file doesn't already exist, if it does, we overwrite its data and mark it unwritten. + lowerFileName := strings.ToLower(fileName) + for i := 0; i < len(cd.files); i++ { + if lowerFileName == strings.ToLower(cd.files[i].fileName) { + cd.files[i].data = data + cd.files[i].writtenToDisk = false + return nil + } + } + + // If the file otherwise did not exist, we add it. + cd.files = append(cd.files, &corpusFile[T]{ + fileName: fileName, + data: data, + writtenToDisk: false, + }) + return nil +} + +// removeFile removes a given file from the file list. This does not delete it from disk. +// Returns a boolean indicating if a corpusFile with the provided file name was found and removed. +func (cd *corpusDirectory[T]) removeFile(fileName string) bool { + // Lock to avoid concurrency issues when accessing the files list + cd.filesLock.Lock() + defer cd.filesLock.Unlock() + + // If we find the filename, remove it from our list of files. + lowerFileName := strings.ToLower(fileName) + for i := 0; i < len(cd.files); i++ { + if lowerFileName == strings.ToLower(cd.files[i].fileName) { + cd.files = append(cd.files[:i], cd.files[i+1:]...) + return true + } + } + return false +} + +// readFiles takes a provided glob pattern representing files to parse within the corpusDirectory.path. +// It parses any matching file into a corpusFile and adds it to the corpusDirectory. +// Returns an error, if one occurred. +func (cd *corpusDirectory[T]) readFiles(filePattern string) error { + // If our directory path specified is empty, we do not read/write to disk. + if cd.path == "" { + return nil + } + + // Discover all corpus files in the given directory. + filePaths, err := filepath.Glob(filepath.Join(cd.path, filePattern)) + if err != nil { + return err + } + + // Refresh our files list + cd.files = make([]*corpusFile[T], 0) + + // Loop for every file path provided + for _, filePath := range filePaths { + // Read the file data. + b, err := os.ReadFile(filePath) + if err != nil { + return err + } + + // Parse the call sequence data. + var fileData T + err = json.Unmarshal(b, &fileData) + if err != nil { + return err + } + + // Add entry to corpus + cd.files = append(cd.files, &corpusFile[T]{ + fileName: filepath.Base(filePath), + data: fileData, + writtenToDisk: true, + }) + } + return nil +} + +// writeFiles flushes all corpusDirectory.files to disk, if they have corpusFile.writtenToDisk set as false. +// It then sets corpusFile.writtenToDisk as true for each flushed to disk. +// Returns an error, if one occurred. +func (cd *corpusDirectory[T]) writeFiles() error { + // TODO: This can be optimized by storing/indexing unwritten sequences separately and only iterating over those. + + // If our directory path is empty, we do not write anything. + if cd.path == "" { + return nil + } + + // Lock to avoid concurrency issues when accessing the files list + cd.filesLock.Lock() + defer cd.filesLock.Unlock() + + // Ensure the corpus directory path exists. + err := utils.MakeDirectory(cd.path) + if err != nil { + return err + } + + // For each file which does not have an assigned file path yet, we flush it to disk. + for _, file := range cd.files { + if !file.writtenToDisk { + // If we don't have a filename, throw an error. + if len(file.fileName) == 0 { + return fmt.Errorf("failed to flush corpus item to disk as it does not have a filename") + } + + // Determine the file path to write this to. + filePath := filepath.Join(cd.path, file.fileName) + + // Marshal the data + jsonEncodedData, err := json.MarshalIndent(file.data, "", " ") + if err != nil { + return err + } + + // Write the JSON encoded data. + err = os.WriteFile(filePath, jsonEncodedData, os.ModePerm) + if err != nil { + return fmt.Errorf("An error occurred while writing corpus data to file: %v\n", err) + } + + // Update our written to disk status. + file.writtenToDisk = true + } + } + return nil +} diff --git a/fuzzing/corpus/corpus_test.go b/fuzzing/corpus/corpus_test.go index f9c5751a..3f32eefe 100644 --- a/fuzzing/corpus/corpus_test.go +++ b/fuzzing/corpus/corpus_test.go @@ -23,7 +23,7 @@ func getMockSimpleCorpus(minSequences int, maxSequences, minBlocks int, maxBlock // Add the requested number of entries. numSequences := minSequences + (rand.Int() % (maxSequences - minSequences)) for i := 0; i < numSequences; i++ { - err := corpus.AddCallSequence(getMockCallSequence(minBlocks+(rand.Int()%(maxBlocks-minBlocks))), nil, false) + err := corpus.addCallSequence(corpus.mutableSequenceFiles, getMockCallSequence(minBlocks+(rand.Int()%(maxBlocks-minBlocks))), true, nil, false) if err != nil { return nil, err } @@ -100,9 +100,9 @@ func TestCorpusReadWrite(t *testing.T) { assert.NoError(t, err) // Ensure that there are the correct number of call sequence files - matches, err := filepath.Glob(filepath.Join(corpus.CallSequencesDirectory(), "*.json")) + matches, err := filepath.Glob(filepath.Join(corpus.mutableSequenceFiles.path, "*.json")) assert.NoError(t, err) - assert.EqualValues(t, corpus.CallSequenceCount(), len(matches), "Did not find numEntries matches") + assert.EqualValues(t, len(corpus.mutableSequenceFiles.files), len(matches)) // Wipe corpus clean so that you can now read it in from disk corpus, err = NewCorpus("corpus") @@ -124,7 +124,7 @@ func TestCorpusCallSequenceMarshaling(t *testing.T) { // Run the test in our temporary test directory to avoid artifact pollution. testutils.ExecuteInDirectory(t, t.TempDir(), func() { // For each entry, marshal it and then unmarshal the byte array - for _, entryFile := range corpus.callSequences { + for _, entryFile := range corpus.mutableSequenceFiles.files { // Marshal the entry b, err := json.Marshal(entryFile.data) assert.NoError(t, err) @@ -137,5 +137,11 @@ func TestCorpusCallSequenceMarshaling(t *testing.T) { // Check equality testCorpusCallSequencesEqual(t, entryFile.data, sameEntry) } + + // Remove all items + for i := 0; i < len(corpus.mutableSequenceFiles.files); { + corpus.mutableSequenceFiles.removeFile(corpus.mutableSequenceFiles.files[i].fileName) + } + assert.Empty(t, corpus.mutableSequenceFiles.files) }) } diff --git a/fuzzing/coverage/coverage_maps.go b/fuzzing/coverage/coverage_maps.go index e9d54811..71ed9852 100644 --- a/fuzzing/coverage/coverage_maps.go +++ b/fuzzing/coverage/coverage_maps.go @@ -3,29 +3,32 @@ package coverage import ( "bytes" "fmt" + compilationTypes "github.com/crytic/medusa/compilation/types" + "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "sync" ) // CoverageMaps represents a data structure used to identify instruction execution coverage of various smart contracts // across a transaction or multiple transactions. type CoverageMaps struct { - // maps represents a structure used to track every codeCoverageData by a given deployed address/code hash. - maps map[common.Address]map[common.Hash]*codeCoverageData + // maps represents a structure used to track every ContractCoverageMap by a given deployed address/lookup hash. + maps map[common.Hash]map[common.Address]*ContractCoverageMap // cachedCodeAddress represents the last code address which coverage was updated for. This is used to prevent an // expensive lookup in maps. If cachedCodeHash does not match the current code address for which we are updating // coverage for, it, along with other cache variables are updated. cachedCodeAddress common.Address - // cachedCodeHash represents the last code hash which coverage was updated for. This is used to prevent an expensive - // lookup in maps. If cachedCodeHash does not match the current code hash for which we are updating coverage for, - // it, along with other cache variables are updated. + // cachedCodeHash represents the last lookup hash which coverage was updated for. This is used to prevent an + // expensive lookup in maps. If cachedCodeHash does not match the current code hash which we are updating + // coverage for, it, along with other cache variables are updated. cachedCodeHash common.Hash // cachedMap represents the last coverage map which was updated. If the coverage to update resides at the // cachedCodeAddress and matches the cachedCodeHash, then this map is used to avoid an expensive lookup into maps. - cachedMap *codeCoverageData + cachedMap *ContractCoverageMap // updateLock is a lock to offer concurrent thread safety for map accesses. updateLock sync.Mutex @@ -40,15 +43,91 @@ func NewCoverageMaps() *CoverageMaps { // Reset clears the coverage state for the CoverageMaps. func (cm *CoverageMaps) Reset() { - cm.maps = make(map[common.Address]map[common.Hash]*codeCoverageData) + cm.maps = make(map[common.Hash]map[common.Address]*ContractCoverageMap) + cm.cachedCodeAddress = common.Address{} + cm.cachedCodeHash = common.Hash{} + cm.cachedMap = nil +} + +// Equal checks whether two coverage maps are the same. Equality is determined if the keys and values are all the same. +func (cm *CoverageMaps) Equal(b *CoverageMaps) bool { + // Iterate through all maps + for codeHash, mapsByAddressA := range cm.maps { + mapsByAddressB, ok := b.maps[codeHash] + // Hash is not in b - we're done + if !ok { + return false + } + for codeAddress, coverageMapA := range mapsByAddressA { + coverageMapB, ok := mapsByAddressB[codeAddress] + // Address is not in b - we're done + if !ok { + return false + } + + // Verify the equality of the map data. + if !coverageMapA.Equal(coverageMapB) { + return false + } + } + } + return true +} + +// getContractCoverageMapHash obtain the hash used to look up a given contract's ContractCoverageMap. +// If this is init bytecode, metadata and abi arguments will attempt to be stripped, then a hash is computed. +// If this is runtime bytecode, the metadata ipfs/swarm hash will be used if available, otherwise the bytecode +// is hashed. +// Returns the resulting lookup hash. +func getContractCoverageMapHash(bytecode []byte, init bool) common.Hash { + // If available, the metadata code hash should be unique and reliable to use above all (for runtime bytecode). + if !init { + metadata := compilationTypes.ExtractContractMetadata(bytecode) + if metadata != nil { + metadataHash := metadata.ExtractBytecodeHash() + if metadataHash != nil { + return common.BytesToHash(metadataHash) + } + } + } + + // Otherwise, we use the hash of the bytecode after attempting to strip metadata (and constructor args). + strippedBytecode := compilationTypes.RemoveContractMetadata(bytecode) + return crypto.Keccak256Hash(strippedBytecode) +} + +// GetContractCoverageMap obtains a total coverage map representing coverage for the provided bytecode. +// If the provided bytecode could not find coverage maps, nil is returned. +// Returns the total coverage map, or an error if one occurs. +func (cm *CoverageMaps) GetContractCoverageMap(bytecode []byte, init bool) (*ContractCoverageMap, error) { + // Obtain the lookup hash + hash := getContractCoverageMapHash(bytecode, init) + + // Acquire our thread lock and defer our unlocking for when we exit this method + cm.updateLock.Lock() + defer cm.updateLock.Unlock() + + // Loop through all coverage maps for this hash and collect our total coverage. + if coverageByAddresses, ok := cm.maps[hash]; ok { + totalCoverage := newContractCoverageMap() + for _, coverage := range coverageByAddresses { + _, _, err := totalCoverage.update(coverage) + if err != nil { + return nil, err + } + } + return totalCoverage, nil + } else { + return nil, nil + } } -// Update updates the current coverage maps with the provided ones. It returns a boolean indicating whether -// new coverage was achieved, or an error if one was encountered. -func (cm *CoverageMaps) Update(coverageMaps *CoverageMaps) (bool, error) { +// Update updates the current coverage maps with the provided ones. +// Returns two booleans indicating whether successful or reverted coverage changed, or an error if one occurred. +func (cm *CoverageMaps) Update(coverageMaps *CoverageMaps) (bool, bool, error) { // If our maps provided are nil, do nothing if coverageMaps == nil { - return false, nil + return false, false, nil } // Acquire our thread lock and defer our unlocking for when we exit this method @@ -56,39 +135,42 @@ func (cm *CoverageMaps) Update(coverageMaps *CoverageMaps) (bool, error) { defer cm.updateLock.Unlock() // Create a boolean indicating whether we achieved new coverage - changed := false + successCoverageChanged := false + revertedCoverageChanged := false // Loop for each coverage map provided - for codeAddressToMerge, mapsByCodeHashToMerge := range coverageMaps.maps { - for codeHashToMerge, coverageMapToMerge := range mapsByCodeHashToMerge { - // If a coverage map lookup for this code address doesn't exist, create the mapping. - mapsByCodeHash, codeAddressExists := cm.maps[codeAddressToMerge] - if !codeAddressExists { - mapsByCodeHash = make(map[common.Hash]*codeCoverageData) - cm.maps[codeAddressToMerge] = mapsByCodeHash + for codeHash, mapsByAddressToMerge := range coverageMaps.maps { + for codeAddress, coverageMapToMerge := range mapsByAddressToMerge { + // If a coverage map lookup for this code hash doesn't exist, create the mapping. + mapsByAddress, codeHashExists := cm.maps[codeHash] + if !codeHashExists { + mapsByAddress = make(map[common.Address]*ContractCoverageMap) + cm.maps[codeHash] = mapsByAddress } - // If a coverage map for this code hash already exists in our current mapping, update it with the one + // If a coverage map for this address already exists in our current mapping, update it with the one // to merge. If it doesn't exist, set it to the one to merge. - if existingCoverageMap, codeHashExists := mapsByCodeHash[codeHashToMerge]; codeHashExists { - coverageMapChanged, err := existingCoverageMap.updateCodeCoverageData(coverageMapToMerge) - changed = changed || coverageMapChanged + if existingCoverageMap, codeAddressExists := mapsByAddress[codeAddress]; codeAddressExists { + sChanged, rChanged, err := existingCoverageMap.update(coverageMapToMerge) + successCoverageChanged = successCoverageChanged || sChanged + revertedCoverageChanged = revertedCoverageChanged || rChanged if err != nil { - return changed, err + return successCoverageChanged, revertedCoverageChanged, err } } else { - mapsByCodeHash[codeHashToMerge] = coverageMapToMerge - changed = true + mapsByAddress[codeAddress] = coverageMapToMerge + successCoverageChanged = coverageMapToMerge.successfulCoverage != nil + revertedCoverageChanged = coverageMapToMerge.revertedCoverage != nil } } } // Return our results - return changed, nil + return successCoverageChanged, revertedCoverageChanged, nil } -// SetCoveredAt sets the coverage state of a given program counter location within a codeCoverageData. -func (cm *CoverageMaps) SetCoveredAt(codeAddress common.Address, codeHash common.Hash, init bool, codeSize int, pc uint64) (bool, error) { +// 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) { // If the code size is zero, do nothing if codeSize == 0 { return false, nil @@ -98,152 +180,198 @@ func (cm *CoverageMaps) SetCoveredAt(codeAddress common.Address, codeHash common var ( addedNewMap bool changedInMap bool - coverageMap *codeCoverageData + coverageMap *ContractCoverageMap err error ) - // Try to obtain a coverage map for the given code hash from our cache - if cm.cachedMap != nil && cm.cachedCodeAddress == codeAddress && cm.cachedCodeHash == codeHash { + // Try to obtain a coverage map from our cache + if cm.cachedMap != nil && cm.cachedCodeAddress == codeAddress && cm.cachedCodeHash == codeLookupHash { coverageMap = cm.cachedMap } else { - // If a coverage map lookup for this code address doesn't exist, create the mapping. - coverageMapsByCodeHash, codeAddressExists := cm.maps[codeAddress] - if !codeAddressExists { - coverageMapsByCodeHash = make(map[common.Hash]*codeCoverageData) - cm.maps[codeAddress] = coverageMapsByCodeHash + // If a coverage map lookup for this code hash doesn't exist, create the mapping. + mapsByCodeAddress, codeHashExists := cm.maps[codeLookupHash] + if !codeHashExists { + mapsByCodeAddress = make(map[common.Address]*ContractCoverageMap) + cm.maps[codeLookupHash] = mapsByCodeAddress } - // Obtain the coverage map for this code hash if it already exists. If it does not, create a new one. - if existingCoverageMap, codeHashExists := coverageMapsByCodeHash[codeHash]; codeHashExists { + // Obtain the coverage map for this code address if it already exists. If it does not, create a new one. + if existingCoverageMap, codeAddressExists := mapsByCodeAddress[codeAddress]; codeAddressExists { coverageMap = existingCoverageMap } else { - coverageMap = &codeCoverageData{ - initBytecodeCoverageData: nil, - deployedBytecodeCoverageData: nil, - } - cm.maps[codeAddress][codeHash] = coverageMap + coverageMap = newContractCoverageMap() + cm.maps[codeLookupHash][codeAddress] = coverageMap addedNewMap = true } // Set our cached variables for faster coverage setting next time this method is called. cm.cachedMap = coverageMap - cm.cachedCodeHash = codeHash + cm.cachedCodeHash = codeLookupHash cm.cachedCodeAddress = codeAddress } // Set our coverage in the map and return our change state - changedInMap, err = coverageMap.setCodeCoverageDataAt(init, codeSize, pc) + changedInMap, err = coverageMap.setCoveredAt(codeSize, pc) return addedNewMap || changedInMap, err } -// Equals checks whether two coverage maps are the same. Equality is determined if the keys and values are all the same. -func (a *CoverageMaps) Equals(b *CoverageMaps) bool { - // Note: the `map` field is what is being tested for equality. Not the cached values +// RevertAll sets all coverage in the coverage map as reverted coverage. Reverted coverage is updated with successful +// coverage, the successful coverage is cleared. +// Returns a boolean indicating whether reverted coverage increased, and an error if one occurred. +func (cm *CoverageMaps) RevertAll() (bool, error) { + // Acquire our thread lock and defer our unlocking for when we exit this method + cm.updateLock.Lock() + defer cm.updateLock.Unlock() - // Iterate through all maps - for addr, aHashToCoverage := range a.maps { - bHashToCoverage, ok := b.maps[addr] - // Address is not in b - we're done - if !ok { - return false - } - for hash, aCoverage := range aHashToCoverage { - bCoverage, ok := bHashToCoverage[hash] - // Hash is not in b - we're done - if !ok { - return false - } - // Compare that the deployed bytecode coverages are the same - equal := bytes.Compare(aCoverage.deployedBytecodeCoverageData, bCoverage.deployedBytecodeCoverageData) - if equal != 0 { - return false - } - // Compare that the init bytecode coverages are the same - equal = bytes.Compare(aCoverage.initBytecodeCoverageData, bCoverage.initBytecodeCoverageData) - if equal != 0 { - return false + // Define a variable to track if our reverted coverage changed. + revertedCoverageChanged := false + + // Loop for each coverage map provided + for _, mapsByAddressToMerge := range cm.maps { + for _, contractCoverageMap := range mapsByAddressToMerge { + // Update our reverted coverage with the (previously thought to be) successful coverage. + changed, err := contractCoverageMap.revertedCoverage.update(contractCoverageMap.successfulCoverage) + revertedCoverageChanged = revertedCoverageChanged || changed + if err != nil { + return revertedCoverageChanged, err } + + // Clear our successful coverage, as these maps were marked as reverted. + contractCoverageMap.successfulCoverage.Reset() } } - return true + return revertedCoverageChanged, nil } -// codeCoverageData represents a data structure used to identify instruction execution coverage of contract byte code. -type codeCoverageData struct { - // initBytecodeCoverageData represents a list of bytes for each byte of a contract's init bytecode. Non-zero values - // indicate the program counter executed an instruction at that offset. - initBytecodeCoverageData []byte - // deployedBytecodeCoverageData represents a list of bytes for each byte of a contract's deployed bytecode. Non-zero - // values indicate the program counter executed an instruction at that offset. - deployedBytecodeCoverageData []byte +// 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 + // deemed successful. + successfulCoverage *CoverageMapBytecodeData + + // revertedCoverage represents coverage for the contract bytecode, which encountered a revert. + revertedCoverage *CoverageMapBytecodeData } -// updateCodeCoverageData creates updates the current coverage map with the provided one. It returns a boolean indicating whether -// new coverage was achieved, or an error if one was encountered. -func (cm *codeCoverageData) updateCodeCoverageData(coverageMap *codeCoverageData) (bool, error) { - // Define our return variable - changed := false +// newContractCoverageMap creates and returns a new ContractCoverageMap. +func newContractCoverageMap() *ContractCoverageMap { + return &ContractCoverageMap{ + successfulCoverage: &CoverageMapBytecodeData{}, + revertedCoverage: &CoverageMapBytecodeData{}, + } +} - // Update our init bytecode coverage data. - if coverageMap.initBytecodeCoverageData != nil { - if cm.initBytecodeCoverageData == nil { - cm.initBytecodeCoverageData = coverageMap.initBytecodeCoverageData - changed = true - } else { - // Update each byte which represents a position in the bytecode which was covered. We ignore any size - // differences as init bytecode can have arbitrary length arguments appended. - for i := 0; i < len(cm.initBytecodeCoverageData) || i < len(coverageMap.initBytecodeCoverageData); i++ { - if cm.initBytecodeCoverageData[i] == 0 && coverageMap.initBytecodeCoverageData[i] != 0 { - cm.initBytecodeCoverageData[i] = 1 - changed = true - } - } - } +// Equal checks whether the provided ContractCoverageMap contains the same data as the current one. +// Returns a boolean indicating whether the two maps match. +func (cm *ContractCoverageMap) Equal(b *ContractCoverageMap) bool { + // Compare both our underlying bytecode coverage maps. + return cm.successfulCoverage.Equal(b.successfulCoverage) && cm.revertedCoverage.Equal(b.revertedCoverage) +} + +// update creates 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 + successfulCoverageChanged, err := cm.successfulCoverage.update(coverageMap.successfulCoverage) + if err != nil { + return false, false, err + } + + // Update our reverted coverage data + revertedCoverageChanged, err := cm.revertedCoverage.update(coverageMap.revertedCoverage) + if err != nil { + return successfulCoverageChanged, false, err + } + + return successfulCoverageChanged, revertedCoverageChanged, nil +} + +// setCoveredAt sets the coverage state 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) { + // Set our coverage data for the successful path. + return cm.successfulCoverage.setCoveredAt(codeSize, pc) +} + +// CoverageMapBytecodeData represents a data structure used to identify instruction execution coverage of some init +// or runtime bytecode. +type CoverageMapBytecodeData struct { + executedFlags []byte +} + +// Reset resets the bytecode coverage map data to be empty. +func (cm *CoverageMapBytecodeData) Reset() { + cm.executedFlags = nil +} + +// Equal checks whether the provided CoverageMapBytecodeData contains the same data as the current one. +// Returns a boolean indicating whether the two maps match. +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]) +} + +// 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 { + // If the coverage map bytecode data is nil, this is not covered. + if cm == nil { + return false + } + + // 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 + } + + // Otherwise, return the execution flag + return cm.executedFlags[pc] != 0 +} + +// update creates updates 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 + if coverageMap.executedFlags == nil { + return false, nil + } + + // If the current map has no execution data, simply set it to the provided one. + if cm.executedFlags == nil { + cm.executedFlags = coverageMap.executedFlags + return true, nil } - // Update our deployed bytecode coverage data. - if coverageMap.deployedBytecodeCoverageData != nil { - if cm.deployedBytecodeCoverageData == nil { - cm.deployedBytecodeCoverageData = coverageMap.deployedBytecodeCoverageData + // Update each byte which represents a position in the bytecode which was covered. We ignore any size + // differences as init bytecode can have arbitrary length arguments appended. + changed := false + for i := 0; i < len(cm.executedFlags) || i < len(coverageMap.executedFlags); i++ { + if cm.executedFlags[i] == 0 && coverageMap.executedFlags[i] != 0 { + cm.executedFlags[i] = 1 changed = true - } else { - // Update each byte which represents a position in the bytecode which was covered. - for i := 0; i < len(cm.deployedBytecodeCoverageData); i++ { - if cm.deployedBytecodeCoverageData[i] == 0 && coverageMap.deployedBytecodeCoverageData[i] != 0 { - cm.deployedBytecodeCoverageData[i] = 1 - changed = true - } - } } } - return changed, nil } -// setCodeCoverageDataAt sets the coverage state of a given program counter location within a codeCoverageData. -func (cm *codeCoverageData) setCodeCoverageDataAt(init bool, codeSize int, pc uint64) (bool, error) { - // Obtain our coverage data depending on if we're initializing/deploying a contract now. If coverage data doesn't - // exist, we create it. - var coverageData []byte - if init { - if cm.initBytecodeCoverageData == nil { - cm.initBytecodeCoverageData = make([]byte, codeSize) - } - coverageData = cm.initBytecodeCoverageData - } else { - if cm.deployedBytecodeCoverageData == nil { - cm.deployedBytecodeCoverageData = make([]byte, codeSize) - } - coverageData = cm.deployedBytecodeCoverageData +// setCoveredAt sets the coverage state 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) { + // If the execution flags don't exist, create them for this code size. + if cm.executedFlags == nil { + cm.executedFlags = make([]byte, codeSize) } // If our program counter is in range, determine if we achieved new coverage for the first time, and update it. - if pc < uint64(len(coverageData)) { - if coverageData[pc] == 0 { - coverageData[pc] = 1 + if pc < uint64(len(cm.executedFlags)) { + if cm.executedFlags[pc] == 0 { + cm.executedFlags[pc] = 1 return true, nil } return false, nil } - return false, fmt.Errorf("tried to set coverage map out of bounds (pc: %d, code size %d)", pc, len(coverageData)) + return false, fmt.Errorf("tried to set coverage map out of bounds (pc: %d, code size %d)", pc, len(cm.executedFlags)) } diff --git a/fuzzing/coverage/coverage_tracer.go b/fuzzing/coverage/coverage_tracer.go index eed18ca4..4882c640 100644 --- a/fuzzing/coverage/coverage_tracer.go +++ b/fuzzing/coverage/coverage_tracer.go @@ -3,7 +3,6 @@ package coverage import ( "fmt" "github.com/crytic/medusa/chain/types" - compilationTypes "github.com/crytic/medusa/compilation/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "math/big" @@ -43,12 +42,6 @@ type CoverageTracer struct { // callDepth refers to the current EVM depth during tracing. callDepth uint64 - - // cachedCodeHashOriginal describes the code hash used to last store coverage. - cachedCodeHashOriginal common.Hash - // cachedCodeHashResolved describes the code hash used to store the last coverage map. If the contract metadata - // code hash is embedded, then it is used. Otherwise, this refers to cachedCodeHashOriginal. - cachedCodeHashResolved common.Hash } // coverageTracerCallFrameState tracks state across call frames in the tracer. @@ -58,6 +51,9 @@ type coverageTracerCallFrameState struct { // pendingCoverageMap describes the coverage maps recorded for this call frame. pendingCoverageMap *CoverageMaps + + // lookupHash describes the hash used to look up the ContractCoverageMap being updated in this frame. + lookupHash *common.Hash } // NewCoverageTracer returns a new CoverageTracer. @@ -71,14 +67,10 @@ func NewCoverageTracer() *CoverageTracer { // CaptureTxStart is called upon the start of transaction execution, as defined by vm.EVMLogger. func (t *CoverageTracer) CaptureTxStart(gasLimit uint64) { - // Reset our capture state + // Reset our call frame states t.callDepth = 0 t.coverageMaps = NewCoverageMaps() t.callFrameStates = make([]*coverageTracerCallFrameState, 0) - t.cachedCodeHashOriginal = common.Hash{} - - // Reset our call frame states. - t.callFrameStates = make([]*coverageTracerCallFrameState, 0) } // CaptureTxEnd is called upon the end of transaction execution, as defined by vm.EVMLogger. @@ -96,15 +88,20 @@ func (t *CoverageTracer) CaptureStart(env *vm.EVM, from common.Address, to commo // CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger. func (t *CoverageTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { - // If we didn't encounter an error in the end, we commit all our coverage maps to the final coverage map. - // If we encountered an error, we reverted, so we don't consider them. - if err == nil { - _, coverageUpdateErr := t.coverageMaps.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) - if coverageUpdateErr != nil { - panic(fmt.Sprintf("coverage tracer failed to update coverage map during capture end: %v", coverageUpdateErr)) + // If we encountered an error in this call frame, mark all coverage as reverted. + if err != nil { + _, revertCoverageErr := t.callFrameStates[t.callDepth].pendingCoverageMap.RevertAll() + if revertCoverageErr != nil { + panic(revertCoverageErr) } } + // Commit all our coverage maps up one call frame. + _, _, coverageUpdateErr := t.coverageMaps.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) + if coverageUpdateErr != nil { + panic(fmt.Sprintf("coverage tracer failed to update coverage map during capture exit: %v", coverageUpdateErr)) + } + // Pop the state tracking struct for this call frame off the stack. t.callFrameStates = t.callFrameStates[:t.callDepth] } @@ -123,15 +120,20 @@ func (t *CoverageTracer) CaptureEnter(typ vm.OpCode, from common.Address, to com // CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger. func (t *CoverageTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - // If we didn't encounter an error in the end, we commit all our coverage maps up one call frame. - // If we encountered an error, we reverted, so we don't consider them. - if err == nil { - _, coverageUpdateErr := t.callFrameStates[t.callDepth-1].pendingCoverageMap.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) - if coverageUpdateErr != nil { - panic(fmt.Sprintf("coverage tracer failed to update coverage map during capture exit: %v", coverageUpdateErr)) + // If we encountered an error in this call frame, mark all coverage as reverted. + if err != nil { + _, revertCoverageErr := t.callFrameStates[t.callDepth].pendingCoverageMap.RevertAll() + if revertCoverageErr != nil { + panic(revertCoverageErr) } } + // Commit all our coverage maps up one call frame. + _, _, coverageUpdateErr := t.callFrameStates[t.callDepth-1].pendingCoverageMap.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) + if coverageUpdateErr != nil { + panic(fmt.Sprintf("coverage tracer failed to update coverage map during capture exit: %v", coverageUpdateErr)) + } + // Pop the state tracking struct for this call frame off the stack. t.callFrameStates = t.callFrameStates[:t.callDepth] @@ -146,28 +148,16 @@ func (t *CoverageTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, // If there is code we're executing, collect coverage. if len(scope.Contract.Code) > 0 { - // We record coverage maps under a code hash to merge coverage across different deployments of a contract. - // We rely on the embedded contract metadata code hash if it is available, otherwise the immediate hash - // for this code. Because this method is called for every instruction executed, we cache the resolved - // code hash for performance reasons. - if t.cachedCodeHashOriginal != scope.Contract.CodeHash { - t.cachedCodeHashOriginal = scope.Contract.CodeHash - t.cachedCodeHashResolved = t.cachedCodeHashOriginal - if metadata := compilationTypes.ExtractContractMetadata(scope.Contract.Code); metadata != nil { - if metadataHash := metadata.ExtractBytecodeHash(); metadataHash != nil { - t.cachedCodeHashResolved = common.BytesToHash(metadataHash) - } - } + // Obtain our contract coverage map lookup hash. + if callFrameState.lookupHash == nil { + lookupHash := getContractCoverageMapHash(scope.Contract.Code, callFrameState.create) + callFrameState.lookupHash = &lookupHash } - // If the resolved code hash is not zero (indicating a contract deployment from which we could not extract - // a metadata code hash), then we record coverage for this location in our map. - zeroHash := common.BigToHash(big.NewInt(0)) - if t.cachedCodeHashResolved != zeroHash { - _, coverageUpdateErr := callFrameState.pendingCoverageMap.SetCoveredAt(scope.Contract.Address(), t.cachedCodeHashResolved, callFrameState.create, len(scope.Contract.Code), pc) - if coverageUpdateErr != nil { - panic(fmt.Sprintf("coverage tracer failed to update coverage map while tracing state: %v", coverageUpdateErr)) - } + // Record coverage for this location in our map. + _, coverageUpdateErr := callFrameState.pendingCoverageMap.SetAt(scope.Contract.Address(), *callFrameState.lookupHash, len(scope.Contract.Code), pc) + if coverageUpdateErr != nil { + panic(fmt.Sprintf("coverage tracer failed to update coverage map while tracing state: %v", coverageUpdateErr)) } } } diff --git a/fuzzing/coverage/report_generation.go b/fuzzing/coverage/report_generation.go new file mode 100644 index 00000000..184038f7 --- /dev/null +++ b/fuzzing/coverage/report_generation.go @@ -0,0 +1,106 @@ +package coverage + +import ( + _ "embed" + "fmt" + "github.com/crytic/medusa/compilation/types" + "github.com/crytic/medusa/utils" + "html/template" + "math" + "os" + "path/filepath" + "strconv" + "time" +) + +var ( + //go:embed report_template.gohtml + htmlReportTemplate []byte +) + +// GenerateReport takes a set of CoverageMaps and compilations, and produces a coverage report using them, detailing +// all source mapped ranges of the source files which were covered or not. +// Returns an error if one occurred. +func GenerateReport(compilations []types.Compilation, coverageMaps *CoverageMaps, htmlReportPath string) error { + // Perform source analysis. + sourceAnalysis, err := AnalyzeSourceCoverage(compilations, coverageMaps) + if err != nil { + return err + } + + // Finally, export the report data we analyzed. + if htmlReportPath != "" { + err = exportCoverageReport(sourceAnalysis, htmlReportPath) + } + return err +} + +// exportCoverageReport takes a previously performed source analysis and generates an HTML coverage report from it. +// Returns an error if one occurs. +func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) error { + // Define mappings onto some useful variables/functions. + functionMap := template.FuncMap{ + "timeNow": time.Now, + "add": func(x int, y int) int { + return x + y + }, + "relativePath": func(path string) string { + // Obtain a path relative to our current working directory. + // If we encounter an error, return the original path. + cwd, err := os.Getwd() + if err != nil { + return path + } + relativePath, err := filepath.Rel(cwd, path) + if err != nil { + return path + } + + return relativePath + }, + "percentageStr": func(x int, y int, decimals int) string { + // Determine our precision string + formatStr := "%." + strconv.Itoa(decimals) + "f" + + // If no lines are active and none are covered, show 100% coverage + if x == 0 && y == 0 { + return fmt.Sprintf(formatStr, float64(100)) + } + return fmt.Sprintf(formatStr, (float64(x)/float64(y))*100) + }, + "percentageInt": func(x int, y int) int { + if y == 0 { + return 100 + } + return int(math.Round(float64(x) / float64(y) * 100)) + }, + } + + // Parse our HTML template + tmpl, err := template.New("coverage_report.html").Funcs(functionMap).Parse(string(htmlReportTemplate)) + if err != nil { + return fmt.Errorf("could not export report, failed to parse report template: %v", err) + } + + // If the parent directory doesn't exist, create it. + parentDirectory := filepath.Dir(outputPath) + err = utils.MakeDirectory(parentDirectory) + if err != nil { + return err + } + + // Create our report file + file, err := os.Create(outputPath) + if err != nil { + _ = file.Close() + return fmt.Errorf("could not export report, failed to open file for writing: %v", err) + } + + // Execute the template and write it back to file. + err = tmpl.Execute(file, sourceAnalysis) + fileCloseErr := file.Close() + if err == nil { + err = fileCloseErr + } + return err +} diff --git a/fuzzing/coverage/report_template.gohtml b/fuzzing/coverage/report_template.gohtml new file mode 100644 index 00000000..cab7c07e --- /dev/null +++ b/fuzzing/coverage/report_template.gohtml @@ -0,0 +1,243 @@ + + + + + Coverage Report + + + + +
+

Coverage Report

+
+ + + + + + + +
Files: {{len .Files}}
Lines: {{.LineCount}}
Covered: + {{/* Analyze some initial coverage metrics */}} + {{$totalLinesCovered := .CoveredLineCount}} + {{$totalLinesActive := .ActiveLineCount}} + {{$totalPercentCoverageInt := percentageInt $totalLinesCovered $totalLinesActive}} + + {{/* Output our coverage info with a progress bar alongside it.*/}} + {{/*The progress bar's color is set from HSL values (hue 0-100 is red->orange->yellow->green)*/}} + {{$totalLinesCovered}} / {{$totalLinesActive}} ({{percentageStr $totalLinesCovered $totalLinesActive 1}}%) + +
+
+
+ + {{/* Loop through all sources */}} + {{range $sourceFile := .SortedFiles}} + {{/* Analyze some initial coverage metrics */}} + {{$linesCovered := $sourceFile.CoveredLineCount}} + {{$linesActive := $sourceFile.ActiveLineCount}} + {{$linesCoveredPercentInt := percentageInt $linesCovered $linesActive}} + + {{/* Output a collapsible header/container for each source*/}} + +
+
+
+ {{/* Output the total line coverage statistics*/}} + + + + + +
Lines covered: {{$linesCovered}} / {{$linesActive}} ({{percentageStr $linesCovered $linesActive 1}}%)
+
+ {{/* Output a tables with a row for each source line*/}} + + {{range $lineIndex, $line := $sourceFile.Lines}} + {{/* Create a row for this source line */}} + + {{/* Output a cell for the line number */}} + + + {{/* Output two cells for the reverted/non-reverted execution status */}} + + + + {{/* Output a cell for the source line */}} + {{/* If a source line is "active", it has a source mapping so we mark it green/red */}} + {{/* If a source line is "covered", it is green, otherwise it is red. */}} + + + {{end}} +
{{add $lineIndex 1}} + {{if $line.IsCovered}} +
+ {{end}} +
+ {{if $line.IsCoveredReverted}} +
+ {{end}} +
+ {{if not $line.IsActive}} +
{{printf "%s" $line.Contents}}
+ {{else if or $line.IsCovered $line.IsCoveredReverted}} +
{{printf "%s" $line.Contents}}
+ {{else}} +
{{printf "%s" $line.Contents}}
+ {{end}} +
+
+
+ {{end}} + + + + +
+ + + diff --git a/fuzzing/coverage/source_analysis.go b/fuzzing/coverage/source_analysis.go new file mode 100644 index 00000000..baed6505 --- /dev/null +++ b/fuzzing/coverage/source_analysis.go @@ -0,0 +1,313 @@ +package coverage + +import ( + "bytes" + "fmt" + "github.com/crytic/medusa/compilation/types" + "golang.org/x/exp/maps" + "sort" +) + +// SourceAnalysis describes source code coverage across a list of compilations, after analyzing associated CoverageMaps. +type SourceAnalysis struct { + // Files describes the analysis results for a given source file path. + Files map[string]*SourceFileAnalysis +} + +// SortedFiles returns a list of Files within the SourceAnalysis, sorted by source file path in alphabetical order. +func (s *SourceAnalysis) SortedFiles() []*SourceFileAnalysis { + // Copy all source files from our analysis into a list. + sourceFiles := maps.Values(s.Files) + + // Sort source files by path + sort.Slice(sourceFiles, func(x, y int) bool { + return sourceFiles[x].Path < sourceFiles[y].Path + }) + + return sourceFiles +} + +// LineCount returns the count of lines across all source files. +func (s *SourceAnalysis) LineCount() int { + count := 0 + for _, file := range s.Files { + count += len(file.Lines) + } + return count +} + +// ActiveLineCount returns the count of lines that are marked executable/active across all source files. +func (s *SourceAnalysis) ActiveLineCount() int { + count := 0 + for _, file := range s.Files { + count += file.ActiveLineCount() + } + return count +} + +// CoveredLineCount returns the count of lines that were covered across all source files. +func (s *SourceAnalysis) CoveredLineCount() int { + count := 0 + for _, file := range s.Files { + count += file.CoveredLineCount() + } + return count +} + +// SourceFileAnalysis describes coverage information for a given source file. +type SourceFileAnalysis struct { + // Path describes the file path of the source file. This is kept here for access during report generation. + Path string + + // Lines describes information about a given source line and its coverage. + Lines []*SourceLineAnalysis +} + +// ActiveLineCount returns the count of lines that are marked executable/active within the source file. +func (s *SourceFileAnalysis) ActiveLineCount() int { + count := 0 + for _, line := range s.Lines { + if line.IsActive { + count++ + } + } + return count +} + +// CoveredLineCount returns the count of lines that were covered within the source file. +func (s *SourceFileAnalysis) CoveredLineCount() int { + count := 0 + for _, line := range s.Lines { + if line.IsCovered || line.IsCoveredReverted { + count++ + } + } + return count +} + +// SourceLineAnalysis describes coverage information for a specific source file line. +type SourceLineAnalysis struct { + // IsActive indicates the given source line was executable. + IsActive bool + + // Start describes the starting byte offset of the line in its parent source file. + Start int + + // End describes the ending byte offset of the line in its parent source file. + End int + + // Contents describe the bytes associated with the given source line. + Contents []byte + + // IsCovered indicates whether the source line has been executed without reverting. + IsCovered bool + + // IsCoveredReverted indicates whether the source line has been executed before reverting. + IsCoveredReverted bool +} + +// AnalyzeSourceCoverage takes a list of compilations and a set of coverage maps, and performs source analysis +// to determine source coverage information. +// Returns a SourceAnalysis object, or an error if one occurs. +func AnalyzeSourceCoverage(compilations []types.Compilation, coverageMaps *CoverageMaps) (*SourceAnalysis, error) { + // Create a new source analysis object + sourceAnalysis := &SourceAnalysis{ + Files: make(map[string]*SourceFileAnalysis), + } + + // Loop through all sources in all compilations to add them to our source file analysis container. + for _, compilation := range compilations { + for sourcePath := range compilation.Sources { + // If we have no source code loaded for this source, skip it. + if _, ok := compilation.SourceCode[sourcePath]; !ok { + return nil, fmt.Errorf("could not perform source code analysis, code was not cached for '%v'", sourcePath) + } + + // Obtain the parsed source code lines for this source. + if _, ok := sourceAnalysis.Files[sourcePath]; !ok { + sourceAnalysis.Files[sourcePath] = &SourceFileAnalysis{ + Path: sourcePath, + Lines: parseSourceLines(compilation.SourceCode[sourcePath]), + } + } + } + } + + // Loop through all sources in all compilations to process coverage information. + for _, compilation := range compilations { + for _, source := range compilation.Sources { + // Loop for each contract in this source + for _, contract := range source.Contracts { + // Obtain coverage map data for this contract. + initCoverageMapData, err := coverageMaps.GetContractCoverageMap(contract.InitBytecode, true) + if err != nil { + return nil, fmt.Errorf("could not perform source code analysis due to error fetching init coverage map data: %v", err) + } + runtimeCoverageMapData, err := coverageMaps.GetContractCoverageMap(contract.RuntimeBytecode, false) + if err != nil { + return nil, fmt.Errorf("could not perform source code analysis due to error fetching runtime coverage map data: %v", err) + } + + // Parse the source map for this contract. + initSourceMap, err := types.ParseSourceMap(contract.SrcMapsInit) + if err != nil { + return nil, fmt.Errorf("could not perform source code analysis due to error fetching init source map: %v", err) + } + runtimeSourceMap, err := types.ParseSourceMap(contract.SrcMapsRuntime) + if err != nil { + return nil, fmt.Errorf("could not perform source code analysis due to error fetching runtime source map: %v", err) + } + + // Parse our instruction index to offset lookups + initInstructionOffsetLookup, err := initSourceMap.GetInstructionIndexToOffsetLookup(contract.InitBytecode) + if err != nil { + return nil, fmt.Errorf("could not perform source code analysis due to error parsing init byte code: %v", err) + } + runtimeInstructionOffsetLookup, err := runtimeSourceMap.GetInstructionIndexToOffsetLookup(contract.RuntimeBytecode) + if err != nil { + return nil, fmt.Errorf("could not perform source code analysis due to error parsing runtime byte code: %v", err) + } + + // Filter our source maps + initSourceMap = filterSourceMaps(compilation, initSourceMap) + runtimeSourceMap = filterSourceMaps(compilation, runtimeSourceMap) + + // Analyze both init and runtime coverage for our source lines. + err = analyzeContractSourceCoverage(compilation, sourceAnalysis, initSourceMap, initInstructionOffsetLookup, initCoverageMapData) + if err != nil { + return nil, err + } + err = analyzeContractSourceCoverage(compilation, sourceAnalysis, runtimeSourceMap, runtimeInstructionOffsetLookup, runtimeCoverageMapData) + if err != nil { + return nil, err + } + } + } + } + return sourceAnalysis, nil +} + +// analyzeContractSourceCoverage takes a compilation, a SourceAnalysis, the source map they were derived from, +// a lookup of instruction index->offset, and coverage map data. It updates the coverage source line mapping with +// coverage data, after analyzing the coverage data for the given file in the given compilation. +// Returns an error if one occurs. +func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis *SourceAnalysis, sourceMap types.SourceMap, instructionOffsetLookup []int, contractCoverageData *ContractCoverageMap) error { + // Loop through each source map element + for _, sourceMapElement := range sourceMap { + // If this source map element doesn't map to any file (compiler generated inline code), it will have no + // relevance to the coverage map, so we skip it. + if sourceMapElement.FileID == -1 { + continue + } + + // Verify this file ID is not out of bounds for a source file index + if sourceMapElement.FileID < 0 || sourceMapElement.FileID >= len(compilation.SourceList) { + // TODO: We may also go out of bounds because this maps to a "generated source" which we do not have. + // For now, we silently skip these cases. + continue + } + + // Obtain our source for this file ID + sourcePath := compilation.SourceList[sourceMapElement.FileID] + + // Check if the source map element was executed. + sourceMapElementCovered := false + sourceMapElementCoveredReverted := false + if contractCoverageData != nil { + sourceMapElementCovered = contractCoverageData.successfulCoverage.IsCovered(instructionOffsetLookup[sourceMapElement.Index]) + sourceMapElementCoveredReverted = contractCoverageData.revertedCoverage.IsCovered(instructionOffsetLookup[sourceMapElement.Index]) + } + + // Obtain the source file this element maps to. + if sourceFile, ok := sourceAnalysis.Files[sourcePath]; ok { + // Mark all lines which fall within this range. + matchedSourceLine := false + for _, sourceLine := range sourceFile.Lines { + // Check if the line is within range + if sourceMapElement.Offset >= sourceLine.Start && sourceMapElement.Offset < sourceLine.End { + // Mark the line active/executable. + sourceLine.IsActive = true + + // Set its coverage state + sourceLine.IsCovered = sourceLine.IsCovered || sourceMapElementCovered + sourceLine.IsCoveredReverted = sourceLine.IsCoveredReverted || sourceMapElementCoveredReverted + + // Indicate we matched a source line, so when we stop matching sequentially, we know we can exit + // early. + matchedSourceLine = true + } else if matchedSourceLine { + break + } + } + } else { + return fmt.Errorf("could not perform source code analysis, missing source '%v'", sourcePath) + } + + } + return nil +} + +// filterSourceMaps takes a given source map and filters it so overlapping (superset) source map elements are removed. +// In addition to any which do not map to any source code. This is necessary as some source map entries select an +// entire method definition. +// Returns the filtered source map. +func filterSourceMaps(compilation types.Compilation, sourceMap types.SourceMap) types.SourceMap { + // Create our resulting source map + filteredMap := make(types.SourceMap, 0) + + // Loop for each source map entry and determine if it should be included. + for i, sourceMapElement := range sourceMap { + // Verify this file ID is not out of bounds for a source file index + if sourceMapElement.FileID < 0 || sourceMapElement.FileID >= len(compilation.SourceList) { + // TODO: We may also go out of bounds because this maps to a "generated source" which we do not have. + // For now, we silently skip these cases. + continue + } + + // Verify this source map does not overlap another + encapsulatesOtherMapping := false + for x, sourceMapElement2 := range sourceMap { + if i != x && sourceMapElement.FileID == sourceMapElement2.FileID && + !(sourceMapElement.Offset == sourceMapElement2.Offset && sourceMapElement.Length == sourceMapElement2.Length) { + if sourceMapElement2.Offset >= sourceMapElement.Offset && + sourceMapElement2.Offset+sourceMapElement2.Length <= sourceMapElement.Offset+sourceMapElement.Length { + encapsulatesOtherMapping = true + break + } + } + } + + if !encapsulatesOtherMapping { + filteredMap = append(filteredMap, sourceMapElement) + } + } + return filteredMap +} + +// parseSourceLines splits the provided source code into SourceLineAnalysis objects. +// Returns the SourceLineAnalysis objects. +func parseSourceLines(sourceCode []byte) []*SourceLineAnalysis { + // Create our lines and a variable to track where our current line start offset is. + var lines []*SourceLineAnalysis + var lineStart int + + // Split the source code on new line characters + sourceCodeLinesBytes := bytes.Split(sourceCode, []byte("\n")) + + // For each source code line, initialize a struct that defines its start/end offsets, set its contents. + for i := 0; i < len(sourceCodeLinesBytes); i++ { + lineEnd := lineStart + len(sourceCodeLinesBytes[i]) + 1 + lines = append(lines, &SourceLineAnalysis{ + IsActive: false, + Start: lineStart, + End: lineEnd, + Contents: sourceCodeLinesBytes[i], + IsCovered: false, + IsCoveredReverted: false, + }) + lineStart = lineEnd + } + + // Return the resulting lines + return lines +} diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index d65fcd67..6a083161 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -3,8 +3,10 @@ package fuzzing import ( "context" "fmt" + "github.com/crytic/medusa/fuzzing/coverage" "math/big" "math/rand" + "path/filepath" "sort" "strings" "sync" @@ -40,7 +42,11 @@ type Fuzzer struct { senders []common.Address // deployer describes an account address used to deploy contracts in fuzzing campaigns. deployer common.Address - // contractDefinitions defines targets to be fuzzed once their deployment is detected. + + // compilations describes all compilations added as targets. + compilations []compilationTypes.Compilation + // contractDefinitions defines targets to be fuzzed once their deployment is detected. They are derived from + // compilations. contractDefinitions fuzzerTypes.Contracts // baseValueSet represents a valuegeneration.ValueSet containing input values for our fuzz tests. baseValueSet *valuegeneration.ValueSet @@ -222,18 +228,29 @@ func (f *Fuzzer) ReportTestCaseFinished(testCase TestCase) { // definitions and Fuzzer.BaseValueSet values. func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilation) { // Loop for each contract in each compilation and deploy it to the test node. - for _, comp := range compilations { - for sourcePath, source := range comp.Sources { + for i := 0; i < len(compilations); i++ { + // Add our compilation to the list and get a reference to it. + f.compilations = append(f.compilations, compilations[i]) + compilation := &f.compilations[len(f.compilations)-1] + + // Loop for each source + for sourcePath, source := range compilation.Sources { // Seed our base value set from every source's AST f.baseValueSet.SeedFromAst(source.Ast) // Loop for every contract and register it in our contract definitions for contractName := range source.Contracts { contract := source.Contracts[contractName] - contractDefinition := fuzzerTypes.NewContract(contractName, sourcePath, &contract) + contractDefinition := fuzzerTypes.NewContract(contractName, sourcePath, &contract, compilation) f.contractDefinitions = append(f.contractDefinitions, contractDefinition) } } + + // Cache all of our source code if it hasn't been already. + err := compilation.CacheSourceCode() + if err != nil { + fmt.Printf("Warning: could not cache compilation source file data due to error: %v", err) + } } } @@ -596,6 +613,13 @@ func (f *Fuzzer) Start() error { // Print our results on exit. f.printExitingResults() + // Finally, generate our coverage report if we have set a valid corpus directory. + if err == nil && f.config.Fuzzing.CorpusDirectory != "" { + coverageReportPath := filepath.Join(f.config.Fuzzing.CorpusDirectory, "coverage_report.html") + err = coverage.GenerateReport(f.compilations, f.corpus.CoverageMaps(), coverageReportPath) + fmt.Printf("coverage report saved to file: %v\n", coverageReportPath) + } + // Return any encountered error. return err } @@ -637,7 +661,7 @@ func (f *Fuzzer) printMetricsLoop() { uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate), uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate), uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate), - f.corpus.ActiveCallSequenceCount(), + f.corpus.ActiveMutableSequenceCount(), ) // Update our delta tracking metrics diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 0334a32a..a1b7aade 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -576,7 +576,7 @@ func TestCorpusReplayability(t *testing.T) { // Cache current coverage maps originalCoverage := f.fuzzer.corpus.CoverageMaps() - originalCorpusSequenceCount := f.fuzzer.corpus.CallSequenceCount() + originalCorpusSequenceCount := f.fuzzer.corpus.CallSequenceEntryCount(true, true, true) // Next, set the fuzzer worker count to one, this allows us to count the call sequences executed before // solving a problem. We will verify the problem is solved with less or equal sequences tested, than @@ -590,8 +590,8 @@ func TestCorpusReplayability(t *testing.T) { assertCorpusCallSequencesCollected(f, true) newCoverage := f.fuzzer.corpus.CoverageMaps() - // Check to see if original and new coverage are the same - assert.True(t, originalCoverage.Equals(newCoverage)) + // Check to see if original and new coverage are the same. + assert.True(t, originalCoverage.Equal(newCoverage)) // Verify that the fuzzer finished after fewer sequences than there are in the corpus assert.LessOrEqual(t, f.fuzzer.metrics.SequencesTested().Uint64(), uint64(originalCorpusSequenceCount)) @@ -639,7 +639,7 @@ func TestDeploymentOrderWithCoverage(t *testing.T) { // Check to see if original and new coverage are the same newCoverage := f.fuzzer.corpus.CoverageMaps() - assert.False(t, originalCoverage.Equals(newCoverage)) + assert.False(t, originalCoverage.Equal(newCoverage)) }, }) } diff --git a/fuzzing/fuzzer_test_methods_test.go b/fuzzing/fuzzer_test_methods_test.go index b4db1d51..a022551b 100644 --- a/fuzzing/fuzzer_test_methods_test.go +++ b/fuzzing/fuzzer_test_methods_test.go @@ -80,14 +80,17 @@ func assertFailedTestsExpected(f *fuzzerTestContext, expectFailure bool) { // assertCorpusCallSequencesCollected will check to see whether we captured coverage-increasing call sequences in the // corpus. It asserts that the actual result matches the provided expected result. func assertCorpusCallSequencesCollected(f *fuzzerTestContext, expectCallSequences bool) { + // Obtain our count of mutable (often representing just non-reverted coverage increasing) sequences. + callSequenceCount := f.fuzzer.corpus.CallSequenceEntryCount(true, false, false) + // Ensure we captured some coverage-increasing call sequences. if expectCallSequences { - assert.Greater(f.t, f.fuzzer.corpus.CallSequenceCount(), 0, "No coverage was captured") + assert.Greater(f.t, callSequenceCount, 0, "No coverage was captured") } // If we don't expect coverage-increasing call sequences, or it is not enabled, we should not get any coverage if !expectCallSequences || !f.fuzzer.config.Fuzzing.CoverageEnabled { - assert.EqualValues(f.t, 0, f.fuzzer.corpus.CallSequenceCount(), "Coverage was captured") + assert.EqualValues(f.t, 0, callSequenceCount, "Coverage was captured") } } diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index bc9b9c96..89ff7576 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -250,7 +250,7 @@ func (fw *FuzzerWorker) testCallSequence() (calls.CallSequence, []ShrinkCallSequ executionCheckFunc := func(currentlyExecutedSequence calls.CallSequence) (bool, error) { // Check for updates to coverage and corpus. // If we detect coverage changes, add this sequence with weight as 1 + sequences tested (to avoid zero weights) - err := fw.fuzzer.corpus.AddCallSequenceIfCoverageChanged(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) + err := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) if err != nil { return true, err } @@ -343,7 +343,7 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri executionCheckFunc := func(currentlyExecutedSequence calls.CallSequence) (bool, error) { // Check for updates to coverage and corpus (using only the section of the sequence we tested so far). // If we detect coverage changes, add this sequence. - err := fw.fuzzer.corpus.AddCallSequenceIfCoverageChanged(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) + err := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) if err != nil { return true, err } @@ -392,7 +392,7 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri // If the shrink request wanted the sequence recorded in the corpus, do so now. if shrinkRequest.RecordResultInCorpus { - err = fw.fuzzer.corpus.AddCallSequence(optimizedSequence, fw.getNewCorpusCallSequenceWeight(), true) + err = fw.fuzzer.corpus.AddTestResultCallSequence(optimizedSequence, fw.getNewCorpusCallSequenceWeight(), true) if err != nil { return nil, err } diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index d57d05fe..bbb83fc1 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -202,7 +202,7 @@ func (g *CallSequenceGenerator) InitializeNextSequence() (bool, error) { // If this provider has no corpus mutation methods or corpus call sequences, we return a call sequence with // nil elements to signal that we want an entirely new sequence. - if g.mutationStrategyChooser.ChoiceCount() == 0 || g.worker.fuzzer.corpus.ActiveCallSequenceCount() == 0 { + if g.mutationStrategyChooser.ChoiceCount() == 0 || g.worker.fuzzer.corpus.ActiveMutableSequenceCount() == 0 { return true, nil } @@ -329,7 +329,7 @@ func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement // Returns an error if one occurs. func callSeqGenFuncCorpusHead(sequenceGenerator *CallSequenceGenerator, sequence calls.CallSequence) error { // Obtain a call sequence from the corpus - corpusSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomCallSequence() + corpusSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { return fmt.Errorf("could not obtain corpus call sequence for tail mutation: %v", err) } @@ -346,7 +346,7 @@ func callSeqGenFuncCorpusHead(sequenceGenerator *CallSequenceGenerator, sequence // Returns an error if one occurs. func callSeqGenFuncCorpusTail(sequenceGenerator *CallSequenceGenerator, sequence calls.CallSequence) error { // Obtain a call sequence from the corpus - corpusSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomCallSequence() + corpusSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { return fmt.Errorf("could not obtain corpus call sequence for tail mutation: %v", err) } @@ -365,11 +365,11 @@ func callSeqGenFuncCorpusTail(sequenceGenerator *CallSequenceGenerator, sequence // Returns an error if one occurs. func callSeqGenFuncSpliceAtRandom(sequenceGenerator *CallSequenceGenerator, sequence calls.CallSequence) error { // Obtain two corpus call sequence entries - headSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomCallSequence() + headSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { return fmt.Errorf("could not obtain head corpus call sequence for splice-at-random corpus mutation: %v", err) } - tailSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomCallSequence() + tailSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { return fmt.Errorf("could not obtain tail corpus call sequence for splice-at-random corpus mutation: %v", err) } @@ -397,11 +397,11 @@ func callSeqGenFuncSpliceAtRandom(sequenceGenerator *CallSequenceGenerator, sequ // Returns an error if one occurs. func callSeqGenFuncInterleaveAtRandom(sequenceGenerator *CallSequenceGenerator, sequence calls.CallSequence) error { // Obtain two corpus call sequence entries - firstSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomCallSequence() + firstSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { return fmt.Errorf("could not obtain first corpus call sequence for interleave-at-random corpus mutation: %v", err) } - secondSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomCallSequence() + secondSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { return fmt.Errorf("could not obtain second corpus call sequence for interleave-at-random corpus mutation: %v", err) } diff --git a/fuzzing/testdata/contracts/execution_tracing/proxy_call.sol b/fuzzing/testdata/contracts/execution_tracing/proxy_call.sol index 20e90237..32ae472c 100644 --- a/fuzzing/testdata/contracts/execution_tracing/proxy_call.sol +++ b/fuzzing/testdata/contracts/execution_tracing/proxy_call.sol @@ -17,7 +17,7 @@ contract TestContract { i = new InnerDeploymentContract(); } - function testDelegateCall() public returns (address) { + function testDelegateCall() public { // Perform a delegate call to set our variables in this contract. (bool success, bytes memory data) = address(i).delegatecall(abi.encodeWithSignature("setXY(uint256,uint256,string)", 123, 321, "Hello from proxy call args!")); diff --git a/utils/fs_utils.go b/utils/fs_utils.go index 129b76ef..f515b243 100644 --- a/utils/fs_utils.go +++ b/utils/fs_utils.go @@ -63,14 +63,15 @@ func GetFilePathWithoutExtension(filePath string) string { return filePath[:len(filePath)-len(filepath.Ext(filePath))] } -// MakeDirectory creates a directory at the given path +// MakeDirectory creates a directory at the given path, including any parent directories which do not exist. +// Returns an error, if one occurred. func MakeDirectory(dirToMake string) error { dirInfo, err := os.Stat(dirToMake) if err != nil { // Directory does not exist, as expected. if os.IsNotExist(err) { // TODO: Permissions are way too much but even 666 is not working - err = os.Mkdir(dirToMake, 0777) + err = os.MkdirAll(dirToMake, 0777) if err != nil { return err } From 62c06f1eef9b9e1f918a74e40be9047768315961 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 6 Jun 2023 15:30:11 -0400 Subject: [PATCH 003/109] Fix addr cheatcode (#155) - Fix `addr` and `sign` cheatcodes to correctly left-pad private keys that do not fill up 32-byte slices --- chain/cheat_codes.go | 18 ++++++++++---- .../contracts/cheat_codes/utils/addr.sol | 22 +++++++++++++---- utils/crypto_utils.go | 24 +++++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 utils/crypto_utils.go diff --git a/chain/cheat_codes.go b/chain/cheat_codes.go index 8404eaa4..49bd4ae3 100644 --- a/chain/cheat_codes.go +++ b/chain/cheat_codes.go @@ -340,8 +340,12 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, // addr: Compute the address for a given private key contract.addMethod("addr", abi.Arguments{{Type: typeUint256}}, abi.Arguments{{Type: typeAddress}}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { - // Using TOECDSAUnsafe b/c the private key is guaranteed to be of length 256 bits, at most - privateKey := crypto.ToECDSAUnsafe(inputs[0].(*big.Int).Bytes()) + // Get the private key object + privateKey, err := utils.GetPrivateKey(inputs[0].(*big.Int).Bytes()) + if err != nil { + errorMessage := "addr: " + err.Error() + return nil, cheatCodeRevertData([]byte(errorMessage)) + } // Get ECDSA public key publicKey := privateKey.Public().(*ecdsa.PublicKey) @@ -357,11 +361,15 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, contract.addMethod("sign", abi.Arguments{{Type: typeUint256}, {Type: typeBytes32}}, abi.Arguments{{Type: typeUint8}, {Type: typeBytes32}, {Type: typeBytes32}}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { - // Using TOECDSAUnsafe b/c the private key is guaranteed to be of length 256 bits, at most - privateKey := crypto.ToECDSAUnsafe(inputs[0].(*big.Int).Bytes()) - digest := inputs[1].([32]byte) + // Get the private key object + privateKey, err := utils.GetPrivateKey(inputs[0].(*big.Int).Bytes()) + if err != nil { + errorMessage := "sign: " + err.Error() + return nil, cheatCodeRevertData([]byte(errorMessage)) + } // Sign digest + digest := inputs[1].([32]byte) sig, err := crypto.Sign(digest[:], privateKey) if err != nil { return nil, cheatCodeRevertData([]byte("sign: malformed input to signature algorithm")) diff --git a/fuzzing/testdata/contracts/cheat_codes/utils/addr.sol b/fuzzing/testdata/contracts/cheat_codes/utils/addr.sol index 6a4cd930..81aae1e4 100644 --- a/fuzzing/testdata/contracts/cheat_codes/utils/addr.sol +++ b/fuzzing/testdata/contracts/cheat_codes/utils/addr.sol @@ -8,11 +8,23 @@ contract TestContract { // Obtain our cheat code contract reference. CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); - uint256 privateKey = 0x6df21769a2082e03f7e21f6395561279e9a7feb846b2bf740798c794ad196e00; - address expectedAddress = 0xdf8Ef652AdE0FA4790843a726164df8cf8649339; + // Test with random private key + uint256 pkOne = 0x6df21769a2082e03f7e21f6395561279e9a7feb846b2bf740798c794ad196e00; + address addrOne = 0xdf8Ef652AdE0FA4790843a726164df8cf8649339; + address result = cheats.addr(pkOne); + assert(result == addrOne); - // Call cheats.addr - address result = cheats.addr(privateKey); - assert(result == expectedAddress); + // Test with private key that requires padding + uint256 pkTwo = 1; + address addrTwo = 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf; + result = cheats.addr(pkTwo); + assert(result == addrTwo); + + // Test with zero + uint256 pkThree = 0; + cheats.addr(pkThree); + // A private key of zero is not allowed so if we hit this assertion, then cheats.addr() did not revert which + // is incorrect + assert(false); } } diff --git a/utils/crypto_utils.go b/utils/crypto_utils.go new file mode 100644 index 00000000..0144e51d --- /dev/null +++ b/utils/crypto_utils.go @@ -0,0 +1,24 @@ +package utils + +import ( + "crypto/ecdsa" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" +) + +// GetPrivateKey will return a private key object given a byte slice. Only slices between lengths 1 and 32 (inclusive) +// are valid. +func GetPrivateKey(b []byte) (*ecdsa.PrivateKey, error) { + // Make sure that private key is not zero + if len(b) < 1 || len(b) > 32 { + return nil, errors.New("invalid private key") + } + + // Then pad the private key slice to a fixed 32-byte array + paddedPrivateKey := make([]byte, 32) + copy(paddedPrivateKey[32-len(b):], b) + + // Next we will actually retrieve the private key object + privateKey, err := crypto.ToECDSA(paddedPrivateKey[:]) + return privateKey, errors.WithStack(err) +} From 5a7d044a78f59b74aaab718142a663b7e33bd05d Mon Sep 17 00:00:00 2001 From: Tarun Bansal Date: Wed, 21 Jun 2023 22:39:21 +0530 Subject: [PATCH 004/109] Add support for optimization mode (#75) integrate optimization mode into fuzzer --- fuzzing/config/config.go | 12 + fuzzing/config/config_defaults.go | 6 + fuzzing/fuzzer.go | 5 + fuzzing/fuzzer_test.go | 34 ++ fuzzing/test_case_assertion.go | 10 +- fuzzing/test_case_assertion_provider.go | 2 +- fuzzing/test_case_optimization.go | 77 ++++ fuzzing/test_case_optimization_provider.go | 373 ++++++++++++++++++ fuzzing/test_case_property.go | 13 +- fuzzing/test_case_property_provider.go | 4 +- .../contracts/optimizations/optimize.sol | 14 + 11 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 fuzzing/test_case_optimization.go create mode 100644 fuzzing/test_case_optimization_provider.go create mode 100644 fuzzing/testdata/contracts/optimizations/optimize.sol diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 72e04acf..f6df3aad 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -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 @@ -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) { diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index cdcf86af..45957f69 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -65,6 +65,12 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { "fuzz_", }, }, + OptimizationTesting: OptimizationTestingConfig{ + Enabled: false, + TestPrefixes: []string{ + "optimize_", + }, + }, }, TestChainConfig: *chainConfig, }, diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 6a083161..9e7dabc2 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -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 } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index a1b7aade..66d5d763 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -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" @@ -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. diff --git a/fuzzing/test_case_assertion.go b/fuzzing/test_case_assertion.go index 8ea42eb0..354c2161 100644 --- a/fuzzing/test_case_assertion.go +++ b/fuzzing/test_case_assertion.go @@ -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. diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 35f0d7e6..e332cd65 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -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 { diff --git a/fuzzing/test_case_optimization.go b/fuzzing/test_case_optimization.go new file mode 100644 index 00000000..3110c0ad --- /dev/null +++ b/fuzzing/test_case_optimization.go @@ -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 +} diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go new file mode 100644 index 00000000..622aa155 --- /dev/null +++ b/fuzzing/test_case_optimization_provider.go @@ -0,0 +1,373 @@ +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" + "github.com/ethereum/go-ethereum/core" + "math/big" + "strings" + "sync" +) + +const MIN_INT = "-8000000000000000000000000000000000000000000000000000000000000000" + +// OptimizationTestCaseProvider is a provider for on-chain optimization tests. +// Optimization tests are represented as publicly-accessible functions which have a name prefix specified by a +// config.FuzzingConfig. They take no input arguments and return an integer value that needs to be maximized. +type OptimizationTestCaseProvider struct { + // fuzzer describes the Fuzzer which this provider is attached to. + fuzzer *Fuzzer + + // testCases is a map of contract-method IDs to optimization test cases.GetContractMethodID + testCases map[contracts.ContractMethodID]*OptimizationTestCase + + // testCasesLock is used for thread-synchronization when updating testCases + testCasesLock sync.Mutex + + // workerStates is a slice where each element stores state for a given worker index. + workerStates []optimizationTestCaseProviderWorkerState +} + +// optimizationTestCaseProviderWorkerState represents the state for an individual worker maintained by +// OptimizationTestCaseProvider. +type optimizationTestCaseProviderWorkerState struct { + // optimizationTestMethods a mapping from contract-method ID to deployed contract-method descriptors. + // Each deployed contract-method represents an optimization test method to call for evaluation. Optimization tests + // should be read-only functions which take no input parameters and return an integer variable. + optimizationTestMethods map[contracts.ContractMethodID]contracts.DeployedContractMethod + + // optimizationTestMethodsLock is used for thread-synchronization when updating optimizationTestMethods + optimizationTestMethodsLock sync.Mutex +} + +// attachOptimizationTestCaseProvider attaches a new OptimizationTestCaseProvider to the Fuzzer and returns it. +func attachOptimizationTestCaseProvider(fuzzer *Fuzzer) *OptimizationTestCaseProvider { + // Create a test case provider + t := &OptimizationTestCaseProvider{ + fuzzer: fuzzer, + } + + // Subscribe the provider to relevant events the fuzzer emits. + fuzzer.Events.FuzzerStarting.Subscribe(t.onFuzzerStarting) + fuzzer.Events.FuzzerStopping.Subscribe(t.onFuzzerStopping) + fuzzer.Events.WorkerCreated.Subscribe(t.onWorkerCreated) + + // Add the provider's call sequence test function to the fuzzer. + fuzzer.Hooks.CallSequenceTestFuncs = append(fuzzer.Hooks.CallSequenceTestFuncs, t.callSequencePostCallTest) + return t +} + +// isOptimizationTest check whether the method is an optimization test given potential naming prefixes it must conform to +// and its underlying input/output arguments. +func (t *OptimizationTestCaseProvider) isOptimizationTest(method abi.Method) bool { + // Loop through all enabled prefixes to find a match + for _, prefix := range t.fuzzer.Config().Fuzzing.Testing.OptimizationTesting.TestPrefixes { + if strings.HasPrefix(method.Name, prefix) { + // An optimization test must take no inputs and return an int256 + if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.IntTy && method.Outputs[0].Type.Size == 256 { + return true + } + } + } + return false +} + +// runOptimizationTest executes a given optimization test method (w/ an optional execution trace) and returns the return value +// from the optimization test method. This is called after every call the Fuzzer makes when testing call sequences for each test case. +func (t *OptimizationTestCaseProvider) runOptimizationTest(worker *FuzzerWorker, optimizationTestMethod *contracts.DeployedContractMethod, trace bool) (*big.Int, *executiontracer.ExecutionTrace, error) { + // Generate our ABI input data for the call. In this case, optimization test methods take no arguments, so the + // variadic argument list here is empty. + data, err := optimizationTestMethod.Contract.CompiledContract().Abi.Pack(optimizationTestMethod.Method.Name) + if err != nil { + return nil, nil, err + } + + // Call the underlying contract + value := big.NewInt(0) + // TODO: Determine if we should use `Senders[0]` or have a separate funded account for the optimizations. + msg := calls.NewCallMessage(worker.Fuzzer().senders[0], &optimizationTestMethod.Address, 0, value, worker.fuzzer.config.Fuzzing.TransactionGasLimit, nil, nil, nil, data) + msg.FillFromTestChainProperties(worker.chain) + + // Execute the call. If we are tracing, we attach an execution tracer and obtain the result. + var executionResult *core.ExecutionResult + var executionTrace *executiontracer.ExecutionTrace + if trace { + executionTracer := executiontracer.NewExecutionTracer(worker.fuzzer.contractDefinitions, worker.chain.CheatCodeContracts()) + executionResult, err = worker.Chain().CallContract(msg, nil, executionTracer) + executionTrace = executionTracer.Trace() + } else { + executionResult, err = worker.Chain().CallContract(msg, nil) + } + if err != nil { + return nil, nil, fmt.Errorf("failed to call optimization test method: %v", err) + } + + // If the execution reverted, then we know that we do not have any valuable return data, so we return the smallest + // integer value + if executionResult.Failed() { + minInt256, _ := new(big.Int).SetString(MIN_INT, 16) + return minInt256, nil, nil + } + + // Decode our ABI outputs + retVals, err := optimizationTestMethod.Method.Outputs.Unpack(executionResult.Return()) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode optimization test method return value: %v", err) + } + + // We should have one return value. + if len(retVals) != 1 { + return nil, nil, fmt.Errorf("detected an unexpected number of return values from optimization test '%s'", optimizationTestMethod.Method.Name) + } + + // Parse the return value and it should be an int256 + newValue, ok := retVals[0].(*big.Int) + if !ok { + return nil, nil, fmt.Errorf("failed to parse optimization test's: %s return value: %v", optimizationTestMethod.Method.Name, retVals[0]) + } + + return newValue, executionTrace, nil +} + +// onFuzzerStarting is the event handler triggered when the Fuzzer is starting a fuzzing campaign. It creates test cases +// in a "not started" state for every optimization test method discovered in the contract definitions known to the Fuzzer. +func (t *OptimizationTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) error { + // Reset our state + t.testCases = make(map[contracts.ContractMethodID]*OptimizationTestCase) + t.workerStates = make([]optimizationTestCaseProviderWorkerState, t.fuzzer.Config().Fuzzing.Workers) + + // Create a test case for every optimization test method. + for _, contract := range t.fuzzer.ContractDefinitions() { + for _, method := range contract.CompiledContract().Abi.Methods { + // Verify this method is an optimization test method + if !t.isOptimizationTest(method) { + continue + } + // Create local variables to avoid pointer types in the loop being overridden. + contract := contract + method := method + minInt256, _ := new(big.Int).SetString(MIN_INT, 16) + + // Create our optimization test case + optimizationTestCase := &OptimizationTestCase{ + status: TestCaseStatusNotStarted, + targetContract: contract, + targetMethod: method, + callSequence: nil, + value: minInt256, + } + + // Add to our test cases and register them with the fuzzer + methodId := contracts.GetContractMethodID(contract, &method) + t.testCases[methodId] = optimizationTestCase + t.fuzzer.RegisterTestCase(optimizationTestCase) + } + } + return nil +} + +// 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 *OptimizationTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error { + // Clear our optimization test methods + t.workerStates = nil + + // Loop through each test case and set any tests with a running status to a passed status. + for _, testCase := range t.testCases { + if testCase.status == TestCaseStatusRunning { + testCase.status = TestCaseStatusPassed + // Since optimization tests do not really "finish", we will report that they are finished when the fuzzer + // stops. + if event.Fuzzer != nil { + event.Fuzzer.ReportTestCaseFinished(testCase) + } + } + } + return nil +} + +// onWorkerCreated is the event handler triggered when a FuzzerWorker is created by the Fuzzer. It ensures state tracked +// for that worker index is refreshed and subscribes to relevant worker events. +func (t *OptimizationTestCaseProvider) onWorkerCreated(event FuzzerWorkerCreatedEvent) error { + // Create a new state for this worker. + t.workerStates[event.Worker.WorkerIndex()] = optimizationTestCaseProviderWorkerState{ + optimizationTestMethods: make(map[contracts.ContractMethodID]contracts.DeployedContractMethod), + optimizationTestMethodsLock: sync.Mutex{}, + } + + // Subscribe to relevant worker events. + event.Worker.Events.ContractAdded.Subscribe(t.onWorkerDeployedContractAdded) + event.Worker.Events.ContractDeleted.Subscribe(t.onWorkerDeployedContractDeleted) + return nil +} + +// onWorkerDeployedContractAdded is the event handler triggered when a FuzzerWorker detects a new contract deployment +// on its underlying chain. It ensures any optimization test methods which the deployed contract contains are tracked by the +// provider for testing. Any test cases previously made for these methods which are in a "not started" state are put +// into a "running" state, as they are now potentially reachable for testing. +func (t *OptimizationTestCaseProvider) onWorkerDeployedContractAdded(event FuzzerWorkerContractAddedEvent) error { + // If we don't have a contract definition, we can't run optimization tests against the contract. + if event.ContractDefinition == nil { + return nil + } + + // Loop through all methods and find ones for which we have tests + for _, method := range event.ContractDefinition.CompiledContract().Abi.Methods { + // Obtain an identifier for this pair + methodId := contracts.GetContractMethodID(event.ContractDefinition, &method) + + // If we have a test case targeting this contract/method that has not failed, track this deployed method in + // our map for this worker. If we have any tests in a not-started state, we can signal a running state now. + t.testCasesLock.Lock() + optimizationTestCase, optimizationTestCaseExists := t.testCases[methodId] + t.testCasesLock.Unlock() + + if optimizationTestCaseExists { + if optimizationTestCase.Status() == TestCaseStatusNotStarted { + optimizationTestCase.status = TestCaseStatusRunning + } + if optimizationTestCase.Status() != TestCaseStatusFailed { + // Create our optimization test method reference. + workerState := &t.workerStates[event.Worker.WorkerIndex()] + workerState.optimizationTestMethodsLock.Lock() + workerState.optimizationTestMethods[methodId] = contracts.DeployedContractMethod{ + Address: event.ContractAddress, + Contract: event.ContractDefinition, + Method: method, + } + workerState.optimizationTestMethodsLock.Unlock() + } + } + } + return nil +} + +// onWorkerDeployedContractDeleted is the event handler triggered when a FuzzerWorker detects that a previously deployed +// contract no longer exists on its underlying chain. It ensures any optimization test methods which the deployed contract +// contained are no longer tracked by the provider for testing. +func (t *OptimizationTestCaseProvider) onWorkerDeployedContractDeleted(event FuzzerWorkerContractDeletedEvent) error { + // If we don't have a contract definition, there's nothing to do. + if event.ContractDefinition == nil { + return nil + } + + // Loop through all methods and find ones for which we have tests + for _, method := range event.ContractDefinition.CompiledContract().Abi.Methods { + // Obtain an identifier for this pair + methodId := contracts.GetContractMethodID(event.ContractDefinition, &method) + + // If this identifier is in our test cases map, then we remove it from our optimization test method lookup for + // this worker index. + t.testCasesLock.Lock() + _, isOptimizationTestMethod := t.testCases[methodId] + t.testCasesLock.Unlock() + + if isOptimizationTestMethod { + // Delete our optimization test method reference. + workerState := &t.workerStates[event.Worker.WorkerIndex()] + workerState.optimizationTestMethodsLock.Lock() + delete(workerState.optimizationTestMethods, methodId) + workerState.optimizationTestMethodsLock.Unlock() + } + } + return nil +} + +// callSequencePostCallTest provides is a CallSequenceTestFunc that performs post-call testing logic for the attached Fuzzer +// and any underlying FuzzerWorker. It is called after every call made in a call sequence. It checks whether any +// optimization test's value has increased. +func (t *OptimizationTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorker, callSequence calls.CallSequence) ([]ShrinkCallSequenceRequest, error) { + // Create a list of shrink call sequence verifiers, which we populate for each maximized optimization test we want a call + // sequence shrunk for. + shrinkRequests := make([]ShrinkCallSequenceRequest, 0) + + // Obtain the test provider state for this worker + workerState := &t.workerStates[worker.WorkerIndex()] + + // Loop through all optimization test methods and test them. + for optimizationTestMethodId, workerOptimizationTestMethod := range workerState.optimizationTestMethods { + // Obtain the test case for this optimization test method + t.testCasesLock.Lock() + testCase := t.testCases[optimizationTestMethodId] + t.testCasesLock.Unlock() + + // Run our optimization test (create a local copy to avoid loop overwriting the method) + workerOptimizationTestMethod := workerOptimizationTestMethod + newValue, _, err := t.runOptimizationTest(worker, &workerOptimizationTestMethod, false) + if err != nil { + return nil, err + } + + // If we updated the test case's maximum value, we update our state immediately. We provide a shrink verifier which will update + // the call sequence for each shrunken sequence provided that still it maintains the maximum value. + // TODO: This is very inefficient since this runs every time a new max value is found. It would be ideal if we + // could perform a one-time shrink request. This code should be refactored when we introduce the high-level + // testing API. + if newValue.Cmp(testCase.value) == 1 { + // Create a request to shrink this call sequence. + shrinkRequest := ShrinkCallSequenceRequest{ + VerifierFunction: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) (bool, error) { + // First verify the contract to the optimization test is still deployed to call upon. + _, optimizationTestContractDeployed := worker.deployedContracts[workerOptimizationTestMethod.Address] + if !optimizationTestContractDeployed { + // If the contract isn't available, this shrunk sequence likely messed up deployment, so we + // report it as an invalid solution. + return false, nil + } + + // Then the shrink verifier ensures that the maximum value has either stayed the same or, hopefully, + // increased. + shrunkenSequenceNewValue, _, err := t.runOptimizationTest(worker, &workerOptimizationTestMethod, false) + + // If the shrunken value is greater than new value, then set new value to the shrunken one so that it + // can be tracked correctly in the finished callback + if err == nil && shrunkenSequenceNewValue.Cmp(newValue) == 1 { + newValue = new(big.Int).Set(shrunkenSequenceNewValue) + } + + return shrunkenSequenceNewValue.Cmp(newValue) >= 0, err + }, + FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) error { + // When we're finished shrinking, attach an execution trace to the last call + if len(shrunkenCallSequence) > 0 { + err = shrunkenCallSequence[len(shrunkenCallSequence)-1].AttachExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions) + if err != nil { + return err + } + } + + // Execute the property test a final time, this time obtaining an execution trace + shrunkenSequenceNewValue, executionTrace, err := t.runOptimizationTest(worker, &workerOptimizationTestMethod, true) + if err != nil { + return err + } + + // If, for some reason, the shrunken sequence lowers the new max value, do not save anything and exit + if shrunkenSequenceNewValue.Cmp(newValue) < 0 { + return fmt.Errorf("optimized call sequence failed to maximize value") + } + + // Update our value with lock + testCase.valueLock.Lock() + testCase.value = new(big.Int).Set(shrunkenSequenceNewValue) + testCase.valueLock.Unlock() + + // Update call sequence and trace + testCase.callSequence = &shrunkenCallSequence + testCase.optimizationTestTrace = executionTrace + return nil + }, + RecordResultInCorpus: true, + } + + // Add our shrink request to our list. + shrinkRequests = append(shrinkRequests, shrinkRequest) + } + } + + return shrinkRequests, nil +} diff --git a/fuzzing/test_case_property.go b/fuzzing/test_case_property.go index 14b5e7de..9967b1ae 100644 --- a/fuzzing/test_case_property.go +++ b/fuzzing/test_case_property.go @@ -11,10 +11,15 @@ import ( // PropertyTestCase describes a test being run by a PropertyTestCaseProvider. type PropertyTestCase struct { - status TestCaseStatus - targetContract *fuzzerTypes.Contract - targetMethod abi.Method - callSequence *calls.CallSequence + // 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 describes the target method for the test case + targetMethod abi.Method + // callSequence describes the call sequence that broke the property + callSequence *calls.CallSequence + // propertyTestTrace describes the execution trace when running the callSequence propertyTestTrace *executiontracer.ExecutionTrace } diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index c509807b..ca8be15c 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -175,7 +175,7 @@ func (t *PropertyTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) e 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 *PropertyTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEvent) error { @@ -247,7 +247,7 @@ func (t *PropertyTestCaseProvider) onWorkerDeployedContractAdded(event FuzzerWor return nil } -// onWorkerDeployedContractAdded is the event handler triggered when a FuzzerWorker detects that a previously deployed +// onWorkerDeployedContractDeleted is the event handler triggered when a FuzzerWorker detects that a previously deployed // contract no longer exists on its underlying chain. It ensures any property test methods which the deployed contract // contained are no longer tracked by the provider for testing. func (t *PropertyTestCaseProvider) onWorkerDeployedContractDeleted(event FuzzerWorkerContractDeletedEvent) error { diff --git a/fuzzing/testdata/contracts/optimizations/optimize.sol b/fuzzing/testdata/contracts/optimizations/optimize.sol new file mode 100644 index 00000000..3be1e27a --- /dev/null +++ b/fuzzing/testdata/contracts/optimizations/optimize.sol @@ -0,0 +1,14 @@ +contract TestContract { + int256 input; + + function set(int256 _input) public { + input = _input; + } + + function optimize_opt_linear() public view returns (int256) { + if (input > -4242) + return -input; + else + return 0; + } +} From c38c6b07bb4816217c5f785d81b5304942946290 Mon Sep 17 00:00:00 2001 From: Maciej Domanski <38883201+ahpaleus@users.noreply.github.com> Date: Wed, 21 Jun 2023 21:30:22 +0200 Subject: [PATCH 005/109] Do not start the fuzzing campaign if no properties are detected (#88) Add config option to prevent fuzzing from starting if no tests are found --- fuzzing/config/config.go | 4 ++++ fuzzing/config/config_defaults.go | 1 + fuzzing/fuzzer.go | 5 +++++ fuzzing/fuzzer_test.go | 1 + 4 files changed, 11 insertions(+) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index f6df3aad..5bad9b52 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -90,6 +90,10 @@ type TestingConfig struct { // to determine which contract a deployed contract is. StopOnFailedContractMatching bool `json:"stopOnFailedContractMatching"` + // StopOnNoTests describes whether the fuzzing.Fuzzer should stop the fuzzer from starting if no tests (property, + // assertion, optimization, custom) are found. + StopOnNoTests bool `json:"stopOnNoTests"` + // TestAllContracts indicates whether all contracts should be tested (including dynamically deployed ones), rather // than just the contracts specified in the project configuration's deployment order. TestAllContracts bool `json:"testAllContracts"` diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 45957f69..58e6b74d 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -53,6 +53,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { Testing: TestingConfig{ StopOnFailedTest: true, StopOnFailedContractMatching: true, + StopOnNoTests: true, TestAllContracts: false, TraceAll: false, AssertionTesting: AssertionTestingConfig{ diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 9e7dabc2..15edf513 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -595,6 +595,11 @@ func (f *Fuzzer) Start() error { return err } + // If StopOnNoTests is true and there are no test cases, then throw an error + if f.config.Fuzzing.Testing.StopOnNoTests && len(f.testCases) == 0 { + return fmt.Errorf("no tests of any kind (assertion/property/optimization/custom) have been identified for fuzzing") + } + // Run the main worker loop err = f.spawnWorkersLoop(baseTestChain) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 66d5d763..17e858b9 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -325,6 +325,7 @@ func TestDeploymentsSelfDestruct(t *testing.T) { configUpdates: func(config *config.ProjectConfig) { config.Fuzzing.DeploymentOrder = []string{"InnerDeploymentFactory"} config.Fuzzing.TestLimit = 500 // this test should expose a failure quickly. + config.Fuzzing.Testing.StopOnNoTests = false }, method: func(f *fuzzerTestContext) { // Subscribe to any mined block events globally. When receiving them, check contract changes for a From f7983dc1cd205fcd3b27f6fd86f78c9bad99e679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:43:06 -0300 Subject: [PATCH 006/109] ci: avoid using C: on Windows CI (#158) use workspace instead of C drive to speed up windows CI --- .github/workflows/ci.yml | 56 +++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 437885c6..f9cdf49f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,10 @@ concurrency: group: ci-${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + jobs: build: needs: [lint, test] @@ -21,9 +25,25 @@ jobs: environment: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.environment }} + timeout-minutes: 10 + steps: - uses: actions/checkout@v3 + - name: Speed up Go (Windows) + if: runner.os == 'Windows' + run: | + DIR='D:\a\local' + mkdir -p "$DIR" && cd "$DIR" + mkdir go go-cache go-tmp tmpdir + go env -w GOPATH="$DIR\\go" + go env -w GOCACHE="$DIR\\go-cache" + go env -w GOTMPDIR="$DIR\\go-tmp" + printf '%s\\go\\bin\n' "$DIR" | tee -a "$GITHUB_PATH" + printf 'TMP=%s\\tmpdir\n' "$DIR" | tee -a "$GITHUB_ENV" + printf 'TEMP=%s\\tmpdir\n' "$DIR" | tee -a "$GITHUB_ENV" + go env + - uses: actions/setup-go@v4 with: go-version: "^1.18.1" @@ -53,6 +73,7 @@ jobs: lint: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v3 @@ -92,11 +113,36 @@ jobs: environment: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.environment }} + timeout-minutes: 20 steps: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - name: Speed up Go, Python, Node (Windows) + if: runner.os == 'Windows' + run: | + DIR='D:\a\local' + mkdir -p "$DIR" && cd "$DIR" + echo "::group::Go" + mkdir -p go go-cache go-tmp tmpdir + go env -w GOPATH="$DIR\\go" + go env -w GOCACHE="$DIR\\go-cache" + go env -w GOTMPDIR="$DIR\\go-tmp" + printf '%s\\go\\bin\n' "$DIR" | tee -a "$GITHUB_PATH" + printf 'TMP=%s\\tmpdir\n' "$DIR" | tee -a "$GITHUB_ENV" + printf 'TEMP=%s\\tmpdir\n' "$DIR" | tee -a "$GITHUB_ENV" + go env + echo "::endgroup::" + echo "::group::Python" + python3 -m venv venv + printf '%s\\venv\\Scripts\n' "$DIR" | tee -a "$GITHUB_PATH" + printf 'VIRTUAL_ENV=%s\\venv\n' "$DIR" | tee -a "$GITHUB_ENV" + echo "::endgroup::" + echo "::group::Node" + npm config set cache "$DIR\\npm-cache" --global + echo "::endgroup::" + + - uses: actions/setup-go@v4 with: go-version: "^1.18.1" @@ -105,17 +151,15 @@ jobs: node-version: 18.15 - name: Install Node dependencies - run: npm install -g hardhat + run: npm install hardhat - name: Install Python dependencies run: | - pip3 install solc-select - pip3 install slither-analyzer + pip3 install --no-cache-dir solc-select crytic-compile - name: Install solc run: | - solc-select install 0.8.17 - solc-select use 0.8.17 + solc-select use 0.8.17 --always-install - name: Test run: go test ./... From d23dc6c92eb9a320419c314cded585f6ce86bede Mon Sep 17 00:00:00 2001 From: Maciej Domanski <38883201+ahpaleus@users.noreply.github.com> Date: Fri, 23 Jun 2023 00:25:14 +0200 Subject: [PATCH 007/109] Autocompletion (#79) introduce autocompletion command to medusa CLI --- .gitignore | 7 ++-- cmd/completion.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++ cmd/fuzz.go | 21 +++++++++++ cmd/init.go | 39 ++++++++++++++++++-- 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 cmd/completion.go diff --git a/.gitignore b/.gitignore index bc77363a..051bf3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,10 @@ # Dependency directories (remove the comment below to include it) # vendor/ -# PyCharm project dir +# Goland project dir .idea/ -*node_modules/ \ No newline at end of file +*node_modules/ + +# Medusa binary +medusa \ No newline at end of file diff --git a/cmd/completion.go b/cmd/completion.go new file mode 100644 index 00000000..57e45539 --- /dev/null +++ b/cmd/completion.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "fmt" + "golang.org/x/exp/slices" + "os" + "strings" + + "github.com/spf13/cobra" +) + +// generalComDesc describes the long description for the completion command +const generalComDesc = ` +Generate the autocompletion script for medusa for the specific shell. + +Bash: +To load completions in the current shell session: + + source <(medusa completion bash) + +To load completions for every new session, execute once: +- Linux: + medusa completion bash > /etc/bash_completion.d/medusa + +- macOS: + medusa completion bash > /usr/local/etc/bash_completion.d/medusa + +Zsh: +To load completions in the current shell session: + + source <(medusa completion zsh) + +To load completions for every new session, execute once: + + medusa completion zsh > "${fpath[1]}/_medusa" + +PowerShell: +To load completions in the current shell session: +PS> medusa completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, run: +PS> medusa completion powershell > medusa.ps1 +and source this file from your PowerShell profile. +` + +var supportedShells = []string{"bash", "zsh", "powershell"} + +// completionCmd represents the completion command +var completionCmd = &cobra.Command{ + Use: "completion ", + Short: "generate the autocompletion script for medusa for the specific shell", + Long: generalComDesc, + Args: cmdValidateCompletionArgs, + RunE: cmdRunCompletion, +} + +func init() { + rootCmd.AddCommand(completionCmd) +} + +// cmdValidateCompletionArgs validates CLI arguments +func cmdValidateCompletionArgs(cmd *cobra.Command, args []string) error { + // Make sure we have exactly 1 argument + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return fmt.Errorf("completion requires only 1 shell argument (options: %s)", strings.Join(supportedShells, ", ")) + } + + // Make sure that the shell is a supported type + if contains := slices.Contains(supportedShells, args[0]); !contains { + return fmt.Errorf("%s is not a supported shell", args[0]) + } + + return nil +} + +// cmdRunCompletion executes the completion CLI command +func cmdRunCompletion(cmd *cobra.Command, args []string) error { + // NOTE: Please be aware that if the supported shells changes, then this switch statement must also change + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletionV2(os.Stdout, true) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + // We are throwing a panic here because our validation function should have handled this and something is wrong. + panic(fmt.Errorf("%s is not a supported shell type", args[0])) + } +} diff --git a/cmd/fuzz.go b/cmd/fuzz.go index 0c6508d3..c77ac5e0 100644 --- a/cmd/fuzz.go +++ b/cmd/fuzz.go @@ -9,6 +9,7 @@ import ( "github.com/crytic/medusa/fuzzing" "github.com/crytic/medusa/fuzzing/config" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // fuzzCmd represents the command provider for fuzzing @@ -18,6 +19,26 @@ var fuzzCmd = &cobra.Command{ Long: `Starts a fuzzing campaign`, Args: cmdValidateFuzzArgs, RunE: cmdRunFuzz, + // Run dynamic completion of nouns + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Gather a list of flags that are available to be used in the current command but have not been used yet + var unusedFlags []string + + // Examine all the flags, and add any flags that have not been set in the current command line + // to a list of unused flags + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if !flag.Changed { + // When adding a flag to a command, include the "--" prefix to indicate that it is a flag + // and not a positional argument. Additionally, when the user presses the TAB key twice after typing + // a flag name, the "--" prefix will appear again, indicating that more flags are available and that + // none of the arguments are positional. + unusedFlags = append(unusedFlags, "--"+flag.Name) + } + }) + // Provide a list of flags that can be used in the current command (but have not been used yet) + // for autocompletion suggestions + return unusedFlags, cobra.ShellCompDirectiveNoFileComp + }, } // cmdValidateFuzzArgs makes sure that there are no positional arguments provided to the fuzz command diff --git a/cmd/init.go b/cmd/init.go index a9685fe8..79087667 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -9,8 +9,13 @@ import ( "github.com/crytic/medusa/compilation" "github.com/crytic/medusa/fuzzing/config" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) +// Get supported platforms for customized static completions of "init" flag `$ medusa init ` +// and to cache supported platforms for CLI arguments validation +var supportedPlatforms = compilation.GetSupportedCompilationPlatforms() + // initCmd represents the command provider for init var initCmd = &cobra.Command{ Use: "init [platform]", @@ -18,6 +23,37 @@ var initCmd = &cobra.Command{ Long: `Initializes a project configuration`, Args: cmdValidateInitArgs, RunE: cmdRunInit, + // Run dynamic completion of nouns + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Gather a list of flags that are available to be used in the current command but have not been used yet + var unusedFlags []string + + // Examine all the flags, and add any flags that have not been set in the current command line + // to a list of unused flags + flagUsed := false + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if !flag.Changed { + // When adding a flag to a command, include the "--" prefix to indicate that it is a flag + // and not a positional argument. Additionally, when the user presses the TAB key twice after typing + // a flag name, the "--" prefix will appear again, indicating that more flags are available and that + // none of the arguments are positional. + unusedFlags = append(unusedFlags, "--"+flag.Name) + } else { + // If any flag has been used, set flag used to true. This will be used later in the function. + flagUsed = true + } + }) + + // If a default platform is not specified, add a list of available platforms to the list of unused flags. + // If any flag is used, then we can assume that the default platform is used so we don't need to add supported platforms + if len(args) == 0 && !flagUsed { + unusedFlags = append(unusedFlags, supportedPlatforms...) + } + + // Provide a list of flags that can be used in the current command (but have not been used yet) + // for autocompletion suggestions + return unusedFlags, cobra.ShellCompDirectiveNoFileComp + }, } func init() { @@ -33,9 +69,6 @@ func init() { // cmdValidateInitArgs validates CLI arguments func cmdValidateInitArgs(cmd *cobra.Command, args []string) error { - // Cache supported platforms - supportedPlatforms := compilation.GetSupportedCompilationPlatforms() - // Make sure we have no more than 1 arg if err := cobra.RangeArgs(0, 1)(cmd, args); err != nil { return fmt.Errorf("init accepts at most 1 platform argument (options: %s). "+ From df823033ce60dd738ab85dda7a2d46df4bdf9090 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Fri, 23 Jun 2023 09:54:17 -0400 Subject: [PATCH 008/109] increase test limit (#167) --- fuzzing/fuzzer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 17e858b9..85631501 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -141,7 +141,7 @@ func TestOptimizationsSolving(t *testing.T) { 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. + config.Fuzzing.TestLimit = 10_000 // this test should expose a failure quickly. }, method: func(f *fuzzerTestContext) { // Start the fuzzer From b1a8917ffa5fc8cfe0108a3fd44a62f0ed41c743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 13:50:13 -0400 Subject: [PATCH 009/109] Bump golang.org/x/crypto from 0.8.0 to 0.10.0 (#160) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.8.0 to 0.10.0. - [Commits](https://github.com/golang/crypto/compare/v0.8.0...v0.10.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 10 +++++----- go.sum | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index efaec0f5..d047b12b 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,12 @@ require ( github.com/ethereum/go-ethereum v1.11.1 github.com/fxamacker/cbor v1.5.1 github.com/google/uuid v1.3.0 + github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.2 - golang.org/x/crypto v0.8.0 + golang.org/x/crypto v0.10.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/net v0.9.0 + golang.org/x/net v0.10.0 ) require ( @@ -44,7 +45,6 @@ require ( github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect @@ -60,8 +60,8 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/sys v0.7.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6a0beced..10327ec3 100644 --- a/go.sum +++ b/go.sum @@ -346,8 +346,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -379,8 +379,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -423,8 +423,8 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -432,8 +432,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From c5f0ffa33f1b90e22dd5f231dfe39272aa4aec23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:09:12 -0400 Subject: [PATCH 010/109] Bump golang.org/x/net from 0.9.0 to 0.11.0 (#159) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.9.0 to 0.11.0. - [Commits](https://github.com/golang/net/compare/v0.9.0...v0.11.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d047b12b..f0d95844 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/stretchr/testify v1.8.2 golang.org/x/crypto v0.10.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/net v0.10.0 + golang.org/x/net v0.11.0 ) require ( diff --git a/go.sum b/go.sum index 10327ec3..a61cb968 100644 --- a/go.sum +++ b/go.sum @@ -379,8 +379,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 9c2cfcef5fbf9f934cccb537205b883e4e2112e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:35:43 -0400 Subject: [PATCH 011/109] Bump github.com/stretchr/testify from 1.8.2 to 1.8.4 (#154) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.4. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.4) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 2 +- go.sum | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index f0d95844..a0421e61 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.10.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/net v0.11.0 diff --git a/go.sum b/go.sum index a61cb968..5044540d 100644 --- a/go.sum +++ b/go.sum @@ -295,17 +295,13 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= From 00e59d89c304615b80f3fee951a0cd2c4f4e0548 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:54:24 -0400 Subject: [PATCH 012/109] Bump actions/setup-go from 3 to 4 (#133) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9cdf49f..3722c69c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: "^1.18.1" From 8a9c92ec3362c5d2f6d2eefe9982e81e100d320c Mon Sep 17 00:00:00 2001 From: anishnaik Date: Sun, 9 Jul 2023 03:51:54 -0400 Subject: [PATCH 013/109] Add optimization mode cli flag (#174) --- cmd/fuzz_flags.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index 936d6598..02300ecb 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -61,6 +61,10 @@ func addFuzzFlags() error { fuzzCmd.Flags().Bool("assertion-mode", false, fmt.Sprintf("enable assertion mode (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.AssertionTesting.Enabled)) + // Optimization mode + fuzzCmd.Flags().Bool("optimization-mode", false, + fmt.Sprintf("enable optimization mode (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.OptimizationTesting.Enabled)) + // Trace all fuzzCmd.Flags().Bool("trace-all", false, fmt.Sprintf("print the execution trace for every element in a shrunken call sequence instead of only the last element (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.TraceAll)) @@ -157,6 +161,14 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. } } + // Update optimization mode enablement + if cmd.Flags().Changed("optimization-mode") { + projectConfig.Fuzzing.Testing.OptimizationTesting.Enabled, err = cmd.Flags().GetBool("optimization-mode") + if err != nil { + return err + } + } + // Update trace all enablement if cmd.Flags().Changed("trace-all") { projectConfig.Fuzzing.Testing.TraceAll, err = cmd.Flags().GetBool("trace-all") From c320d0f3d126906cfa0b6438901fb293fd3cc0c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 Jul 2023 13:11:01 -0400 Subject: [PATCH 014/109] Bump golang.org/x/net from 0.11.0 to 0.12.0 (#176) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.11.0 to 0.12.0. - [Commits](https://github.com/golang/net/compare/v0.11.0...v0.12.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Pokora --- go.mod | 10 +++++----- go.sum | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index a0421e61..3e2af559 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,11 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.10.0 + golang.org/x/crypto v0.11.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/net v0.11.0 + golang.org/x/net v0.12.0 ) require ( @@ -54,14 +55,13 @@ require ( github.com/rivo/uniseg v0.4.3 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5044540d..7e235f86 100644 --- a/go.sum +++ b/go.sum @@ -342,8 +342,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -375,8 +375,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -419,8 +419,8 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -428,8 +428,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From fa4bd62988de37a7cc41b6e25696c666d55d28f6 Mon Sep 17 00:00:00 2001 From: Damilola Edwards Date: Tue, 11 Jul 2023 00:15:44 +0100 Subject: [PATCH 015/109] Extend assertion tests using different panic codes (#161) update assertion mode to include support for all solidity panic codes --- compilation/abiutils/solidity_errors.go | 31 +++++++++++ fuzzing/config/config.go | 37 +++++++++++++ fuzzing/config/config_defaults.go | 3 ++ fuzzing/executiontracer/execution_trace.go | 4 +- fuzzing/fuzzer_test.go | 22 ++++++-- fuzzing/test_case_assertion_provider.go | 53 +++++++++++++++---- .../assert_allocate_too_much_memory.sol | 9 ++++ .../assert_arithmetic_underflow.sol | 9 ++++ .../assert_call_uninitialized_variable.sol | 15 ++++++ .../assertions/assert_divide_by_zero.sol | 10 ++++ ...ssert_enum_type_conversion_outofbounds.sol | 10 ++++ .../assert_incorrect_storage_access.sol | 11 ++++ .../assert_outofbounds_array_access.sol | 9 ++++ .../assertions/assert_pop_empty_array.sol | 9 ++++ 14 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 fuzzing/testdata/contracts/assertions/assert_allocate_too_much_memory.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_arithmetic_underflow.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_call_uninitialized_variable.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_divide_by_zero.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_enum_type_conversion_outofbounds.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_incorrect_storage_access.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_outofbounds_array_access.sol create mode 100644 fuzzing/testdata/contracts/assertions/assert_pop_empty_array.sol diff --git a/compilation/abiutils/solidity_errors.go b/compilation/abiutils/solidity_errors.go index e7335889..384a8fda 100644 --- a/compilation/abiutils/solidity_errors.go +++ b/compilation/abiutils/solidity_errors.go @@ -2,6 +2,7 @@ package abiutils import ( "bytes" + "fmt" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/core/vm" "math/big" @@ -105,3 +106,33 @@ func GetSolidityCustomRevertError(contractAbi *abi.ABI, returnError error, retur } return nil, nil } + +// GetPanicReason will take in a panic code as an uint64 and will return the string reason behind that panic code. For +// example, if panic code is PanicCodeAssertFailed, then "assertion failure" is returned. +func GetPanicReason(panicCode uint64) string { + // Switch on panic code + switch panicCode { + case PanicCodeCompilerInserted: + return "compiler inserted panic" + case PanicCodeAssertFailed: + return "assertion failed" + case PanicCodeArithmeticUnderOverflow: + return "arithmetic underflow" + case PanicCodeDivideByZero: + return "division by zero" + case PanicCodeEnumTypeConversionOutOfBounds: + return "enum access out of bounds" + case PanicCodeIncorrectStorageAccess: + return "incorrect storage access" + case PanicCodePopEmptyArray: + return "pop on empty array" + case PanicCodeOutOfBoundsArrayAccess: + return "out of bounds array access" + case PanicCodeAllocateTooMuchMemory: + return "overallocation of memory" + case PanicCodeCallUninitializedVariable: + return "call on uninitialized variable" + default: + return fmt.Sprintf("unknown panic code(%v)", panicCode) + } +} diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 5bad9b52..a0a60d3d 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -120,6 +120,43 @@ type AssertionTestingConfig struct { // TestViewMethods dictates whether constant/pure/view methods should be tested. TestViewMethods bool `json:"testViewMethods"` + + // AssertionModes describes the various panic codes that can be enabled and be treated as a "failing case" + AssertionModes AssertionModesConfig `json:"assertionModes"` +} + +// AssertionModesConfig describes the configuration options for the various modes that can be enabled for assertion +// testing +type AssertionModesConfig struct { + // FailOnCompilerInsertedPanic describes whether a generic compiler inserted panic should be treated as a failing case + FailOnCompilerInsertedPanic bool `json:"failOnCompilerInsertedPanic"` + + // FailOnAssertion describes whether an assertion failure should be treated as a failing case + FailOnAssertion bool `json:"failOnAssertion"` + + // FailOnArithmeticUnderflow describes whether an arithmetic underflow should be treated as a failing case + FailOnArithmeticUnderflow bool `json:"failOnArithmeticUnderflow"` + + // FailOnDivideByZero describes whether division by zero should be treated as a failing case + FailOnDivideByZero bool `json:"failOnDivideByZero"` + + // FailOnEnumTypeConversionOutOfBounds describes whether an out-of-bounds enum access should be treated as a failing case + FailOnEnumTypeConversionOutOfBounds bool `json:"failOnEnumTypeConversionOutOfBounds"` + + // FailOnIncorrectStorageAccess describes whether an out-of-bounds storage access should be treated as a failing case + FailOnIncorrectStorageAccess bool `json:"failOnIncorrectStorageAccess"` + + // FailOnPopEmptyArray describes whether a pop operation on an empty array should be treated as a failing case + FailOnPopEmptyArray bool `json:"failOnPopEmptyArray"` + + // FailOnOutOfBoundsArrayAccess describes whether an out-of-bounds array access should be treated as a failing case + FailOnOutOfBoundsArrayAccess bool `json:"failOnOutOfBoundsArrayAccess"` + + // FailOnAllocateTooMuchMemory describes whether excessive memory usage should be treated as a failing case + FailOnAllocateTooMuchMemory bool `json:"failOnAllocateTooMuchMemory"` + + // FailOnCallUninitializedVariable describes whether calling an un-initialized variable should be treated as a failing case + FailOnCallUninitializedVariable bool `json:"failOnCallUninitializedVariable"` } // PropertyTestConfig describes the configuration options used for property testing diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 58e6b74d..3a26d3d3 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -59,6 +59,9 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { AssertionTesting: AssertionTestingConfig{ Enabled: false, TestViewMethods: false, + AssertionModes: AssertionModesConfig{ + FailOnAssertion: true, + }, }, PropertyTesting: PropertyTestConfig{ Enabled: true, diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index 6ae459d7..fcca4d24 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -163,8 +163,8 @@ func (t *ExecutionTrace) generateCallFrameExitString(callFrame *CallFrame) strin // Try to resolve a panic message and check if it signals a failed assertion. panicCode := abiutils.GetSolidityPanicCode(callFrame.ReturnError, callFrame.ReturnData, true) - if panicCode != nil && panicCode.Uint64() == abiutils.PanicCodeAssertFailed { - return "[assertion failed]" + if panicCode != nil { + return "[" + abiutils.GetPanicReason(panicCode.Uint64()) + "]" } // Try to resolve an assertion failed panic code. diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 85631501..7a80786f 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -56,11 +56,19 @@ func TestFuzzerHooks(t *testing.T) { }) } -// TestAssertionsBasicSolving runs tests to ensure that assertion testing behaves as expected. -func TestAssertionsBasicSolving(t *testing.T) { +// TestAssertionMode runs tests to ensure that assertion testing behaves as expected. +func TestAssertionMode(t *testing.T) { filePaths := []string{ "testdata/contracts/assertions/assert_immediate.sol", "testdata/contracts/assertions/assert_even_number.sol", + "testdata/contracts/assertions/assert_arithmetic_underflow.sol", + "testdata/contracts/assertions/assert_divide_by_zero.sol", + "testdata/contracts/assertions/assert_enum_type_conversion_outofbounds.sol", + "testdata/contracts/assertions/assert_incorrect_storage_access.sol", + "testdata/contracts/assertions/assert_pop_empty_array.sol", + "testdata/contracts/assertions/assert_outofbounds_array_access.sol", + "testdata/contracts/assertions/assert_allocate_too_much_memory.sol", + "testdata/contracts/assertions/assert_call_uninitialized_variable.sol", } for _, filePath := range filePaths { runFuzzerTest(t, &fuzzerSolcFileTest{ @@ -69,12 +77,20 @@ func TestAssertionsBasicSolving(t *testing.T) { config.Fuzzing.DeploymentOrder = []string{"TestContract"} config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnAssertion = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnAllocateTooMuchMemory = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnArithmeticUnderflow = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnCallUninitializedVariable = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnEnumTypeConversionOutOfBounds = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnDivideByZero = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnIncorrectStorageAccess = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnOutOfBoundsArrayAccess = true + config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnPopEmptyArray = true }, method: func(f *fuzzerTestContext) { // Start the fuzzer err := f.fuzzer.Start() assert.NoError(t, err) - // Check for failed assertion tests. assertFailedTestsExpected(f, true) }, diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index e332cd65..300d97dc 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -3,11 +3,11 @@ package fuzzing import ( "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/calls" - "golang.org/x/exp/slices" - "sync" - + "github.com/crytic/medusa/fuzzing/config" "github.com/crytic/medusa/fuzzing/contracts" "github.com/ethereum/go-ethereum/accounts/abi" + "golang.org/x/exp/slices" + "sync" ) // AssertionTestCaseProvider is am AssertionTestCase provider which spawns test cases for every contract method and @@ -24,8 +24,6 @@ type AssertionTestCaseProvider struct { testCasesLock sync.Mutex } -// Define our ABI method - // attachAssertionTestCaseProvider attaches a new AssertionTestCaseProvider to the Fuzzer and returns it. func attachAssertionTestCaseProvider(fuzzer *Fuzzer) *AssertionTestCaseProvider { // Create a test case provider @@ -66,16 +64,19 @@ func (t *AssertionTestCaseProvider) checkAssertionFailures(callSequence calls.Ca } methodId := contracts.GetContractMethodID(lastCall.Contract, lastCallMethod) - // Check if we encountered an assertion error. - // Try to unpack our error and return data for a panic code and verify it matches the "assert failed" panic code. + // Check if we encountered an enabled panic code. + // Try to unpack our error and return data for a panic code and verify that that panic code should be treated as a failing case. // Solidity >0.8.0 introduced asserts failing as reverts but with special return data. But we indicate we also // want to be backwards compatible with older Solidity which simply hit an invalid opcode and did not actually // have a panic code. lastExecutionResult := lastCall.ChainReference.MessageResults().ExecutionResult panicCode := abiutils.GetSolidityPanicCode(lastExecutionResult.Err, lastExecutionResult.ReturnData, true) - encounteredAssertionFailure := panicCode != nil && panicCode.Uint64() == abiutils.PanicCodeAssertFailed + failure := false + if panicCode != nil { + failure = encounteredAssertionFailure(panicCode.Uint64(), t.fuzzer.config.Fuzzing.Testing.AssertionTesting.AssertionModes) + } - return &methodId, encounteredAssertionFailure, nil + return &methodId, failure, nil } // onFuzzerStarting is the event handler triggered when the Fuzzer is starting a fuzzing campaign. It creates test cases @@ -234,3 +235,37 @@ func (t *AssertionTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorke return shrinkRequests, nil } + +// encounteredAssertionFailure takes in a panic code and a config.AssertionModesConfig and will determine whether the +// panic code that was hit should be treated as a failing case - which will be determined by whether that panic +// code was enabled in the config. Note that the panic codes are defined in the abiutils package and that this function +// panic if it is provided a panic code that is not defined in the abiutils package. +// TODO: This is a terrible design and a future PR should be made to maintain assertion and panic logic correctly +func encounteredAssertionFailure(panicCode uint64, conf config.AssertionModesConfig) bool { + // Switch on panic code + switch panicCode { + case abiutils.PanicCodeCompilerInserted: + return conf.FailOnCompilerInsertedPanic + case abiutils.PanicCodeAssertFailed: + return conf.FailOnAssertion + case abiutils.PanicCodeArithmeticUnderOverflow: + return conf.FailOnArithmeticUnderflow + case abiutils.PanicCodeDivideByZero: + return conf.FailOnDivideByZero + case abiutils.PanicCodeEnumTypeConversionOutOfBounds: + return conf.FailOnEnumTypeConversionOutOfBounds + case abiutils.PanicCodeIncorrectStorageAccess: + return conf.FailOnIncorrectStorageAccess + case abiutils.PanicCodePopEmptyArray: + return conf.FailOnPopEmptyArray + case abiutils.PanicCodeOutOfBoundsArrayAccess: + return conf.FailOnOutOfBoundsArrayAccess + case abiutils.PanicCodeAllocateTooMuchMemory: + return conf.FailOnAllocateTooMuchMemory + case abiutils.PanicCodeCallUninitializedVariable: + return conf.FailOnCallUninitializedVariable + default: + // If we encounter an unknown panic code, we ignore it + return false + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_allocate_too_much_memory.sol b/fuzzing/testdata/contracts/assertions/assert_allocate_too_much_memory.sol new file mode 100644 index 00000000..ff4be401 --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_allocate_too_much_memory.sol @@ -0,0 +1,9 @@ +// This contract attempts to allocate an excessive amount of memory by creating an array with a length of 2^64 causing a panic. +// PanicCodeAllocateTooMuchMemory = 0x41 + +contract TestContract { + function allocateTooMuchMemory() public { + uint256[] memory myArray = new uint256[](2**64); // Allocate too much memory + myArray[2**64 - 1] = 42; + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_arithmetic_underflow.sol b/fuzzing/testdata/contracts/assertions/assert_arithmetic_underflow.sol new file mode 100644 index 00000000..c4930d04 --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_arithmetic_underflow.sol @@ -0,0 +1,9 @@ +// A call to `arithmeticOverflow` function in this contract would trigger an arithmetic overflow panic +// PanicCodeArithmeticUnderOverflow = 0x11 +contract TestContract { + function arithmeticOverflow() public { + uint8 a = 255; + uint8 b = 1; + uint8 c = a + b; + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_call_uninitialized_variable.sol b/fuzzing/testdata/contracts/assertions/assert_call_uninitialized_variable.sol new file mode 100644 index 00000000..7a9b64e3 --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_call_uninitialized_variable.sol @@ -0,0 +1,15 @@ +// An attempt to call an uninitialized function pointer would cause a panic +// PanicCodeCallUninitializedVariable = 0x51 + +contract TestContract { + + function uninitializedVariableCall() public returns (int) + { + // Variable containing a function pointer + function (int, int) internal pure returns (int) funcPtr; + + // This call will fail because funcPtr is still a zero-initialized function pointer + return funcPtr(4, 5); + } + +} diff --git a/fuzzing/testdata/contracts/assertions/assert_divide_by_zero.sol b/fuzzing/testdata/contracts/assertions/assert_divide_by_zero.sol new file mode 100644 index 00000000..14c1736f --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_divide_by_zero.sol @@ -0,0 +1,10 @@ +// Division operation performed with a divisor of zero would cause a panic +// PanicCodeDivideByZero = 0x12 + +contract TestContract { + function divideByZero() public { + uint8 a = 42; + uint8 b = 0; + uint8 c = a / b; + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_enum_type_conversion_outofbounds.sol b/fuzzing/testdata/contracts/assertions/assert_enum_type_conversion_outofbounds.sol new file mode 100644 index 00000000..626a3e10 --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_enum_type_conversion_outofbounds.sol @@ -0,0 +1,10 @@ +// Enum type conversion out of bounds would cause a panic +// PanicCodeEnumTypeConversionOutOfBounds = 0x21 +contract TestContract { + enum MyEnum { A, B, C } + + function enumTypeConversionOutOfBounds() public { + uint8 value = 4; // Out of bounds for MyEnum + MyEnum myEnum = MyEnum(value); + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_incorrect_storage_access.sol b/fuzzing/testdata/contracts/assertions/assert_incorrect_storage_access.sol new file mode 100644 index 00000000..6ff0cabc --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_incorrect_storage_access.sol @@ -0,0 +1,11 @@ +// This contract triggers an incorrect storage access panic +// PanicCodeIncorrectStorageAccess = 0x22 + +contract TestContract { + uint256[] public myArray; + + function incorrectStorageAccess() public returns(uint256) { + uint256 index = 7; // Index out of bounds + return myArray[index]; // Incorrect storage access + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_outofbounds_array_access.sol b/fuzzing/testdata/contracts/assertions/assert_outofbounds_array_access.sol new file mode 100644 index 00000000..79feed60 --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_outofbounds_array_access.sol @@ -0,0 +1,9 @@ +// This contract triggers an out-of-bounds array access panic +// PanicCodeOutOfBoundsArrayAccess = 0x32 + +contract TestContract { + function outOfBoundsArrayAccess() public { + uint256[] memory myArray = new uint256[](5); + uint256 value = myArray[6]; // Out of bounds array access + } +} diff --git a/fuzzing/testdata/contracts/assertions/assert_pop_empty_array.sol b/fuzzing/testdata/contracts/assertions/assert_pop_empty_array.sol new file mode 100644 index 00000000..345cca45 --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_pop_empty_array.sol @@ -0,0 +1,9 @@ +// Popping from an empty array triggers a `PopEmptyArray` panic +// PanicCodePopEmptyArray = 0x31 + +contract TestContract { + uint256[] public myArray; + function popEmptyArray() public { + myArray.pop(); // Pop from empty array + } +} From 13893a63c621c54a72fa6dba47c59fe08b4b97f1 Mon Sep 17 00:00:00 2001 From: Gustavo Grieco <31542053+ggrieco-tob@users.noreply.github.com> Date: Wed, 12 Jul 2023 01:05:28 +0200 Subject: [PATCH 016/109] Value shrinking (#147) perform shrinking of values within the optimized, shrunken call sequence --- fuzzing/fuzzer.go | 27 ++- fuzzing/fuzzer_hooks.go | 18 +- fuzzing/fuzzer_worker.go | 194 +++++++++++------ fuzzing/fuzzer_worker_sequence_generator.go | 7 +- fuzzing/valuegeneration/abi_values.go | 42 ++-- fuzzing/valuegeneration/abi_values_test.go | 8 +- fuzzing/valuegeneration/generator.go | 31 +++ ...or_mutating.go => generator_mutational.go} | 89 ++++---- ...enerator_random.go => generator_random.go} | 12 +- ...alue_generator_interface.go => mutator.go} | 24 +-- fuzzing/valuegeneration/mutator_shrinking.go | 195 ++++++++++++++++++ utils/integer_utils.go | 3 +- 12 files changed, 474 insertions(+), 176 deletions(-) create mode 100644 fuzzing/valuegeneration/generator.go rename fuzzing/valuegeneration/{value_generator_mutating.go => generator_mutational.go} (83%) rename fuzzing/valuegeneration/{value_generator_random.go => generator_random.go} (90%) rename fuzzing/valuegeneration/{value_generator_interface.go => mutator.go} (51%) create mode 100644 fuzzing/valuegeneration/mutator_shrinking.go diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 15edf513..2ad2d2f6 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -107,7 +107,8 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { testCases: make([]TestCase, 0), testCasesFinished: make(map[string]TestCase), Hooks: FuzzerHooks{ - NewCallSequenceGeneratorConfigFunc: defaultNewCallSequenceGeneratorConfigFunc, + NewCallSequenceGeneratorConfigFunc: defaultCallSequenceGeneratorConfigFunc, + NewShrinkingValueMutatorFunc: defaultShrinkingValueMutatorFunc, ChainSetupFunc: chainSetupFromCompilations, CallSequenceTestFuncs: make([]CallSequenceTestFunc, 0), }, @@ -376,11 +377,11 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro return nil } -// defaultNewCallSequenceGeneratorConfigFunc is a NewCallSequenceGeneratorConfigFunc which creates a +// defaultCallSequenceGeneratorConfigFunc is a NewCallSequenceGeneratorConfigFunc which creates a // CallSequenceGeneratorConfig with a default configuration. Returns the config or an error, if one occurs. -func defaultNewCallSequenceGeneratorConfigFunc(fuzzer *Fuzzer, valueSet *valuegeneration.ValueSet, randomProvider *rand.Rand) (*CallSequenceGeneratorConfig, error) { - // Create the underlying value generator for the worker and its sequence generator. - valueGenConfig := &valuegeneration.MutatingValueGeneratorConfig{ +func defaultCallSequenceGeneratorConfigFunc(fuzzer *Fuzzer, valueSet *valuegeneration.ValueSet, randomProvider *rand.Rand) (*CallSequenceGeneratorConfig, error) { + // Create the value generator and mutator for the worker. + mutationalGeneratorConfig := &valuegeneration.MutationalValueGeneratorConfig{ MinMutationRounds: 0, MaxMutationRounds: 1, GenerateRandomAddressBias: 0.5, @@ -406,7 +407,7 @@ func defaultNewCallSequenceGeneratorConfigFunc(fuzzer *Fuzzer, valueSet *valuege GenerateRandomStringMaxSize: 100, }, } - valueGenerator := valuegeneration.NewMutatingValueGenerator(valueGenConfig, valueSet, randomProvider) + mutationalGenerator := valuegeneration.NewMutationalValueGenerator(mutationalGeneratorConfig, valueSet, randomProvider) // Create a sequence generator config which uses the created value generator. sequenceGenConfig := &CallSequenceGeneratorConfig{ @@ -419,11 +420,23 @@ func defaultNewCallSequenceGeneratorConfigFunc(fuzzer *Fuzzer, valueSet *valuege RandomMutatedCorpusTailWeight: 10, RandomMutatedSpliceAtRandomWeight: 20, RandomMutatedInterleaveAtRandomWeight: 10, - ValueGenerator: valueGenerator, + ValueGenerator: mutationalGenerator, + ValueMutator: mutationalGenerator, } return sequenceGenConfig, nil } +// defaultShrinkingValueMutatorFunc is a NewShrinkingValueMutatorFunc which creates value mutator to be used for +// shrinking purposes. Returns the value mutator or an error, if one occurs. +func defaultShrinkingValueMutatorFunc(fuzzer *Fuzzer, valueSet *valuegeneration.ValueSet, randomProvider *rand.Rand) (valuegeneration.ValueMutator, error) { + // Create the shrinking value mutator for the worker. + shrinkingValueMutatorConfig := &valuegeneration.ShrinkingValueMutatorConfig{ + ShrinkValueProbability: 0.1, + } + shrinkingValueMutator := valuegeneration.NewShrinkingValueMutator(shrinkingValueMutatorConfig, valueSet, randomProvider) + return shrinkingValueMutator, nil +} + // spawnWorkersLoop is a method which spawns a config-defined amount of FuzzerWorker to carry out the fuzzing campaign. // This function exits when Fuzzer.ctx is cancelled. func (f *Fuzzer) spawnWorkersLoop(baseTestChain *chain.TestChain) error { diff --git a/fuzzing/fuzzer_hooks.go b/fuzzing/fuzzer_hooks.go index d590d445..ea2d8486 100644 --- a/fuzzing/fuzzer_hooks.go +++ b/fuzzing/fuzzer_hooks.go @@ -1,20 +1,27 @@ package fuzzing import ( + "math/rand" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" - "math/rand" ) // FuzzerHooks defines the hooks that can be used for the Fuzzer on an API level. type FuzzerHooks struct { // NewCallSequenceGeneratorConfigFunc describes the function to use to set up a new CallSequenceGeneratorConfig, // defining parameters for a new FuzzerWorker's CallSequenceGenerator. - // Note: The value generator provided within the config must be either thread safe, or a new instance must be - // provided per call to avoid concurrent access issues between workers. + // The value generator provided must be either thread safe, or a new instance must be provided per invocation to + // avoid concurrent access issues between workers. NewCallSequenceGeneratorConfigFunc NewCallSequenceGeneratorConfigFunc + // NewShrinkingValueMutatorFunc describes the function used to set up a value mutator used to shrink call + // values in the fuzzer's call sequence shrinking process. + // The value mutator provided must be either thread safe, or a new instance must be provided per invocation to + // avoid concurrent access issues between workers. + NewShrinkingValueMutatorFunc NewShrinkingValueMutatorFunc + // ChainSetupFunc describes the function to use to set up a new test chain's initial state prior to fuzzing. ChainSetupFunc TestChainSetupFunc @@ -23,6 +30,11 @@ type FuzzerHooks struct { CallSequenceTestFuncs []CallSequenceTestFunc } +// NewShrinkingValueMutatorFunc describes the function used to set up a value mutator used to shrink call +// values in the fuzzer's call sequence shrinking process. +// Returns a new value mutator, or an error if one occurred. +type NewShrinkingValueMutatorFunc func(fuzzer *Fuzzer, valueSet *valuegeneration.ValueSet, randomProvider *rand.Rand) (valuegeneration.ValueMutator, error) + // NewCallSequenceGeneratorConfigFunc defines a method is called to create a new CallSequenceGeneratorConfig, defining // the parameters for the new FuzzerWorker to use when creating its CallSequenceGenerator used to power fuzzing. // Returns a new CallSequenceGeneratorConfig, or an error if one is encountered. diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 89ff7576..f347c682 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -2,6 +2,9 @@ package fuzzing import ( "fmt" + "math/big" + "math/rand" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" fuzzerTypes "github.com/crytic/medusa/fuzzing/contracts" @@ -10,8 +13,6 @@ import ( "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "golang.org/x/exp/maps" - "math/big" - "math/rand" ) // FuzzerWorker describes a single thread worker utilizing its own go-ethereum test node to run property tests against @@ -43,6 +44,11 @@ type FuzzerWorker struct { // sequenceGenerator creates entirely new or mutated call sequences based on corpus call sequences, for use in // fuzzing campaigns. sequenceGenerator *CallSequenceGenerator + + // shrinkingValueMutator is a value mutator which is used to mutate existing call sequence values in an attempt to shrink + // their values, in the call sequence shrinking process. + shrinkingValueMutator valuegeneration.ValueMutator + // valueSet defines a set derived from Fuzzer.BaseValueSet which is further populated with runtime values by the // FuzzerWorker. It is the value set shared with the underlying valueGenerator. valueSet *valuegeneration.ValueSet @@ -64,6 +70,12 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) return nil, err } + // Create a new shrinking value mutator for this new worker. + shrinkingValueMutator, err := fuzzer.Hooks.NewShrinkingValueMutatorFunc(fuzzer, valueSet, randomProvider) + if err != nil { + return nil, err + } + // Create a new worker with the data provided. worker := &FuzzerWorker{ workerIndex: workerIndex, @@ -75,6 +87,7 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) valueSet: valueSet, } worker.sequenceGenerator = NewCallSequenceGenerator(worker, callSequenceGenConfig) + worker.shrinkingValueMutator = shrinkingValueMutator return worker, nil } @@ -124,6 +137,11 @@ func (fw *FuzzerWorker) ValueGenerator() valuegeneration.ValueGenerator { return fw.sequenceGenerator.config.ValueGenerator } +// ValueMutator obtains the value mutator used by this worker. +func (fw *FuzzerWorker) ValueMutator() valuegeneration.ValueMutator { + return fw.sequenceGenerator.config.ValueMutator +} + // getNewCorpusCallSequenceWeight returns a big integer representing the weight that a new corpus item being added now // should have in the corpus' weighted random chooser. func (fw *FuzzerWorker) getNewCorpusCallSequenceWeight() *big.Int { @@ -215,12 +233,12 @@ func (fw *FuzzerWorker) updateStateChangingMethods() { } } -// testCallSequence tests a call message sequence against the underlying FuzzerWorker's Chain and calls every +// testNextCallSequence tests a call message sequence against the underlying FuzzerWorker's Chain and calls every // CallSequenceTestFunc registered with the parent Fuzzer to update any test results. If any call message in the // sequence is nil, a call message will be created in its place, targeting a state changing method of a contract // deployed in the Chain. // Returns the length of the call sequence tested, any requests for call sequence shrinking, or an error if one occurs. -func (fw *FuzzerWorker) testCallSequence() (calls.CallSequence, []ShrinkCallSequenceRequest, error) { +func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCallSequenceRequest, error) { // After testing the sequence, we'll want to rollback changes to reset our testing state. var err error defer func() { @@ -301,105 +319,149 @@ func (fw *FuzzerWorker) testCallSequence() (calls.CallSequence, []ShrinkCallSequ return testedCallSequence, shrinkCallSequenceRequests, nil } -// shrinkCallSequence takes a provided call sequence and attempts to shrink it by looking for redundant -// calls which can be removed that continue to satisfy the provided shrink verifier. -// Returns a call sequence that was optimized to include as little calls as possible to trigger the -// expected conditions, or an error if one occurred. -func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shrinkRequest ShrinkCallSequenceRequest) (calls.CallSequence, error) { - // In case of any error, we defer an operation to revert our chain state. We purposefully ignore errors from it to - // prioritize any others which occurred. - var err error - defer func() { - if err == nil { - err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber) +// testShrunkenCallSequence tests a provided shrunken call sequence to verify it continues to satisfy the provided +// shrink verifier. Chain state is reverted to the testing base prior to returning. +// Returns a boolean indicating if the shrunken call sequence is valid for a given shrink request, or an error if one occurred. +func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.CallSequence, shrinkRequest ShrinkCallSequenceRequest) (bool, error) { + // Our "fetch next call method" method will simply fetch and fix the call message in case any fields are not correct due to shrinking. + fetchElementFunc := func(currentIndex int) (*calls.CallSequenceElement, error) { + // If we are at the end of our sequence, return nil indicating we should stop executing. + if currentIndex >= len(possibleShrunkSequence) { + return nil, nil } - }() - // Define a variable to track our most optimized sequence across all optimization iterations. - optimizedSequence := callSequence + possibleShrunkSequence[currentIndex].Call.FillFromTestChainProperties(fw.chain) + return possibleShrunkSequence[currentIndex], nil + } - for i := 0; i < len(optimizedSequence); { - // Recreate our current optimized sequence without the item at this index - possibleShrunkSequence, err := optimizedSequence.Clone() + // Our "post-execution check" method will check coverage and call all testing functions. If one returns a + // request for a shrunk call sequence, we exit our call sequence execution immediately to go fulfill the shrink + // request. + executionCheckFunc := func(currentlyExecutedSequence calls.CallSequence) (bool, error) { + // Check for updates to coverage and corpus (using only the section of the sequence we tested so far). + // If we detect coverage changes, add this sequence. + err := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) if err != nil { - return nil, err + return true, err } - possibleShrunkSequence = append(possibleShrunkSequence[:i], possibleShrunkSequence[i+1:]...) - // Our "fetch next call method" method will simply fetch and fix the call message in case any fields are not correct due to shrinking. - fetchElementFunc := func(currentIndex int) (*calls.CallSequenceElement, error) { - // If we are at the end of our sequence, return nil indicating we should stop executing. - if currentIndex >= len(possibleShrunkSequence) { - return nil, nil - } - - possibleShrunkSequence[currentIndex].Call.FillFromTestChainProperties(fw.chain) - return possibleShrunkSequence[currentIndex], nil + // If our fuzzer context is done, exit out immediately without results. + if utils.CheckContextDone(fw.fuzzer.ctx) { + return true, nil } - // Our "post-execution check" method will check coverage and call all testing functions. If one returns a - // request for a shrunk call sequence, we exit our call sequence execution immediately to go fulfill the shrink - // request. - executionCheckFunc := func(currentlyExecutedSequence calls.CallSequence) (bool, error) { - // Check for updates to coverage and corpus (using only the section of the sequence we tested so far). - // If we detect coverage changes, add this sequence. - err := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) - if err != nil { - return true, err - } + return false, nil + } - // If our fuzzer context is done, exit out immediately without results. - if utils.CheckContextDone(fw.fuzzer.ctx) { - return true, nil - } + // Execute our call sequence. + _, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) + if err != nil { + return false, err + } - return false, nil - } + // If our fuzzer context is done, exit out immediately without results. + if utils.CheckContextDone(fw.fuzzer.ctx) { + return false, nil + } - // Execute our call sequence. - testedPossibleShrunkSequence, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) + // Check if our verifier signalled that we met our conditions + validShrunkSequence := false + if len(possibleShrunkSequence) > 0 { + validShrunkSequence, err = shrinkRequest.VerifierFunction(fw, possibleShrunkSequence) if err != nil { - return nil, err + return false, err } + } + // After testing the sequence, we'll want to rollback changes to reset our testing state. + if err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber); err != nil { + return false, err + } + return validShrunkSequence, nil +} + +// shrinkCallSequence takes a provided call sequence and attempts to shrink it by looking for redundant +// calls which can be removed, and values which can be minimized, while continuing to satisfy the provided shrink +// verifier. +// Returns a call sequence that was optimized to include as little calls as possible to trigger the +// expected conditions, or an error if one occurred. +func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shrinkRequest ShrinkCallSequenceRequest) (calls.CallSequence, error) { + // Define a variable to track our most optimized sequence across all optimization iterations. + optimizedSequence := callSequence + + // First try to remove any calls we can. We go from start to end to avoid index shifting. + for i := 0; i < len(optimizedSequence); { // If our fuzzer context is done, exit out immediately without results. if utils.CheckContextDone(fw.fuzzer.ctx) { return nil, nil } - // Check if our verifier signalled that we met our conditions - validShrunkSequence := false - if len(testedPossibleShrunkSequence) > 0 { - validShrunkSequence, err = shrinkRequest.VerifierFunction(fw, testedPossibleShrunkSequence) - if err != nil { - return nil, err - } + // Recreate our current optimized sequence without the item at this index + possibleShrunkSequence, err := optimizedSequence.Clone() + if err != nil { + return nil, err } + possibleShrunkSequence = append(possibleShrunkSequence[:i], possibleShrunkSequence[i+1:]...) - // After testing the sequence, we'll want to rollback changes to reset our testing state. - if err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber); err != nil { + // Test the shrunken sequence. + validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) + if err != nil { return nil, err } // If this current sequence satisfied our conditions, set it as our optimized sequence. if validShrunkSequence { - optimizedSequence = testedPossibleShrunkSequence + optimizedSequence = possibleShrunkSequence } else { // We didn't remove an item at this index, so we'll iterate to the next one. i++ } } + // Next try to shrink our values of every transaction a given number of rounds. + for i := 0; i < len(optimizedSequence); i++ { + for optimizationRound := 0; optimizationRound < 200; optimizationRound++ { + // If our fuzzer context is done, exit out immediately without results. + if utils.CheckContextDone(fw.fuzzer.ctx) { + return nil, nil + } + + // Clone the optimized sequence. + possibleShrunkSequence, _ := optimizedSequence.Clone() + + // Loop for each argument in the currently indexed call to mutate it. + abiValuesMsgData := possibleShrunkSequence[i].Call.MsgDataAbiValues + for j := 0; j < len(abiValuesMsgData.InputValues); j++ { + mutatedInput, err := valuegeneration.MutateAbiValue(fw.sequenceGenerator.config.ValueGenerator, fw.shrinkingValueMutator, &abiValuesMsgData.Method.Inputs[j].Type, abiValuesMsgData.InputValues[j]) + if err != nil { + return nil, fmt.Errorf("error when shrinking call sequence input argument: %v", err) + } + abiValuesMsgData.InputValues[j] = mutatedInput + } + + // Test the shrunken sequence. + validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) + if err != nil { + return nil, err + } + + // If this current sequence satisfied our conditions, set it as our optimized sequence. + if validShrunkSequence { + optimizedSequence = possibleShrunkSequence + } + } + } + // If the shrink request wanted the sequence recorded in the corpus, do so now. if shrinkRequest.RecordResultInCorpus { - err = fw.fuzzer.corpus.AddTestResultCallSequence(optimizedSequence, fw.getNewCorpusCallSequenceWeight(), true) + err := fw.fuzzer.corpus.AddTestResultCallSequence(optimizedSequence, fw.getNewCorpusCallSequenceWeight(), true) if err != nil { return nil, err } } // We have a finalized call sequence, re-execute it, so our current chain state is representative of post-execution. - _, err = calls.ExecuteCallSequence(fw.chain, optimizedSequence) + _, err := calls.ExecuteCallSequence(fw.chain, optimizedSequence) if err != nil { return nil, err } @@ -420,6 +482,10 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri return nil, err } + // After testing the sequence, we'll want to rollback changes to reset our testing state. + if err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber); err != nil { + return nil, err + } return optimizedSequence, err } @@ -493,7 +559,7 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { } // Test a new sequence - callSequence, shrinkVerifiers, err := fw.testCallSequence() + callSequence, shrinkVerifiers, err := fw.testNextCallSequence() if err != nil { return false, err } diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index bbb83fc1..8892cd3b 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -84,10 +84,13 @@ type CallSequenceGeneratorConfig struct { // number of calls from each. RandomMutatedInterleaveAtRandomWeight uint64 - // ValueGenerator defines the value provider to use when generating or mutating call sequences. This is used both + // ValueGenerator defines the value provider to use when generating new values for call sequences. This is used both // for ABI call data generation, and generation of additional values such as the "value" field of a // transaction/call. ValueGenerator valuegeneration.ValueGenerator + + // ValueMutator defines the value provider to use when mutating corpus call sequences. + ValueMutator valuegeneration.ValueMutator } // CallSequenceGeneratorFunc defines a method used to populate a provided call sequence with generated calls. @@ -445,7 +448,7 @@ func prefetchModifyCallFuncMutate(sequenceGenerator *CallSequenceGenerator, elem // Loop for each input value and mutate it abiValuesMsgData := element.Call.MsgDataAbiValues for i := 0; i < len(abiValuesMsgData.InputValues); i++ { - mutatedInput, err := valuegeneration.MutateAbiValue(sequenceGenerator.config.ValueGenerator, &abiValuesMsgData.Method.Inputs[i].Type, abiValuesMsgData.InputValues[i]) + mutatedInput, err := valuegeneration.MutateAbiValue(sequenceGenerator.config.ValueGenerator, sequenceGenerator.config.ValueMutator, &abiValuesMsgData.Method.Inputs[i].Type, abiValuesMsgData.InputValues[i]) if err != nil { return fmt.Errorf("error when mutating call sequence input argument: %v", err) } diff --git a/fuzzing/valuegeneration/abi_values.go b/fuzzing/valuegeneration/abi_values.go index b7b91cc5..5153dee1 100644 --- a/fuzzing/valuegeneration/abi_values.go +++ b/fuzzing/valuegeneration/abi_values.go @@ -99,7 +99,7 @@ func GenerateAbiValue(generator ValueGenerator, inputType *abi.Type) any { // MutateAbiValue takes an ABI packable input value, alongside its type definition and a value generator, to mutate // existing ABI input values. -func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (any, error) { +func MutateAbiValue(generator ValueGenerator, mutator ValueMutator, inputType *abi.Type, value any) (any, error) { // Switch on the type of value and mutate it recursively. switch inputType.T { case abi.AddressTy: @@ -107,38 +107,38 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a if !ok { return nil, fmt.Errorf("could not mutate address input as the value provided is not an address type") } - return generator.MutateAddress(addr), nil + return mutator.MutateAddress(addr), nil case abi.UintTy: if inputType.Size == 64 { v, ok := value.(uint64) if !ok { return nil, fmt.Errorf("could not mutate uint%v input as the value provided is not of the correct type", inputType.Size) } - return generator.MutateInteger(new(big.Int).SetUint64(v), false, inputType.Size).Uint64(), nil + return mutator.MutateInteger(new(big.Int).SetUint64(v), false, inputType.Size).Uint64(), nil } else if inputType.Size == 32 { v, ok := value.(uint32) if !ok { return nil, fmt.Errorf("could not mutate uint%v input as the value provided is not of the correct type", inputType.Size) } - return uint32(generator.MutateInteger(new(big.Int).SetUint64(uint64(v)), false, inputType.Size).Uint64()), nil + return uint32(mutator.MutateInteger(new(big.Int).SetUint64(uint64(v)), false, inputType.Size).Uint64()), nil } else if inputType.Size == 16 { v, ok := value.(uint16) if !ok { return nil, fmt.Errorf("could not mutate uint%v input as the value provided is not of the correct type", inputType.Size) } - return uint16(generator.MutateInteger(new(big.Int).SetUint64(uint64(v)), false, inputType.Size).Uint64()), nil + return uint16(mutator.MutateInteger(new(big.Int).SetUint64(uint64(v)), false, inputType.Size).Uint64()), nil } else if inputType.Size == 8 { v, ok := value.(uint8) if !ok { return nil, fmt.Errorf("could not mutate uint%v input as the value provided is not of the correct type", inputType.Size) } - return uint8(generator.MutateInteger(new(big.Int).SetUint64(uint64(v)), false, inputType.Size).Uint64()), nil + return uint8(mutator.MutateInteger(new(big.Int).SetUint64(uint64(v)), false, inputType.Size).Uint64()), nil } else { v, ok := value.(*big.Int) if !ok { return nil, fmt.Errorf("could not mutate uint%v input as the value provided is not of the correct type", inputType.Size) } - return generator.MutateInteger(new(big.Int).Set(v), false, inputType.Size), nil + return mutator.MutateInteger(new(big.Int).Set(v), false, inputType.Size), nil } case abi.IntTy: if inputType.Size == 64 { @@ -146,55 +146,55 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a if !ok { return nil, fmt.Errorf("could not mutate int%v input as the value provided is not of the correct type", inputType.Size) } - return generator.MutateInteger(new(big.Int).SetInt64(v), true, inputType.Size).Int64(), nil + return mutator.MutateInteger(new(big.Int).SetInt64(v), true, inputType.Size).Int64(), nil } else if inputType.Size == 32 { v, ok := value.(int32) if !ok { return nil, fmt.Errorf("could not mutate int%v input as the value provided is not of the correct type", inputType.Size) } - return int32(generator.MutateInteger(new(big.Int).SetInt64(int64(v)), true, inputType.Size).Int64()), nil + return int32(mutator.MutateInteger(new(big.Int).SetInt64(int64(v)), true, inputType.Size).Int64()), nil } else if inputType.Size == 16 { v, ok := value.(int16) if !ok { return nil, fmt.Errorf("could not mutate int%v input as the value provided is not of the correct type", inputType.Size) } - return int16(generator.MutateInteger(new(big.Int).SetInt64(int64(v)), true, inputType.Size).Int64()), nil + return int16(mutator.MutateInteger(new(big.Int).SetInt64(int64(v)), true, inputType.Size).Int64()), nil } else if inputType.Size == 8 { v, ok := value.(int8) if !ok { return nil, fmt.Errorf("could not mutate int%v input as the value provided is not of the correct type", inputType.Size) } - return int8(generator.MutateInteger(new(big.Int).SetInt64(int64(v)), true, inputType.Size).Int64()), nil + return int8(mutator.MutateInteger(new(big.Int).SetInt64(int64(v)), true, inputType.Size).Int64()), nil } else { v, ok := value.(*big.Int) if !ok { return nil, fmt.Errorf("could not mutate int%v input as the value provided is not of the correct type", inputType.Size) } - return generator.MutateInteger(new(big.Int).Set(v), true, inputType.Size), nil + return mutator.MutateInteger(new(big.Int).Set(v), true, inputType.Size), nil } case abi.BoolTy: v, ok := value.(bool) if !ok { return nil, fmt.Errorf("could not mutate boolean input as the value provided is not a boolean type") } - return generator.MutateBool(v), nil + return mutator.MutateBool(v), nil case abi.StringTy: v, ok := value.(string) if !ok { return nil, fmt.Errorf("could not mutate string input as the value provided is not a string type") } - return generator.MutateString(v), nil + return mutator.MutateString(v), nil case abi.BytesTy: v, ok := value.([]byte) if !ok { return nil, fmt.Errorf("could not mutate dynamic-sized bytes input as the value provided is not a byte slice type") } - return generator.MutateBytes(v), nil + return mutator.MutateBytes(v), nil case abi.FixedBytesTy: // This needs to be an array type, not a slice. But arrays can't be dynamically defined without reflection. // We opt to keep our API for generators simple, creating the array here and copying elements from a slice. valueAsSlice := reflectionutils.ArrayToSlice(reflect.ValueOf(value)).([]byte) - mutatedValue := generator.MutateFixedBytes(valueAsSlice) + mutatedValue := mutator.MutateFixedBytes(valueAsSlice) mutatedValueAsArray := reflectionutils.SliceToArray(reflect.ValueOf(mutatedValue)) mutatedValueAsArrayLen := reflect.ValueOf(mutatedValueAsArray).Len() if mutatedValueAsArrayLen != inputType.Size { @@ -207,7 +207,7 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a array := reflectionutils.CopyReflectedType(reflect.ValueOf(value)) // Mutate our array structure first - mutatedValues := generator.MutateArray(reflectionutils.GetReflectedArrayValues(array), true) + mutatedValues := mutator.MutateArray(reflectionutils.GetReflectedArrayValues(array), true) // Create a new array of the appropriate size array = reflect.New(reflect.ArrayOf(array.Len(), array.Type().Elem())).Elem() @@ -222,7 +222,7 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a generatedElement := GenerateAbiValue(generator, inputType.Elem) reflectedElement.Set(reflect.ValueOf(generatedElement)) } else { - mutatedElement, err := MutateAbiValue(generator, inputType.Elem, mutatedValues[i]) + mutatedElement, err := MutateAbiValue(generator, mutator, inputType.Elem, mutatedValues[i]) if err != nil { return nil, fmt.Errorf("could not mutate array input as the value generator encountered an error: %v", err) } @@ -237,7 +237,7 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a slice := reflectionutils.CopyReflectedType(reflect.ValueOf(value)) // Mutate our slice structure first - mutatedValues := generator.MutateArray(reflectionutils.GetReflectedArrayValues(slice), false) + mutatedValues := mutator.MutateArray(reflectionutils.GetReflectedArrayValues(slice), false) // Create a new slice of the appropriate size slice = reflect.MakeSlice(reflect.SliceOf(slice.Type().Elem()), len(mutatedValues), len(mutatedValues)) @@ -252,7 +252,7 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a generatedElement := GenerateAbiValue(generator, inputType.Elem) reflectedElement.Set(reflect.ValueOf(generatedElement)) } else { - mutatedElement, err := MutateAbiValue(generator, inputType.Elem, mutatedValues[i]) + mutatedElement, err := MutateAbiValue(generator, mutator, inputType.Elem, mutatedValues[i]) if err != nil { return nil, fmt.Errorf("could not mutate slice input as the value generator encountered an error: %v", err) } @@ -267,7 +267,7 @@ func MutateAbiValue(generator ValueGenerator, inputType *abi.Type, value any) (a for i := 0; i < len(inputType.TupleElems); i++ { field := tuple.Field(i) fieldValue := reflectionutils.GetField(field) - mutatedValue, err := MutateAbiValue(generator, inputType.TupleElems[i], fieldValue) + mutatedValue, err := MutateAbiValue(generator, mutator, inputType.TupleElems[i], fieldValue) if err != nil { return nil, fmt.Errorf("could not mutate struct/tuple input as the value generator encountered an error: %v", err) } diff --git a/fuzzing/valuegeneration/abi_values_test.go b/fuzzing/valuegeneration/abi_values_test.go index 8a926656..30c374ae 100644 --- a/fuzzing/valuegeneration/abi_values_test.go +++ b/fuzzing/valuegeneration/abi_values_test.go @@ -247,7 +247,7 @@ func TestABIRoundtripEncodingAllTypes(t *testing.T) { // re-encoded data matches the originally encoded data. func TestABIGenerationAndMutation(t *testing.T) { // Create a value generator - valueGenConfig := &MutatingValueGeneratorConfig{ + mutationalGeneratorConfig := &MutationalValueGeneratorConfig{ MinMutationRounds: 0, MaxMutationRounds: 1, GenerateRandomAddressBias: 0.5, @@ -273,7 +273,7 @@ func TestABIGenerationAndMutation(t *testing.T) { GenerateRandomStringMaxSize: 100, }, } - valueGenerator := NewMutatingValueGenerator(valueGenConfig, NewValueSet(), rand.New(rand.NewSource(time.Now().UnixNano()))) + mutationalGenerator := NewMutationalValueGenerator(mutationalGeneratorConfig, NewValueSet(), rand.New(rand.NewSource(time.Now().UnixNano()))) // Obtain our test ABI arguments args := getTestABIArguments() @@ -283,10 +283,10 @@ func TestABIGenerationAndMutation(t *testing.T) { // Test each argument round trip serialization with different generated values (iterate a number of times). for i := 0; i < 5; i++ { // Generate a value for this argument - value := GenerateAbiValue(valueGenerator, &arg.Type) + value := GenerateAbiValue(mutationalGenerator, &arg.Type) // Mutate and ensure no error occurred. - mutatedValue, err := MutateAbiValue(valueGenerator, &arg.Type, value) + mutatedValue, err := MutateAbiValue(mutationalGenerator, mutationalGenerator, &arg.Type, value) assert.NoError(t, err) // Verify the types of the value and mutated value are the same diff --git a/fuzzing/valuegeneration/generator.go b/fuzzing/valuegeneration/generator.go new file mode 100644 index 00000000..78518977 --- /dev/null +++ b/fuzzing/valuegeneration/generator.go @@ -0,0 +1,31 @@ +package valuegeneration + +import ( + "github.com/ethereum/go-ethereum/common" + "math/big" +) + +// ValueGenerator represents an interface for a provider used to generate function inputs and call arguments for use +// in fuzzing campaigns. +type ValueGenerator interface { + // GenerateAddress generates/selects an address to use when populating inputs. + GenerateAddress() common.Address + + // GenerateArrayOfLength generates/selects an array length to use when populating inputs. + GenerateArrayOfLength() int + + // GenerateBool generates/selects a bool to use when populating inputs. + GenerateBool() bool + + // GenerateBytes generates/selects a dynamic-sized byte array to use when populating inputs. + GenerateBytes() []byte + + // GenerateFixedBytes generates/selects a fixed-sized byte array to use when populating inputs. + GenerateFixedBytes(length int) []byte + + // GenerateString generates/selects a dynamic-sized string to use when populating inputs. + GenerateString() string + + // GenerateInteger generates/selects an integer to use when populating inputs. + GenerateInteger(signed bool, bitLength int) *big.Int +} diff --git a/fuzzing/valuegeneration/value_generator_mutating.go b/fuzzing/valuegeneration/generator_mutational.go similarity index 83% rename from fuzzing/valuegeneration/value_generator_mutating.go rename to fuzzing/valuegeneration/generator_mutational.go index baa80970..bf12ba8c 100644 --- a/fuzzing/valuegeneration/value_generator_mutating.go +++ b/fuzzing/valuegeneration/generator_mutational.go @@ -8,13 +8,13 @@ import ( "math/rand" ) -// MutatingValueGenerator is a provider used to generate function inputs and call arguments using mutation-based -// approaches against items within a base_value_set.ValueSet, such as AST literals. -type MutatingValueGenerator struct { - // config describes the configuration defining value generation parameters. - config *MutatingValueGeneratorConfig +// MutationalValueGenerator represents a ValueGenerator and ValueMutator for function inputs and call arguments. It +// leverages values from a ValueSet (e.g. AST literals) to generate new values or mutate existing ones. +type MutationalValueGenerator struct { + // config describes the configuration defining value generation and mutation parameters. + config *MutationalValueGeneratorConfig - // ValueSet contains a set of values which the ValueGenerator may use to aid in value generation and mutation + // valueSet contains a set of values which the ValueGenerator may use to aid in value generation and mutation // operations. valueSet *ValueSet @@ -22,8 +22,8 @@ type MutatingValueGenerator struct { *RandomValueGenerator } -// MutatingValueGeneratorConfig defines the operating parameters for a MutatingValueGenerator. -type MutatingValueGeneratorConfig struct { +// MutationalValueGeneratorConfig defines the operating parameters for a MutationalValueGenerator. +type MutationalValueGeneratorConfig struct { // MinMutationRounds describes the minimum amount of mutations which should occur when generating a value. // This parameter is used when generating a new value by mutating a value in the value set, or when mutating // an existing value. @@ -34,7 +34,7 @@ type MutatingValueGeneratorConfig struct { MaxMutationRounds int // GenerateRandomIntegerBias defines the probability in which an address generated by the value generator is - // entirely random, rather than selected from the ValueSet provided by MutatingValueGenerator.SetValueSet. Value + // entirely random, rather than selected from the MutationalValueGenerator's ValueSet. // range is [0.0, 1.0]. GenerateRandomAddressBias float32 // GenerateRandomIntegerBias defines the probability in which an integer generated by the value generator is @@ -82,10 +82,11 @@ type MutatingValueGeneratorConfig struct { *RandomValueGeneratorConfig } -// NewMutatingValueGenerator creates a new MutatingValueGenerator using a provided base_value_set.ValueSet to seed base-values for mutation. -func NewMutatingValueGenerator(config *MutatingValueGeneratorConfig, valueSet *ValueSet, randomProvider *rand.Rand) *MutatingValueGenerator { +// NewMutationalValueGenerator creates a new MutationalValueGenerator using a provided ValueSet to seed base-values for +// mutation. +func NewMutationalValueGenerator(config *MutationalValueGeneratorConfig, valueSet *ValueSet, randomProvider *rand.Rand) *MutationalValueGenerator { // Create and return our generator - generator := &MutatingValueGenerator{ + generator := &MutationalValueGenerator{ config: config, valueSet: valueSet, RandomValueGenerator: NewRandomValueGenerator(config.RandomValueGeneratorConfig, randomProvider), @@ -101,8 +102,8 @@ func NewMutatingValueGenerator(config *MutatingValueGeneratorConfig, valueSet *V // getMutationParams takes a length of inputs and returns an initial input index to start with as a base value, as well // as a random number of mutations which should be performed (within the mutation range specified by the -// ValueGeneratorConfig). -func (g *MutatingValueGenerator) getMutationParams(inputsLen int) (int, int) { +// MutationalValueGeneratorConfig). +func (g *MutationalValueGenerator) getMutationParams(inputsLen int) (int, int) { inputIdx := g.randomProvider.Intn(inputsLen) mutationCount := g.randomProvider.Intn(((g.config.MaxMutationRounds - g.config.MinMutationRounds) + 1) + g.config.MinMutationRounds) return inputIdx, mutationCount @@ -111,20 +112,20 @@ func (g *MutatingValueGenerator) getMutationParams(inputsLen int) (int, int) { // integerMutationMethods define methods which take a big integer and a set of inputs and // transform the integer with a random input and operation. This is used in a loop to create // mutated integer values. -var integerMutationMethods = []func(*MutatingValueGenerator, *big.Int, ...*big.Int) *big.Int{ - func(g *MutatingValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { +var integerMutationMethods = []func(*MutationalValueGenerator, *big.Int, ...*big.Int) *big.Int{ + func(g *MutationalValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { // Add a random input return big.NewInt(0).Add(x, inputs[g.randomProvider.Intn(len(inputs))]) }, - func(g *MutatingValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { + func(g *MutationalValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { // Subtract a random input return big.NewInt(0).Sub(x, inputs[g.randomProvider.Intn(len(inputs))]) }, - func(g *MutatingValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { + func(g *MutationalValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { // Multiply a random input return big.NewInt(0).Mul(x, inputs[g.randomProvider.Intn(len(inputs))]) }, - func(g *MutatingValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { + func(g *MutationalValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { // Divide a random input divisor := inputs[g.randomProvider.Intn(len(inputs))] if divisor.Cmp(big.NewInt(0)) == 0 { @@ -132,7 +133,7 @@ var integerMutationMethods = []func(*MutatingValueGenerator, *big.Int, ...*big.I } return big.NewInt(0).Div(x, divisor) }, - func(g *MutatingValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { + func(g *MutationalValueGenerator, x *big.Int, inputs ...*big.Int) *big.Int { // Modulo divide a random input divisor := inputs[g.randomProvider.Intn(len(inputs))] if divisor.Cmp(big.NewInt(0)) == 0 { @@ -144,7 +145,7 @@ var integerMutationMethods = []func(*MutatingValueGenerator, *big.Int, ...*big.I // mutateIntegerInternal takes an integer input and returns either a random new integer, or a mutated value based off the input. // If a nil input is provided, this method uses an existing base value set value as the starting point for mutation. -func (g *MutatingValueGenerator) mutateIntegerInternal(i *big.Int, signed bool, bitLength int) *big.Int { +func (g *MutationalValueGenerator) mutateIntegerInternal(i *big.Int, signed bool, bitLength int) *big.Int { // If our bias directs us to, use the random generator instead randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.GenerateRandomIntegerBias { @@ -188,9 +189,9 @@ func (g *MutatingValueGenerator) mutateIntegerInternal(i *big.Int, signed bool, // bytesMutationMethods define methods which take an initial bytes and a set of inputs to transform the input. The // transformed input is returned. This is used in a loop to mutate byte slices. -var bytesMutationMethods = []func(*MutatingValueGenerator, []byte, ...[]byte) []byte{ +var bytesMutationMethods = []func(*MutationalValueGenerator, []byte, ...[]byte) []byte{ // Replace a random index with a random byte - func(g *MutatingValueGenerator, b []byte, inputs ...[]byte) []byte { + func(g *MutationalValueGenerator, b []byte, inputs ...[]byte) []byte { // Generate a random byte and replace an existing byte in our array with it. If our array has no bytes, we add // it. randomByteValue := byte(g.randomProvider.Intn(256)) @@ -202,7 +203,7 @@ var bytesMutationMethods = []func(*MutatingValueGenerator, []byte, ...[]byte) [] return b }, // Flip a random bit in it. - func(g *MutatingValueGenerator, b []byte, inputs ...[]byte) []byte { + func(g *MutationalValueGenerator, b []byte, inputs ...[]byte) []byte { // If we have bytes in our array, flip a random bit in a random byte. Otherwise, we add a random byte. if len(b) > 0 { i := g.randomProvider.Intn(len(b)) @@ -213,7 +214,7 @@ var bytesMutationMethods = []func(*MutatingValueGenerator, []byte, ...[]byte) [] return b }, // Add a random byte at a random position - func(g *MutatingValueGenerator, b []byte, inputs ...[]byte) []byte { + func(g *MutationalValueGenerator, b []byte, inputs ...[]byte) []byte { // Generate a random byte to insert by := byte(g.randomProvider.Intn(256)) @@ -232,7 +233,7 @@ var bytesMutationMethods = []func(*MutatingValueGenerator, []byte, ...[]byte) [] } }, // Remove a random byte - func(g *MutatingValueGenerator, b []byte, inputs ...[]byte) []byte { + func(g *MutationalValueGenerator, b []byte, inputs ...[]byte) []byte { // If we have no bytes to remove, do nothing. if len(b) == 0 { return b @@ -246,7 +247,7 @@ var bytesMutationMethods = []func(*MutatingValueGenerator, []byte, ...[]byte) [] // mutateBytesInternal takes a byte array and returns either a random new byte array, or a mutated value based off the // input. // If a nil input is provided, this method uses an existing base value set value as the starting point for mutation. -func (g *MutatingValueGenerator) mutateBytesInternal(b []byte) []byte { +func (g *MutationalValueGenerator) mutateBytesInternal(b []byte) []byte { // If we have no inputs or our bias directs us to, use the random generator instead inputs := g.valueSet.Bytes() randomGeneratorDecision := g.randomProvider.Float32() @@ -273,9 +274,9 @@ func (g *MutatingValueGenerator) mutateBytesInternal(b []byte) []byte { // stringMutationMethods define methods which take an initial string and a set of inputs to transform the input. The // transformed input is returned. This is used in a loop to mutate strings. -var stringMutationMethods = []func(*MutatingValueGenerator, string, ...string) string{ +var stringMutationMethods = []func(*MutationalValueGenerator, string, ...string) string{ // Replace a random index with a random character - func(g *MutatingValueGenerator, s string, inputs ...string) string { + func(g *MutationalValueGenerator, s string, inputs ...string) string { // Generate a random rune randomRune := rune(32 + g.randomProvider.Intn(95)) @@ -290,7 +291,7 @@ var stringMutationMethods = []func(*MutatingValueGenerator, string, ...string) s return string(r) }, // Flip a random bit - func(g *MutatingValueGenerator, s string, inputs ...string) string { + func(g *MutationalValueGenerator, s string, inputs ...string) string { // If the string is empty, simply return a new one with a randomly added character. r := []rune(s) if len(r) == 0 { @@ -303,7 +304,7 @@ var stringMutationMethods = []func(*MutatingValueGenerator, string, ...string) s return string(r) }, // Insert a random character at a random position - func(g *MutatingValueGenerator, s string, inputs ...string) string { + func(g *MutationalValueGenerator, s string, inputs ...string) string { // Create a random character. c := string(rune(32 + g.randomProvider.Intn(95))) @@ -317,7 +318,7 @@ var stringMutationMethods = []func(*MutatingValueGenerator, string, ...string) s return s[:i] + c + s[i+1:] }, // Remove a random character - func(g *MutatingValueGenerator, s string, inputs ...string) string { + func(g *MutationalValueGenerator, s string, inputs ...string) string { // If we have no characters to remove, do nothing if len(s) == 0 { return s @@ -331,7 +332,7 @@ var stringMutationMethods = []func(*MutatingValueGenerator, string, ...string) s // mutateStringInternal takes a string and returns either a random new string, or a mutated value based off the input. // If a nil input is provided, this method uses an existing base value set value as the starting point for mutation. -func (g *MutatingValueGenerator) mutateStringInternal(s *string) string { +func (g *MutationalValueGenerator) mutateStringInternal(s *string) string { // If we have no inputs or our bias directs us to, use the random generator instead inputs := g.valueSet.Strings() randomGeneratorDecision := g.randomProvider.Float32() @@ -357,7 +358,7 @@ func (g *MutatingValueGenerator) mutateStringInternal(s *string) string { } // GenerateAddress obtains an existing address from its underlying value set or generates a random one. -func (g *MutatingValueGenerator) GenerateAddress() common.Address { +func (g *MutationalValueGenerator) GenerateAddress() common.Address { // If our bias directs us to, use the random generator instead randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.GenerateRandomAddressBias { @@ -376,7 +377,7 @@ func (g *MutatingValueGenerator) GenerateAddress() common.Address { } // MutateAddress takes an address input and sometimes returns a mutated value based off the input. -func (g *MutatingValueGenerator) MutateAddress(addr common.Address) common.Address { +func (g *MutationalValueGenerator) MutateAddress(addr common.Address) common.Address { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateAddressProbability { @@ -388,7 +389,7 @@ func (g *MutatingValueGenerator) MutateAddress(addr common.Address) common.Addre // MutateArray takes a dynamic or fixed sized array as input, and returns a mutated value based off of the input. // Returns the mutated value. If any element of the returned array is nil, the value generator will be called upon // to generate it new. -func (g *MutatingValueGenerator) MutateArray(value []any, fixedLength bool) []any { +func (g *MutationalValueGenerator) MutateArray(value []any, fixedLength bool) []any { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateArrayStructureProbability { @@ -403,7 +404,7 @@ func (g *MutatingValueGenerator) MutateArray(value []any, fixedLength bool) []an } // MutateBool takes a boolean input and returns a mutated value based off the input. -func (g *MutatingValueGenerator) MutateBool(bl bool) bool { +func (g *MutationalValueGenerator) MutateBool(bl bool) bool { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateBoolProbability { @@ -413,12 +414,12 @@ func (g *MutatingValueGenerator) MutateBool(bl bool) bool { } // GenerateBytes generates bytes and returns them. -func (g *MutatingValueGenerator) GenerateBytes() []byte { +func (g *MutationalValueGenerator) GenerateBytes() []byte { return g.mutateBytesInternal(nil) } // MutateBytes takes a dynamic-sized byte array input and returns a mutated value based off the input. -func (g *MutatingValueGenerator) MutateBytes(b []byte) []byte { +func (g *MutationalValueGenerator) MutateBytes(b []byte) []byte { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateBytesProbability { @@ -434,7 +435,7 @@ func (g *MutatingValueGenerator) MutateBytes(b []byte) []byte { } // MutateFixedBytes takes a fixed-sized byte array input and returns a mutated value based off the input. -func (g *MutatingValueGenerator) MutateFixedBytes(b []byte) []byte { +func (g *MutationalValueGenerator) MutateFixedBytes(b []byte) []byte { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateFixedBytesProbability { @@ -444,12 +445,12 @@ func (g *MutatingValueGenerator) MutateFixedBytes(b []byte) []byte { } // GenerateString generates strings and returns them. -func (g *MutatingValueGenerator) GenerateString() string { +func (g *MutationalValueGenerator) GenerateString() string { return g.mutateStringInternal(nil) } // MutateString takes a string input and returns a mutated value based off the input. -func (g *MutatingValueGenerator) MutateString(s string) string { +func (g *MutationalValueGenerator) MutateString(s string) string { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateStringProbability { @@ -465,14 +466,14 @@ func (g *MutatingValueGenerator) MutateString(s string) string { } // GenerateInteger generates an integer of the provided properties and returns a big.Int representing it. -func (g *MutatingValueGenerator) GenerateInteger(signed bool, bitLength int) *big.Int { +func (g *MutationalValueGenerator) GenerateInteger(signed bool, bitLength int) *big.Int { // Call our internal mutation method with no starting input. This will generate a new input. return g.mutateIntegerInternal(nil, signed, bitLength) } // MutateInteger takes an integer input and applies optional mutations to the provided value. // Returns an optionally mutated copy of the input. -func (g *MutatingValueGenerator) MutateInteger(i *big.Int, signed bool, bitLength int) *big.Int { +func (g *MutationalValueGenerator) MutateInteger(i *big.Int, signed bool, bitLength int) *big.Int { // Determine whether to perform mutations against this input or just return it as-is. randomGeneratorDecision := g.randomProvider.Float32() if randomGeneratorDecision < g.config.MutateIntegerProbability { diff --git a/fuzzing/valuegeneration/value_generator_random.go b/fuzzing/valuegeneration/generator_random.go similarity index 90% rename from fuzzing/valuegeneration/value_generator_random.go rename to fuzzing/valuegeneration/generator_random.go index fe77cd88..eb9de140 100644 --- a/fuzzing/valuegeneration/value_generator_random.go +++ b/fuzzing/valuegeneration/generator_random.go @@ -7,9 +7,8 @@ import ( "math/rand" ) -// RandomValueGenerator represents an interface for a provider used to generate transaction fields and call arguments -// using a random provider. As such it may not be accurate in many test results with tightly-bound pre-conditions. -// This provider does not mutate existing values and will leave them unaltered. +// RandomValueGenerator represents a ValueGenerator used to generate transaction fields and call arguments with values +// provided by a random number generator. type RandomValueGenerator struct { // config describes the configuration defining value generation parameters. config *RandomValueGeneratorConfig @@ -34,7 +33,7 @@ type RandomValueGeneratorConfig struct { GenerateRandomStringMaxSize int } -// NewRandomValueGenerator creates a new RandomValueGenerator with a new random provider. +// NewRandomValueGenerator creates a new RandomValueGenerator. func NewRandomValueGenerator(config *RandomValueGeneratorConfig, randomProvider *rand.Rand) *RandomValueGenerator { // Create and return our generator generator := &RandomValueGenerator{ @@ -44,11 +43,6 @@ func NewRandomValueGenerator(config *RandomValueGeneratorConfig, randomProvider return generator } -// RandomProvider returns the internal random provider used for value generation. -func (g *RandomValueGenerator) RandomProvider() *rand.Rand { - return g.randomProvider -} - // GenerateAddress generates a random address to use when populating inputs. func (g *RandomValueGenerator) GenerateAddress() common.Address { // Generate random bytes of the address length, then convert it to an address. diff --git a/fuzzing/valuegeneration/value_generator_interface.go b/fuzzing/valuegeneration/mutator.go similarity index 51% rename from fuzzing/valuegeneration/value_generator_interface.go rename to fuzzing/valuegeneration/mutator.go index c230ae22..a045bc02 100644 --- a/fuzzing/valuegeneration/value_generator_interface.go +++ b/fuzzing/valuegeneration/mutator.go @@ -3,49 +3,31 @@ package valuegeneration import ( "github.com/ethereum/go-ethereum/common" "math/big" - "math/rand" ) -// ValueGenerator represents an interface for a provider used to generate function inputs and call arguments for use +// ValueMutator represents an interface for a provider used to mutate function inputs and call arguments for use // in fuzzing campaigns. -type ValueGenerator interface { - // RandomProvider returns the internal random provider used for value generation. - RandomProvider() *rand.Rand - - // GenerateAddress generates/selects an address to use when populating inputs. - GenerateAddress() common.Address +type ValueMutator interface { // MutateAddress takes an address input and returns a mutated value based off the input. MutateAddress(addr common.Address) common.Address - // GenerateArrayOfLength generates/selects an array length to use when populating inputs. - GenerateArrayOfLength() int // MutateArray takes a dynamic or fixed sized array as input, and returns a mutated value based off of the input. // Returns the mutated value. If any element of the returned array is nil, the value generator will be called upon - // to generate it new. + // to generate a new value in its place. MutateArray(value []any, fixedLength bool) []any - // GenerateBool generates/selects a bool to use when populating inputs. - GenerateBool() bool // MutateBool takes a boolean input and returns a mutated value based off the input. MutateBool(bl bool) bool - // GenerateBytes generates/selects a dynamic-sized byte array to use when populating inputs. - GenerateBytes() []byte // MutateBytes takes a dynamic-sized byte array input and returns a mutated value based off the input. MutateBytes(b []byte) []byte - // GenerateFixedBytes generates/selects a fixed-sized byte array to use when populating inputs. - GenerateFixedBytes(length int) []byte // MutateFixedBytes takes a fixed-sized byte array input and returns a mutated value based off the input. MutateFixedBytes(b []byte) []byte - // GenerateString generates/selects a dynamic-sized string to use when populating inputs. - GenerateString() string // MutateString takes a string input and returns a mutated value based off the input. MutateString(s string) string - // GenerateInteger generates/selects an integer to use when populating inputs. - GenerateInteger(signed bool, bitLength int) *big.Int // MutateInteger takes an integer input and returns a mutated value based off the input. MutateInteger(i *big.Int, signed bool, bitLength int) *big.Int } diff --git a/fuzzing/valuegeneration/mutator_shrinking.go b/fuzzing/valuegeneration/mutator_shrinking.go new file mode 100644 index 00000000..20b7ff49 --- /dev/null +++ b/fuzzing/valuegeneration/mutator_shrinking.go @@ -0,0 +1,195 @@ +package valuegeneration + +import ( + "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/common" + "math/big" + "math/rand" +) + +// ShrinkingValueMutator represents a ValueMutator used to shrink function inputs and call arguments. +type ShrinkingValueMutator struct { + // config describes the configuration defining value mutation parameters. + config *ShrinkingValueMutatorConfig + + // valueSet contains a set of values which the ValueGenerator may use to aid in value generation and mutation + // operations. + valueSet *ValueSet + + // randomProvider offers a source of random data. + randomProvider *rand.Rand +} + +// ShrinkingValueMutatorConfig defines the operating parameters for a ShrinkingValueMutator. +type ShrinkingValueMutatorConfig struct { + // ShrinkValueProbability is the probability that any shrinkable value will be shrunk/mutated when a mutation + // method is invoked. + ShrinkValueProbability float32 +} + +// NewShrinkingValueMutator creates a new ShrinkingValueMutator using a ValueSet to seed base-values for mutation. +func NewShrinkingValueMutator(config *ShrinkingValueMutatorConfig, valueSet *ValueSet, randomProvider *rand.Rand) *ShrinkingValueMutator { + // Create and return our generator + generator := &ShrinkingValueMutator{ + config: config, + valueSet: valueSet, + randomProvider: randomProvider, + } + + // Ensure some initial values this mutator will depend on for basic mutations to the set. + generator.valueSet.AddInteger(big.NewInt(0)) + generator.valueSet.AddInteger(big.NewInt(1)) + generator.valueSet.AddInteger(big.NewInt(2)) + return generator +} + +// MutateAddress takes an address input and sometimes returns a mutated value based off the input. +// This type is not mutated by the ShrinkingValueMutator. +func (g *ShrinkingValueMutator) MutateAddress(addr common.Address) common.Address { + return addr +} + +// MutateArray takes a dynamic or fixed sized array as input, and returns a mutated value based off of the input. +// Returns the mutated value. If any element of the returned array is nil, the value generator will be called upon +// to generate it new. +// This type is not mutated by the ShrinkingValueMutator. +func (g *ShrinkingValueMutator) MutateArray(value []any, fixedLength bool) []any { + return value +} + +// MutateBool takes a boolean input and returns a mutated value based off the input. +// This type is not mutated by the ShrinkingValueMutator. +func (g *ShrinkingValueMutator) MutateBool(bl bool) bool { + return bl +} + +// MutateFixedBytes takes a fixed-sized byte array input and returns a mutated value based off the input. +// This type is not mutated by the ShrinkingValueMutator. +func (g *ShrinkingValueMutator) MutateFixedBytes(b []byte) []byte { + return b +} + +// bytesShrinkingMethods define methods which take an initial bytes and a set of inputs to transform the input. The +// transformed input is returned. +var bytesShrinkingMethods = []func(*ShrinkingValueMutator, []byte) []byte{ + // Replace a random index with a zero byte + func(g *ShrinkingValueMutator, b []byte) []byte { + if len(b) > 0 { + b[g.randomProvider.Intn(len(b))] = 0 + } + return b + }, + // Remove a random byte + func(g *ShrinkingValueMutator, b []byte) []byte { + // If we have no bytes to remove, do nothing. + if len(b) == 0 { + return b + } + + i := g.randomProvider.Intn(len(b)) + return append(b[:i], b[i+1:]...) + }, +} + +// MutateBytes takes a dynamic-sized byte array input and returns a mutated value based off the input. +func (g *ShrinkingValueMutator) MutateBytes(b []byte) []byte { + randomGeneratorDecision := g.randomProvider.Float32() + if randomGeneratorDecision < g.config.ShrinkValueProbability { + // Mutate the data for our desired number of rounds + input := bytesShrinkingMethods[g.randomProvider.Intn(len(bytesShrinkingMethods))](g, b) + return input + } + return b +} + +// integerShrinkingMethods define methods which take a big integer and a set of inputs and +// transform the integer with a random input and operation. +var integerShrinkingMethods = []func(*ShrinkingValueMutator, *big.Int, ...*big.Int) *big.Int{ + func(g *ShrinkingValueMutator, x *big.Int, inputs ...*big.Int) *big.Int { + // If our base value is positive, we subtract from it. If it's positive, we add to it. + // If it's zero, we leave it unchanged. + r := big.NewInt(0) + if x.Cmp(r) > 0 { + r = r.Sub(x, inputs[g.randomProvider.Intn(len(inputs))]) + } else if x.Cmp(r) < 0 { + r = r.Add(x, inputs[g.randomProvider.Intn(len(inputs))]) + } + return r + + }, + func(g *ShrinkingValueMutator, x *big.Int, inputs ...*big.Int) *big.Int { + // Divide by two + return big.NewInt(0).Div(x, big.NewInt(2)) + }, +} + +// MutateInteger takes an integer input and applies optional mutations to the provided value. +// Returns an optionally mutated copy of the input. +func (g *ShrinkingValueMutator) MutateInteger(i *big.Int, signed bool, bitLength int) *big.Int { + randomGeneratorDecision := g.randomProvider.Float32() + if randomGeneratorDecision < g.config.ShrinkValueProbability { + // Calculate our integer bounds + min, max := utils.GetIntegerConstraints(signed, bitLength) + + // Obtain our inputs. We also add our min/max values for this range to the list of inputs. + // Note: We exclude min being added if we're requesting an unsigned integer, as zero is already + // in our set, and we don't want duplicates. + var inputs []*big.Int + inputs = append(inputs, g.valueSet.Integers()...) + if signed { + inputs = append(inputs, min, max) + } else { + inputs = append(inputs, max) + } + + // Set the input and ensure it is constrained to the value boundaries + input := new(big.Int).Set(i) + input = utils.ConstrainIntegerToBounds(input, min, max) + + // Shrink input + input = integerShrinkingMethods[g.randomProvider.Intn(len(integerShrinkingMethods))](g, input, inputs...) + + // Correct value boundaries (underflow/overflow) + input = utils.ConstrainIntegerToBounds(input, min, max) + return input + } + return i +} + +// stringShrinkingMethods define methods which take an initial string and a set of inputs to transform the input. The +// transformed input is returned. +var stringShrinkingMethods = []func(*ShrinkingValueMutator, string) string{ + // Replace a random index with a NULL char + func(g *ShrinkingValueMutator, s string) string { + // If the string is empty, we can simply return a new string with just the rune in it. + r := []rune(s) + if len(r) == 0 { + return string(r) + } + + // Otherwise, we replace a rune in it and return it. + r[g.randomProvider.Intn(len(r))] = 0 + return string(r) + }, + // Remove a random character + func(g *ShrinkingValueMutator, s string) string { + // If we have no characters to remove, do nothing + if len(s) == 0 { + return s + } + + // Otherwise, remove a random character. + i := g.randomProvider.Intn(len(s)) + return s[:i] + s[i+1:] + }, +} + +// MutateString takes a string input and returns a mutated value based off the input. +func (g *ShrinkingValueMutator) MutateString(s string) string { + randomGeneratorDecision := g.randomProvider.Float32() + if randomGeneratorDecision < g.config.ShrinkValueProbability { + input := stringShrinkingMethods[g.randomProvider.Intn(len(stringShrinkingMethods))](g, s) + return input + } + return s +} diff --git a/utils/integer_utils.go b/utils/integer_utils.go index 8b097901..ef1cf5b8 100644 --- a/utils/integer_utils.go +++ b/utils/integer_utils.go @@ -1,8 +1,9 @@ package utils import ( - "golang.org/x/exp/constraints" "math/big" + + "golang.org/x/exp/constraints" ) // ConstrainIntegerToBounds takes a provided big integer and minimum/maximum bounds (inclusive) and ensures From 25b5de9f7b5dd53eb2321821a2cd5e42d2662580 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Fri, 14 Jul 2023 12:06:41 -0400 Subject: [PATCH 017/109] Custom logger for unstructured and structured logging (#171) This PR introduces colorized logging for console and future support for structured logging --------- Co-authored-by: David Pokora --- chain/cheat_code_contract.go | 6 +- cmd/completion.go | 35 +- cmd/fuzz.go | 89 +++-- cmd/init.go | 96 +++-- cmd/root.go | 6 + compilation/supported_platforms.go | 4 +- fuzzing/calls/call_message.go | 4 +- fuzzing/calls/call_sequence.go | 39 +- fuzzing/config/config.go | 40 +- fuzzing/config/config_defaults.go | 5 + fuzzing/corpus/corpus.go | 8 +- fuzzing/coverage/coverage_tracer.go | 12 +- fuzzing/executiontracer/execution_trace.go | 134 ++++--- fuzzing/fuzzer.go | 102 +++-- fuzzing/fuzzer_test.go | 6 +- fuzzing/test_case.go | 7 +- fuzzing/test_case_assertion.go | 29 +- fuzzing/test_case_optimization.go | 42 ++- fuzzing/test_case_optimization_provider.go | 5 - fuzzing/test_case_property.go | 35 +- fuzzing/valuegeneration/abi_values.go | 6 +- go.mod | 3 + go.sum | 11 + logging/colors/color_funcs.go | 107 ++++++ logging/colors/colorize_unix.go | 15 + logging/colors/colorize_windows.go | 66 ++++ logging/colors/constants.go | 34 ++ logging/colors/init.go | 7 + logging/log_buffer.go | 33 ++ logging/logger.go | 347 ++++++++++++++++++ main.go | 4 - utils/fs_utils.go | 27 ++ utils/randomutils/fork_random.go | 3 +- utils/reflectionutils/reflected_type_utils.go | 13 +- 34 files changed, 1122 insertions(+), 258 deletions(-) create mode 100644 logging/colors/color_funcs.go create mode 100644 logging/colors/colorize_unix.go create mode 100644 logging/colors/colorize_windows.go create mode 100644 logging/colors/constants.go create mode 100644 logging/colors/init.go create mode 100644 logging/log_buffer.go create mode 100644 logging/logger.go diff --git a/chain/cheat_code_contract.go b/chain/cheat_code_contract.go index 43b8fa2f..0b644cfc 100644 --- a/chain/cheat_code_contract.go +++ b/chain/cheat_code_contract.go @@ -2,6 +2,8 @@ package chain import ( "encoding/binary" + "fmt" + "github.com/crytic/medusa/logging" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" @@ -100,12 +102,12 @@ func (c *CheatCodeContract) Abi() *abi.ABI { func (c *CheatCodeContract) addMethod(name string, inputs abi.Arguments, outputs abi.Arguments, handler cheatCodeMethodHandler) { // Verify a method name was provided if name == "" { - panic("could not add method to precompiled cheatcode contract, empty method name provided") + logging.GlobalLogger.Panic("Failed to add method to precompile cheatcode contract", fmt.Errorf("empty method name provided")) } // Verify a method handler was provided if handler == nil { - panic("could not add method to precompiled cheatcode contract, nil method handler provided") + logging.GlobalLogger.Panic("Failed to add method to precompile cheatcode contract", fmt.Errorf("nil method handler provided")) } // Set the method information in our method lookup diff --git a/cmd/completion.go b/cmd/completion.go index 57e45539..a5e1f66a 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -47,11 +47,13 @@ var supportedShells = []string{"bash", "zsh", "powershell"} // completionCmd represents the completion command var completionCmd = &cobra.Command{ - Use: "completion ", - Short: "generate the autocompletion script for medusa for the specific shell", - Long: generalComDesc, - Args: cmdValidateCompletionArgs, - RunE: cmdRunCompletion, + Use: "completion ", + Short: "Generate the autocompletion script for medusa for the specific shell", + Long: generalComDesc, + Args: cmdValidateCompletionArgs, + RunE: cmdRunCompletion, + SilenceUsage: true, + SilenceErrors: true, } func init() { @@ -62,12 +64,16 @@ func init() { func cmdValidateCompletionArgs(cmd *cobra.Command, args []string) error { // Make sure we have exactly 1 argument if err := cobra.ExactArgs(1)(cmd, args); err != nil { - return fmt.Errorf("completion requires only 1 shell argument (options: %s)", strings.Join(supportedShells, ", ")) + err = fmt.Errorf("completion requires only 1 shell argument (options: %s)", strings.Join(supportedShells, ", ")) + cmdLogger.Error("Failed to validate args for completion command", err) + return err } // Make sure that the shell is a supported type if contains := slices.Contains(supportedShells, args[0]); !contains { - return fmt.Errorf("%s is not a supported shell", args[0]) + err := fmt.Errorf("%s is not a supported shell", args[0]) + cmdLogger.Error("Failed to validate args for completion command", err) + return err } return nil @@ -76,15 +82,22 @@ func cmdValidateCompletionArgs(cmd *cobra.Command, args []string) error { // cmdRunCompletion executes the completion CLI command func cmdRunCompletion(cmd *cobra.Command, args []string) error { // NOTE: Please be aware that if the supported shells changes, then this switch statement must also change + var err error switch args[0] { case "bash": - return cmd.Root().GenBashCompletionV2(os.Stdout, true) + err = cmd.Root().GenBashCompletionV2(os.Stdout, true) case "zsh": - return cmd.Root().GenZshCompletion(os.Stdout) + err = cmd.Root().GenZshCompletion(os.Stdout) case "powershell": - return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) default: // We are throwing a panic here because our validation function should have handled this and something is wrong. - panic(fmt.Errorf("%s is not a supported shell type", args[0])) + cmdLogger.Panic("Failed to run the completion command", fmt.Errorf("%s is not a supported shell type", args[0])) } + + // Log an error if we encountered one + if err != nil { + cmdLogger.Error("Failed to run the completion command", err) + } + return err } diff --git a/cmd/fuzz.go b/cmd/fuzz.go index c77ac5e0..3fc22fd3 100644 --- a/cmd/fuzz.go +++ b/cmd/fuzz.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/crytic/medusa/logging/colors" "os" "os/signal" "path/filepath" @@ -14,53 +15,59 @@ import ( // fuzzCmd represents the command provider for fuzzing var fuzzCmd = &cobra.Command{ - Use: "fuzz", - Short: "Starts a fuzzing campaign", - Long: `Starts a fuzzing campaign`, - Args: cmdValidateFuzzArgs, - RunE: cmdRunFuzz, - // Run dynamic completion of nouns - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // Gather a list of flags that are available to be used in the current command but have not been used yet - var unusedFlags []string - - // Examine all the flags, and add any flags that have not been set in the current command line - // to a list of unused flags - cmd.Flags().VisitAll(func(flag *pflag.Flag) { - if !flag.Changed { - // When adding a flag to a command, include the "--" prefix to indicate that it is a flag - // and not a positional argument. Additionally, when the user presses the TAB key twice after typing - // a flag name, the "--" prefix will appear again, indicating that more flags are available and that - // none of the arguments are positional. - unusedFlags = append(unusedFlags, "--"+flag.Name) - } - }) - // Provide a list of flags that can be used in the current command (but have not been used yet) - // for autocompletion suggestions - return unusedFlags, cobra.ShellCompDirectiveNoFileComp - }, -} - -// cmdValidateFuzzArgs makes sure that there are no positional arguments provided to the fuzz command -func cmdValidateFuzzArgs(cmd *cobra.Command, args []string) error { - // Make sure we have no positional args - if err := cobra.NoArgs(cmd, args); err != nil { - return fmt.Errorf("fuzz does not accept any positional arguments, only flags and their associated values") - } - return nil + Use: "fuzz", + Short: "Starts a fuzzing campaign", + Long: `Starts a fuzzing campaign`, + Args: cmdValidateFuzzArgs, + ValidArgsFunction: cmdValidFuzzArgs, + RunE: cmdRunFuzz, + SilenceUsage: true, + SilenceErrors: true, } func init() { // Add all the flags allowed for the fuzz command err := addFuzzFlags() if err != nil { - panic(err) + cmdLogger.Panic("Failed to initialize the fuzz command", err) } // Add the fuzz command and its associated flags to the root command rootCmd.AddCommand(fuzzCmd) } +// cmdValidFuzzArgs will return which flags and sub-commands are valid for dynamic completion for the fuzz command +func cmdValidFuzzArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Gather a list of flags that are available to be used in the current command but have not been used yet + var unusedFlags []string + + // Examine all the flags, and add any flags that have not been set in the current command line + // to a list of unused flags + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if !flag.Changed { + // When adding a flag to a command, include the "--" prefix to indicate that it is a flag + // and not a positional argument. Additionally, when the user presses the TAB key twice after typing + // a flag name, the "--" prefix will appear again, indicating that more flags are available and that + // none of the arguments are positional. + unusedFlags = append(unusedFlags, "--"+flag.Name) + } + }) + // Provide a list of flags that can be used in the current command (but have not been used yet) + // for autocompletion suggestions + return unusedFlags, cobra.ShellCompDirectiveNoFileComp +} + +// cmdValidateFuzzArgs makes sure that there are no positional arguments provided to the fuzz command +func cmdValidateFuzzArgs(cmd *cobra.Command, args []string) error { + // Make sure we have no positional args + if err := cobra.NoArgs(cmd, args); err != nil { + err = fmt.Errorf("fuzz does not accept any positional arguments, only flags and their associated values") + cmdLogger.Error("Failed to validate args to the fuzz command", err) + return err + } + return nil +} + // cmdRunFuzz executes the CLI fuzz command and navigates through the following possibilities: // #1: We will search for either a custom config file (via --config) or the default (medusa.json). // If we find it, read it. If we can't read it, throw an error. @@ -73,6 +80,7 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { configFlagUsed := cmd.Flags().Changed("config") configPath, err := cmd.Flags().GetString("config") if err != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return err } @@ -80,6 +88,7 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { if !configFlagUsed { workingDirectory, err := os.Getwd() if err != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return err } configPath = filepath.Join(workingDirectory, DefaultProjectConfigFilename) @@ -91,24 +100,28 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { // Possibility #1: File was found if existenceError == nil { // Try to read the configuration file and throw an error if something goes wrong + cmdLogger.Info("Reading the configuration file at: ", colors.Bold, configPath, colors.Reset) projectConfig, err = config.ReadProjectConfigFromFile(configPath) if err != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return err } } // Possibility #2: If the --config flag was used, and we couldn't find the file, we'll throw an error if configFlagUsed && existenceError != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return existenceError } // Possibility #3: --config flag was not used and medusa.json was not found, so use the default project config if !configFlagUsed && existenceError != nil { - fmt.Printf("unable to find the config file at %v. will use the default project configuration for the "+ - "%v compilation platform instead\n", configPath, DefaultCompilationPlatform) + cmdLogger.Warn(fmt.Sprintf("Unable to find the config file at %v, will use the default project configuration for the "+ + "%v compilation platform instead", configPath, DefaultCompilationPlatform)) projectConfig, err = config.GetDefaultProjectConfig(DefaultCompilationPlatform) if err != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return err } } @@ -116,6 +129,7 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { // Update the project configuration given whatever flags were set using the CLI err = updateProjectConfigWithFuzzFlags(cmd, projectConfig) if err != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return err } @@ -125,6 +139,7 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { // be in the config directory when running this. err = os.Chdir(filepath.Dir(configPath)) if err != nil { + cmdLogger.Error("Failed to run the fuzz command", err) return err } diff --git a/cmd/init.go b/cmd/init.go index 79087667..4f845517 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/crytic/medusa/logging/colors" "os" "path/filepath" "strings" @@ -18,66 +19,74 @@ var supportedPlatforms = compilation.GetSupportedCompilationPlatforms() // initCmd represents the command provider for init var initCmd = &cobra.Command{ - Use: "init [platform]", - Short: "Initializes a project configuration", - Long: `Initializes a project configuration`, - Args: cmdValidateInitArgs, - RunE: cmdRunInit, - // Run dynamic completion of nouns - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // Gather a list of flags that are available to be used in the current command but have not been used yet - var unusedFlags []string - - // Examine all the flags, and add any flags that have not been set in the current command line - // to a list of unused flags - flagUsed := false - cmd.Flags().VisitAll(func(flag *pflag.Flag) { - if !flag.Changed { - // When adding a flag to a command, include the "--" prefix to indicate that it is a flag - // and not a positional argument. Additionally, when the user presses the TAB key twice after typing - // a flag name, the "--" prefix will appear again, indicating that more flags are available and that - // none of the arguments are positional. - unusedFlags = append(unusedFlags, "--"+flag.Name) - } else { - // If any flag has been used, set flag used to true. This will be used later in the function. - flagUsed = true - } - }) - - // If a default platform is not specified, add a list of available platforms to the list of unused flags. - // If any flag is used, then we can assume that the default platform is used so we don't need to add supported platforms - if len(args) == 0 && !flagUsed { - unusedFlags = append(unusedFlags, supportedPlatforms...) - } - - // Provide a list of flags that can be used in the current command (but have not been used yet) - // for autocompletion suggestions - return unusedFlags, cobra.ShellCompDirectiveNoFileComp - }, + Use: "init [platform]", + Short: "Initializes a project configuration", + Long: `Initializes a project configuration`, + Args: cmdValidateInitArgs, + ValidArgsFunction: cmdValidInitArgs, + RunE: cmdRunInit, + SilenceUsage: true, + SilenceErrors: true, } func init() { // Add flags to init command err := addInitFlags() if err != nil { - panic(err) + cmdLogger.Panic("Failed to initialize the init command", err) } // Add the init command and its associated flags to the root command rootCmd.AddCommand(initCmd) } +// cmdValidInitArgs will return which flags and sub-commands are valid for dynamic completion for the init command +func cmdValidInitArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Gather a list of flags that are available to be used in the current command but have not been used yet + var unusedFlags []string + + // Examine all the flags, and add any flags that have not been set in the current command line + // to a list of unused flags + flagUsed := false + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if !flag.Changed { + // When adding a flag to a command, include the "--" prefix to indicate that it is a flag + // and not a positional argument. Additionally, when the user presses the TAB key twice after typing + // a flag name, the "--" prefix will appear again, indicating that more flags are available and that + // none of the arguments are positional. + unusedFlags = append(unusedFlags, "--"+flag.Name) + } else { + // If any flag has been used, set flag used to true. This will be used later in the function. + flagUsed = true + } + }) + + // If a default platform is not specified, add a list of available platforms to the list of unused flags. + // If any flag is used, then we can assume that the default platform is used so we don't need to add supported platforms + if len(args) == 0 && !flagUsed { + unusedFlags = append(unusedFlags, supportedPlatforms...) + } + + // Provide a list of flags that can be used in the current command (but have not been used yet) + // for autocompletion suggestions + return unusedFlags, cobra.ShellCompDirectiveNoFileComp +} + // cmdValidateInitArgs validates CLI arguments func cmdValidateInitArgs(cmd *cobra.Command, args []string) error { // Make sure we have no more than 1 arg if err := cobra.RangeArgs(0, 1)(cmd, args); err != nil { - return fmt.Errorf("init accepts at most 1 platform argument (options: %s). "+ + err = fmt.Errorf("init accepts at most 1 platform argument (options: %s). "+ "default platform is %v\n", strings.Join(supportedPlatforms, ", "), DefaultCompilationPlatform) + cmdLogger.Error("Failed to validate args to the init command", err) + return err } // Ensure the optional provided argument refers to a supported platform if len(args) == 1 && !compilation.IsSupportedCompilationPlatform(args[0]) { - return fmt.Errorf("init was provided invalid platform argument '%s' (options: %s)", args[0], strings.Join(supportedPlatforms, ", ")) + err := fmt.Errorf("init was provided invalid platform argument '%s' (options: %s)", args[0], strings.Join(supportedPlatforms, ", ")) + cmdLogger.Error("Failed to validate args to the init command", err) + return err } return nil @@ -89,12 +98,14 @@ func cmdRunInit(cmd *cobra.Command, args []string) error { outputFlagUsed := cmd.Flags().Changed("out") outputPath, err := cmd.Flags().GetString("out") if err != nil { + cmdLogger.Error("Failed to run the init command", err) return err } // If we weren't provided an output path (flag was not used), we use our working directory if !outputFlagUsed { workingDirectory, err := os.Getwd() if err != nil { + cmdLogger.Error("Failed to run the init command", err) return err } outputPath = filepath.Join(workingDirectory, DefaultProjectConfigFilename) @@ -103,6 +114,7 @@ func cmdRunInit(cmd *cobra.Command, args []string) error { // By default, projectConfig will be the default project config for the DefaultCompilationPlatform projectConfig, err := config.GetDefaultProjectConfig(DefaultCompilationPlatform) if err != nil { + cmdLogger.Error("Failed to run the init command", err) return err } @@ -111,6 +123,8 @@ func cmdRunInit(cmd *cobra.Command, args []string) error { if len(args) == 1 && args[0] != DefaultCompilationPlatform { projectConfig, err = config.GetDefaultProjectConfig(args[0]) if err != nil { + cmdLogger.Error("Failed to run the init command", err) + return err } } @@ -118,12 +132,14 @@ func cmdRunInit(cmd *cobra.Command, args []string) error { // Update the project configuration given whatever flags were set using the CLI err = updateProjectConfigWithInitFlags(cmd, projectConfig) if err != nil { + cmdLogger.Error("Failed to run the init command", err) return err } // Write our project configuration err = projectConfig.WriteToFile(outputPath) if err != nil { + cmdLogger.Error("Failed to run the init command", err) return err } @@ -131,6 +147,6 @@ func cmdRunInit(cmd *cobra.Command, args []string) error { if absoluteOutputPath, err := filepath.Abs(outputPath); err == nil { outputPath = absoluteOutputPath } - fmt.Printf("Project configuration successfully output to: %s\n", outputPath) + cmdLogger.Info("Project configuration successfully output to: ", colors.Bold, outputPath, colors.Reset) return nil } diff --git a/cmd/root.go b/cmd/root.go index 4cd678f3..cfe2a15e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,10 @@ package cmd import ( + "github.com/crytic/medusa/logging" + "github.com/rs/zerolog" "github.com/spf13/cobra" + "io" ) const version = "0.1.0" @@ -14,6 +17,9 @@ var rootCmd = &cobra.Command{ Long: "medusa is a solidity smart contract fuzzing harness", } +// cmdLogger is the logger that will be used for the cmd package +var cmdLogger = logging.NewLogger(zerolog.InfoLevel, true, make([]io.Writer, 0)...) + // Execute provides an exportable function to invoke the CLI. // Returns an error if one was encountered. func Execute() error { diff --git a/compilation/supported_platforms.go b/compilation/supported_platforms.go index 91dd6732..55178abc 100644 --- a/compilation/supported_platforms.go +++ b/compilation/supported_platforms.go @@ -3,6 +3,7 @@ package compilation import ( "fmt" "github.com/crytic/medusa/compilation/platforms" + "github.com/crytic/medusa/logging" ) // defaultPlatformConfigGenerator is a mapping of platform identifier to generator functions which can be used to create @@ -30,7 +31,8 @@ func init() { // If this platform already exists in our mapping, panic. Each platform should have a unique identifier. if _, platformIdExists := defaultPlatformConfigGenerator[platformId]; platformIdExists { - panic(fmt.Errorf("the compilation platform '%s' is registered with more than one provider", platformId)) + err := fmt.Errorf("the compilation platform '%s' is registered with more than one provider", platformId) + logging.GlobalLogger.Panic("Failed to initialize default platform configurations", err) } // Add this entry to our mapping diff --git a/fuzzing/calls/call_message.go b/fuzzing/calls/call_message.go index 8bde25b0..de3a856f 100644 --- a/fuzzing/calls/call_message.go +++ b/fuzzing/calls/call_message.go @@ -1,8 +1,8 @@ package calls import ( - "fmt" "github.com/crytic/medusa/chain" + "github.com/crytic/medusa/logging" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" coreTypes "github.com/ethereum/go-ethereum/core/types" @@ -138,7 +138,7 @@ func (m *CallMessage) Data() []byte { if m.MsgDataAbiValues != nil { data, err := m.MsgDataAbiValues.Pack() if err != nil { - panic(fmt.Errorf("error while packing call message ABI values: %v", err)) + logging.GlobalLogger.Panic("Failed to pack call message ABI values", err) } return data } diff --git a/fuzzing/calls/call_sequence.go b/fuzzing/calls/call_sequence.go index 872f3cca..41ae2ccc 100644 --- a/fuzzing/calls/call_sequence.go +++ b/fuzzing/calls/call_sequence.go @@ -4,17 +4,16 @@ import ( "encoding/binary" "fmt" "github.com/crytic/medusa/chain" + chainTypes "github.com/crytic/medusa/chain/types" + fuzzingTypes "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/fuzzing/valuegeneration" + "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "strconv" - "strings" - - chainTypes "github.com/crytic/medusa/chain/types" - fuzzingTypes "github.com/crytic/medusa/fuzzing/contracts" - "github.com/crytic/medusa/fuzzing/valuegeneration" - "github.com/ethereum/go-ethereum/accounts/abi" ) // CallSequence describes a sequence of calls sent to a chain. @@ -34,27 +33,37 @@ func (cs CallSequence) AttachExecutionTraces(chain *chain.TestChain, contractDef return nil } -// String returns a displayable string representing the CallSequence. -func (cs CallSequence) String() string { +// Log returns a logging.LogBuffer that represents this call sequence. This buffer will be passed to the underlying +// logger which will format it accordingly for console or file. +func (cs CallSequence) Log() *logging.LogBuffer { + buffer := logging.NewLogBuffer() // If we have an empty call sequence, return a special string if len(cs) == 0 { - return "" + buffer.Append("") + return buffer } - // Construct a list of strings for each call made in the sequence - var elementStrings []string + // Construct the buffer for each call made in the sequence for i := 0; i < len(cs); i++ { // Add the string representing the call - elementStrings = append(elementStrings, fmt.Sprintf("%d) %s", i+1, cs[i].String())) + buffer.Append(fmt.Sprintf("%d) %s\n", i+1, cs[i].String())) // If we have an execution trace attached, print information about it. if cs[i].ExecutionTrace != nil { - elementStrings = append(elementStrings, cs[i].ExecutionTrace.String()) + buffer.Append(cs[i].ExecutionTrace.Log().Elements()...) + buffer.Append("\n") } } - // Join each element with new lines and return it. - return strings.Join(elementStrings, "\n") + // Return the buffer + return buffer +} + +// String returns the string representation of this call sequence +func (cs CallSequence) String() string { + // Internally, we just call the log function, get the list of elements and create their non-colorized string representation + // Might be useful for 3rd party apps + return cs.Log().String() } // Clone creates a copy of the underlying CallSequence. diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index a0a60d3d..283e5329 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -3,8 +3,8 @@ package config import ( "encoding/json" "errors" - "fmt" "github.com/crytic/medusa/chain/config" + "github.com/rs/zerolog" "os" "github.com/crytic/medusa/compilation" @@ -17,6 +17,9 @@ type ProjectConfig struct { // Compilation describes the configuration used to compile the underlying project. Compilation *compilation.CompilationConfig `json:"compilation"` + + // Logging describes the configuration used for logging to file and console + Logging LoggingConfig `json:"logging"` } // FuzzingConfig describes the configuration options used by the fuzzing.Fuzzer. @@ -177,11 +180,38 @@ type OptimizationTestingConfig struct { TestPrefixes []string `json:"testPrefixes"` } +// LoggingConfig describes the configuration options for logging to console and file +type LoggingConfig struct { + // Level describes whether logs of certain severity levels (eg info, warning, etc.) will be emitted or discarded. + // Increasing level values represent more severe logs + Level zerolog.Level `json:"level"` + + // LogDirectory describes what directory log files should be outputted in/ LogDirectory being a non-empty string is + // equivalent to enabling file logging. + LogDirectory string `json:"logDirectory"` +} + +// ConsoleLoggingConfig describes the configuration options for logging to console. Note that this not being used right now +// but will be added to LoggingConfig down the line +// TODO: Update when implementing a structured logging solution +type ConsoleLoggingConfig struct { + // Enabled describes whether console logging is enabled. + Enabled bool `json:"enabled"` +} + +// FileLoggingConfig describes the configuration options for logging to file. Note that this not being used right now +// but will be added to LoggingConfig down the line +// TODO: Update when implementing a structured logging solution +type FileLoggingConfig struct { + // LogDirectory describes what directory log files should be outputted in. LogDirectory being a non-empty string + // is equivalent to enabling file logging. + LogDirectory bool `json:"logDirectory"` +} + // 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) { // Read our project configuration file data - fmt.Printf("Reading configuration file: %s\n", path) b, err := os.ReadFile(path) if err != nil { return nil, err @@ -261,5 +291,11 @@ func (p *ProjectConfig) Validate() error { return errors.New("project configuration must specify test name prefixes if property testing is enabled") } } + + // Ensure that the log level is a valid one + if _, err := zerolog.ParseLevel(p.Logging.Level.String()); err != nil { + return err + } + return nil } diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 3a26d3d3..2a6e92f0 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -3,6 +3,7 @@ package config import ( testChainConfig "github.com/crytic/medusa/chain/config" "github.com/crytic/medusa/compilation" + "github.com/rs/zerolog" ) // GetDefaultProjectConfig obtains a default configuration for a project. It populates a default compilation config @@ -79,6 +80,10 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { TestChainConfig: *chainConfig, }, Compilation: compilationConfig, + Logging: LoggingConfig{ + Level: zerolog.InfoLevel, + LogDirectory: "", + }, } // Return the project configuration diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 28255579..827a3f58 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -6,6 +6,8 @@ import ( "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/coverage" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" @@ -51,6 +53,9 @@ type Corpus struct { // callSequencesLock provides thread synchronization to prevent concurrent access errors into // callSequences. callSequencesLock sync.Mutex + + // logger describes the Corpus's log object that can be used to log important events + logger *logging.Logger } // NewCorpus initializes a new Corpus object, reading artifacts from the provided directory. If the directory refers @@ -64,6 +69,7 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) { immutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""), testResultSequenceFiles: newCorpusDirectory[calls.CallSequence](""), unexecutedCallSequences: make([]calls.CallSequence, 0), + logger: logging.GlobalLogger.NewSubLogger("module", "corpus"), } // If we have a corpus directory set, parse our call sequences. @@ -216,7 +222,7 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe } c.unexecutedCallSequences = append(c.unexecutedCallSequences, sequence) } else { - fmt.Printf("corpus item '%v' disabled due to error when replaying it: %v\n", sequenceFileData.fileName, sequenceInvalidError) + c.logger.Warn("Corpus item ", colors.Bold, sequenceFileData.fileName, colors.Reset, " disabled due to error when replaying it", sequenceInvalidError) } // Revert chain state to our starting point to test the next sequence. diff --git a/fuzzing/coverage/coverage_tracer.go b/fuzzing/coverage/coverage_tracer.go index 4882c640..4652fde4 100644 --- a/fuzzing/coverage/coverage_tracer.go +++ b/fuzzing/coverage/coverage_tracer.go @@ -1,8 +1,8 @@ package coverage import ( - "fmt" "github.com/crytic/medusa/chain/types" + "github.com/crytic/medusa/logging" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" "math/big" @@ -92,14 +92,14 @@ func (t *CoverageTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { if err != nil { _, revertCoverageErr := t.callFrameStates[t.callDepth].pendingCoverageMap.RevertAll() if revertCoverageErr != nil { - panic(revertCoverageErr) + logging.GlobalLogger.Panic("Coverage tracer failed to update revert coverage map during capture end", revertCoverageErr) } } // Commit all our coverage maps up one call frame. _, _, coverageUpdateErr := t.coverageMaps.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) if coverageUpdateErr != nil { - panic(fmt.Sprintf("coverage tracer failed to update coverage map during capture exit: %v", coverageUpdateErr)) + logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map during capture end", coverageUpdateErr) } // Pop the state tracking struct for this call frame off the stack. @@ -124,14 +124,14 @@ func (t *CoverageTracer) CaptureExit(output []byte, gasUsed uint64, err error) { if err != nil { _, revertCoverageErr := t.callFrameStates[t.callDepth].pendingCoverageMap.RevertAll() if revertCoverageErr != nil { - panic(revertCoverageErr) + logging.GlobalLogger.Panic("Coverage tracer failed to update revert coverage map during capture exit", revertCoverageErr) } } // Commit all our coverage maps up one call frame. _, _, coverageUpdateErr := t.callFrameStates[t.callDepth-1].pendingCoverageMap.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) if coverageUpdateErr != nil { - panic(fmt.Sprintf("coverage tracer failed to update coverage map during capture exit: %v", coverageUpdateErr)) + logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map during capture exit", coverageUpdateErr) } // Pop the state tracking struct for this call frame off the stack. @@ -157,7 +157,7 @@ func (t *CoverageTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, // Record coverage for this location in our map. _, coverageUpdateErr := callFrameState.pendingCoverageMap.SetAt(scope.Contract.Address(), *callFrameState.lookupHash, len(scope.Contract.Code), pc) if coverageUpdateErr != nil { - panic(fmt.Sprintf("coverage tracer failed to update coverage map while tracing state: %v", coverageUpdateErr)) + logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map while tracing state", coverageUpdateErr) } } } diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index fcca4d24..a021de9d 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -6,6 +6,8 @@ import ( "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/valuegeneration" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" "github.com/ethereum/go-ethereum/accounts/abi" coreTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -32,13 +34,16 @@ func newExecutionTrace(contracts contracts.Contracts) *ExecutionTrace { } } -// generateCallFrameEnterString generates a header string to print for the given call frame. It contains -// information about the invoked call. -// Returns the header string -func (t *ExecutionTrace) generateCallFrameEnterString(callFrame *CallFrame) string { - // Define some strings that represent our current call frame +// generateCallFrameEnterElements generates a list of elements describing top level information about this call frame. +// This list of elements will hold information about what kind of call it is, wei sent, what method is called, and more. +// Additionally, the list may also hold formatting options for console output. +func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) []any { + // Create list of elements + elements := make([]any, 0) + + // Define some strings and objects that represent our current call frame var ( - callType = "call" + callType = []any{colors.BlueBold, "[call] ", colors.Reset} proxyContractName = "" codeContractName = "" methodName = "" @@ -46,13 +51,16 @@ func (t *ExecutionTrace) generateCallFrameEnterString(callFrame *CallFrame) stri err error ) - // If this is a contract creation, use a different prefix + // If this is a contract creation or proxy call, use different formatting for call type if callFrame.IsContractCreation() { - callType = "creation" + callType = []any{colors.YellowBold, "[creation] ", colors.Reset} } else if callFrame.IsProxyCall() { - callType = "proxy call" + callType = []any{colors.CyanBold, "[proxy call] ", colors.Reset} } + // Append the formatted call type information to the list of elements + elements = append(elements, callType...) + // Resolve our contract names, as well as our method and its name from the code contract. if callFrame.ToContractAbi != nil { proxyContractName = callFrame.ToContractName @@ -103,25 +111,33 @@ func (t *ExecutionTrace) generateCallFrameEnterString(callFrame *CallFrame) stri // Generate the message we wish to output finally, using all these display string components. // If we executed code, attach additional context such as the contract name, method, etc. + var callInfo string if callFrame.IsProxyCall() { if callFrame.ExecutedCode { - return fmt.Sprintf("[%v] %v -> %v.%v(%v) (addr=%v, code=%v, value=%v, sender=%v)", callType, proxyContractName, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CodeAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + callInfo = fmt.Sprintf("%v -> %v.%v(%v) (addr=%v, code=%v, value=%v, sender=%v)", proxyContractName, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CodeAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) } else { - return fmt.Sprintf("[%v] (addr=%v, value=%v, sender=%v)", callType, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) } } else { if callFrame.ExecutedCode { - return fmt.Sprintf("[%v] %v.%v(%v) (addr=%v, value=%v, sender=%v)", callType, codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + callInfo = fmt.Sprintf("%v.%v(%v) (addr=%v, value=%v, sender=%v)", codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) } else { - return fmt.Sprintf("[%v] (addr=%v, value=%v, sender=%v)", callType, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) } } + + // Add call information to the elements + elements = append(elements, callInfo, "\n") + + return elements } -// generateCallFrameExitString generates a footer string to print for the given call frame. It contains -// result information about the call. -// Returns the footer string. -func (t *ExecutionTrace) generateCallFrameExitString(callFrame *CallFrame) string { +// generateCallFrameExitElements generates a list of elements describing the return data of the call frame (e.g. +// traditional return data, assertion failure, revert data, etc.). Additionally, the list may also hold formatting options for console output. +func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *CallFrame) []any { + // Create list of elements + elements := make([]any, 0) + // Define some strings that represent our current call frame var method *abi.Method @@ -158,19 +174,22 @@ func (t *ExecutionTrace) generateCallFrameExitString(callFrame *CallFrame) strin // Wrap our return message and output it at the end. if callFrame.ReturnError == nil { - return fmt.Sprintf("[return (%v)]", *outputArgumentsDisplayText) + elements = append(elements, colors.GreenBold, fmt.Sprintf("[return (%v)]", *outputArgumentsDisplayText), colors.Reset, "\n") + return elements } // Try to resolve a panic message and check if it signals a failed assertion. panicCode := abiutils.GetSolidityPanicCode(callFrame.ReturnError, callFrame.ReturnData, true) if panicCode != nil { - return "[" + abiutils.GetPanicReason(panicCode.Uint64()) + "]" + elements = append(elements, colors.RedBold, fmt.Sprintf("[%v]", abiutils.GetPanicReason(panicCode.Uint64())), colors.Reset, "\n") + return elements } // Try to resolve an assertion failed panic code. errorMessage := abiutils.GetSolidityRevertErrorString(callFrame.ReturnError, callFrame.ReturnData) if errorMessage != nil { - return fmt.Sprintf("[revert ('%v')]", *errorMessage) + elements = append(elements, colors.RedBold, fmt.Sprintf("[revert ('%v')]", *errorMessage), colors.Reset, "\n") + return elements } // Try to unpack a custom Solidity error from the return values. @@ -178,23 +197,28 @@ func (t *ExecutionTrace) generateCallFrameExitString(callFrame *CallFrame) strin if matchedCustomError != nil { customErrorArgsDisplayText, err := valuegeneration.EncodeABIArgumentsToString(matchedCustomError.Inputs, unpackedCustomErrorArgs) if err == nil { - return fmt.Sprintf("[revert (error: %v(%v))]", matchedCustomError.Name, customErrorArgsDisplayText) + elements = append(elements, colors.RedBold, fmt.Sprintf("[revert (error: %v(%v))]", matchedCustomError.Name, customErrorArgsDisplayText), colors.Reset, "\n") + return elements } } // Check if this is a generic revert. if callFrame.ReturnError == vm.ErrExecutionReverted { - return "[revert]" + elements = append(elements, colors.RedBold, "[revert]", colors.Reset, "\n") + return elements } // If we could not resolve any custom error, we simply print out the generic VM error message. - return fmt.Sprintf("[vm error ('%v')]", callFrame.ReturnError.Error()) + elements = append(elements, colors.RedBold, fmt.Sprintf("[vm error ('%v')]", callFrame.ReturnError.Error()), colors.Reset, "\n") + return elements } -// generateEventEmittedString generates a string used to express an event emission. It contains information about an -// event log. -// Returns a string representing an event emission. -func (t *ExecutionTrace) generateEventEmittedString(callFrame *CallFrame, eventLog *coreTypes.Log) string { +// generateEventEmittedElements generates a list of elements used to express an event emission. It contains information about an +// event log such as the topics and the event data. Additionally, the list may also hold formatting options for console output. +func (t *ExecutionTrace) generateEventEmittedElements(callFrame *CallFrame, eventLog *coreTypes.Log) []any { + // Create list of elements + elements := make([]any, 0) + // If this is an event log, match it in our contract's ABI. var eventDisplayText *string @@ -235,26 +259,27 @@ func (t *ExecutionTrace) generateEventEmittedString(callFrame *CallFrame, eventL } // Finally, add our output line with this event data to it. - return fmt.Sprintf("[event] %v", *eventDisplayText) + elements = append(elements, colors.MagentaBold, "[event] ", colors.Reset, *eventDisplayText, "\n") + return elements } -// generateStringsForCallFrame generates indented strings for a given call frame and its children. -// Returns the list of strings, to be joined by new line separators. -func (t *ExecutionTrace) generateStringsForCallFrame(currentDepth int, callFrame *CallFrame) []string { - // Create our resulting strings array - var outputLines []string +// generateElementsForCallFrame generates a list of elements for a given call frame and its children. Additionally, +// the list may also hold formatting options for console output. +func (t *ExecutionTrace) generateElementsForCallFrame(currentDepth int, callFrame *CallFrame) []any { + // Create list of elements + elements := make([]any, 0) // Create our current call line prefix (indented by call depth) - prefix := strings.Repeat("\t", currentDepth) + " -> " + prefix := strings.Repeat("\t", currentDepth) + " => " // If we're printing the root frame, add the overall execution trace header. if currentDepth == 0 { - outputLines = append(outputLines, prefix+"[Execution Trace]") + elements = append(elements, colors.Bold, "[Execution Trace]", colors.Reset, "\n") } - // Add the call frame enter header - header := prefix + t.generateCallFrameEnterString(callFrame) - outputLines = append(outputLines, header) + // Add the call frame enter header elements + elements = append(elements, prefix) + elements = append(elements, t.generateCallFrameEnterElements(callFrame)...) // Now that the header has been printed, create our indent level to express everything that // happened under it. @@ -268,31 +293,40 @@ func (t *ExecutionTrace) generateStringsForCallFrame(currentDepth int, callFrame for _, operation := range callFrame.Operations { if childCallFrame, ok := operation.(*CallFrame); ok { // If this is a call frame being entered, generate information recursively. - childOutputLines := t.generateStringsForCallFrame(currentDepth+1, childCallFrame) - outputLines = append(outputLines, childOutputLines...) + childOutputLines := t.generateElementsForCallFrame(currentDepth+1, childCallFrame) + elements = append(elements, childOutputLines...) } else if eventLog, ok := operation.(*coreTypes.Log); ok { // If an event log was emitted, add a message for it. - eventMessage := prefix + t.generateEventEmittedString(callFrame, eventLog) - outputLines = append(outputLines, eventMessage) + elements = append(elements, prefix) + elements = append(elements, t.generateEventEmittedElements(callFrame, eventLog)...) } } // If we self-destructed, add a message for it before our footer. if callFrame.SelfDestructed { - outputLines = append(outputLines, fmt.Sprintf("%v[selfdestruct]", prefix)) + elements = append(elements, prefix, colors.MagentaBold, "[selfdestruct]", colors.Reset, "\n") } // Add the call frame exit footer - footer := prefix + t.generateCallFrameExitString(callFrame) - outputLines = append(outputLines, footer) + elements = append(elements, prefix) + elements = append(elements, t.generateCallFrameExitElements(callFrame)...) } - // Return our output lines - return outputLines + // Return our elements + return elements +} + +// Log returns a logging.LogBuffer that represents this execution trace. This buffer will be passed to the underlying +// logger which will format it accordingly for console or file. +func (t *ExecutionTrace) Log() *logging.LogBuffer { + buffer := logging.NewLogBuffer() + buffer.Append(t.generateElementsForCallFrame(0, t.TopLevelCallFrame)...) + return buffer } -// String returns a string representation of the execution trace. +// String returns the string representation of this execution trace func (t *ExecutionTrace) String() string { - outputLines := t.generateStringsForCallFrame(0, t.TopLevelCallFrame) - return strings.Join(outputLines, "\n") + // Internally, we just call the log function, get the list of elements and create their non-colorized string representation + // Might be useful for 3rd party apps + return t.Log().String() } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 2ad2d2f6..e64a7ba2 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -4,10 +4,16 @@ import ( "context" "fmt" "github.com/crytic/medusa/fuzzing/coverage" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" + "io" "math/big" "math/rand" "path/filepath" "sort" + "strconv" "strings" "sync" "time" @@ -74,26 +80,53 @@ type Fuzzer struct { // Hooks describes the replaceable functions used by the Fuzzer. Hooks FuzzerHooks + + // logger describes the Fuzzer's log object that can be used to log important events + logger *logging.Logger } // NewFuzzer returns an instance of a new Fuzzer provided a project configuration, or an error if one is encountered // while initializing the code. func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { + // Create the global logger, set some global logging parameters, and enable terminal coloring + logging.GlobalLogger = logging.NewLogger(config.Logging.Level, true, make([]io.Writer, 0)...) + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + // If the log directory is a non-empty string, create a file for file logging + if config.Logging.LogDirectory != "" { + // Filename will be the "log-current_unix_timestamp.log" + filename := "log-" + strconv.FormatInt(time.Now().Unix(), 10) + ".log" + // Create the file + file, err := utils.CreateFile(config.Logging.LogDirectory, filename) + if err != nil { + logging.GlobalLogger.Error("Failed to create log file", err) + return nil, err + } + logging.GlobalLogger.AddWriter(file, logging.UNSTRUCTURED) + } + + // Get the fuzzer's custom sub-logger + logger := logging.GlobalLogger.NewSubLogger("module", "fuzzer") + // Validate our provided config err := config.Validate() if err != nil { + logger.Error("Invalid configuration", err) return nil, err } // Parse the senders addresses from our account config. senders, err := utils.HexStringsToAddresses(config.Fuzzing.SenderAddresses) if err != nil { + logger.Error("Invalid sender address(es)", err) return nil, err } // Parse the deployer address from our account config deployer, err := utils.HexStringToAddress(config.Fuzzing.DeployerAddress) if err != nil { + logger.Error("Invalid deployer address", err) return nil, err } @@ -112,6 +145,7 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { ChainSetupFunc: chainSetupFromCompilations, CallSequenceTestFuncs: make([]CallSequenceTestFunc, 0), }, + logger: logger, } // Add our sender and deployer addresses to the base value set for the value generator, so they will be used as @@ -124,12 +158,12 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { // If we have a compilation config if fuzzer.config.Compilation != nil { // Compile the targets specified in the compilation config - fmt.Printf("Compiling targets (platform '%s') ...\n", fuzzer.config.Compilation.Platform) - compilations, compilationOutput, err := (*fuzzer.config.Compilation).Compile() + fuzzer.logger.Info("Compiling targets with ", colors.Bold, fuzzer.config.Compilation.Platform, colors.Reset) + compilations, _, err := (*fuzzer.config.Compilation).Compile() if err != nil { + fuzzer.logger.Error("Failed to compile target", err) return nil, err } - fmt.Printf("%s", compilationOutput) // Add our compilation targets fuzzer.AddCompilationTargets(compilations) @@ -143,8 +177,8 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { 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") + // 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 @@ -221,7 +255,7 @@ func (f *Fuzzer) ReportTestCaseFinished(testCase TestCase) { // We only log here if we're not configured to stop on the first test failure. This is because the fuzzer prints // results on exit, so we avoid duplicate messages. if !f.config.Fuzzing.Testing.StopOnFailedTest { - fmt.Printf("\n[%s] %s\n%s\n\n", testCase.Status(), testCase.Name(), testCase.Message()) + f.logger.Info(testCase.LogMessage().Elements()...) } // If the config specifies, we stop after the first failed test reported. @@ -255,7 +289,7 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati // Cache all of our source code if it hasn't been already. err := compilation.CacheSourceCode() if err != nil { - fmt.Printf("Warning: could not cache compilation source file data due to error: %v", err) + f.logger.Warn("Failed to cache compilation source file data", err) } } } @@ -464,7 +498,7 @@ func (f *Fuzzer) spawnWorkersLoop(baseTestChain *chain.TestChain) error { working := !utils.CheckContextDone(f.ctx) // Log that we are about to create the workers and start fuzzing - fmt.Printf("Creating %d workers ...\n", f.config.Fuzzing.Workers) + f.logger.Info("Creating ", colors.Bold, f.config.Fuzzing.Workers, colors.Reset, " workers...") 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 @@ -562,13 +596,14 @@ func (f *Fuzzer) Start() error { // If we set a timeout, create the timeout context now, as we're about to begin fuzzing. if f.config.Fuzzing.Timeout > 0 { - fmt.Printf("Running with timeout of %d seconds\n", f.config.Fuzzing.Timeout) + f.logger.Info("Running with a timeout of ", colors.Bold, f.config.Fuzzing.Timeout, " seconds") f.ctx, f.ctxCancelFunc = context.WithTimeout(f.ctx, time.Duration(f.config.Fuzzing.Timeout)*time.Second) } // Set up the corpus f.corpus, err = corpus.NewCorpus(f.config.Fuzzing.CorpusDirectory) if err != nil { + f.logger.Error("Failed to create the corpus", err) return err } @@ -584,18 +619,21 @@ func (f *Fuzzer) Start() error { // Create our test chain baseTestChain, err := f.createTestChain() if err != nil { + f.logger.Error("Failed to create the test chain", err) return err } // Set it up with our deployment/setup strategy defined by the fuzzer. 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) if err != nil { + f.logger.Error("Failed to initialize the corpus", err) return err } @@ -605,16 +643,22 @@ func (f *Fuzzer) Start() error { // Publish a fuzzer starting event. err = f.Events.FuzzerStarting.Publish(FuzzerStartingEvent{Fuzzer: f}) if err != nil { + f.logger.Error("FuzzerStarting event subscriber returned an error", err) return err } // If StopOnNoTests is true and there are no test cases, then throw an error if f.config.Fuzzing.Testing.StopOnNoTests && len(f.testCases) == 0 { - return fmt.Errorf("no tests of any kind (assertion/property/optimization/custom) have been identified for fuzzing") + err = fmt.Errorf("no tests of any kind (assertion/property/optimization/custom) have been identified for fuzzing") + f.logger.Error("Failed to start fuzzer", err) + return err } // Run the main worker loop err = f.spawnWorkersLoop(baseTestChain) + if err != nil { + f.logger.Error("Encountered an error in the main fuzzing loop", err) + } // NOTE: After this point, we capture errors but do not return immediately, as we want to exit gracefully. @@ -622,8 +666,9 @@ func (f *Fuzzer) Start() error { // previous error, as we don't want to lose corpus entries. if f.config.Fuzzing.CoverageEnabled { corpusFlushErr := f.corpus.Flush() - if err == nil { + if err == nil && corpusFlushErr != nil { err = corpusFlushErr + f.logger.Info("Failed to flush the corpus", err) } } @@ -631,6 +676,7 @@ func (f *Fuzzer) Start() error { fuzzerStoppingErr := f.Events.FuzzerStopping.Publish(FuzzerStoppingEvent{Fuzzer: f, err: err}) if err == nil && fuzzerStoppingErr != nil { err = fuzzerStoppingErr + f.logger.Error("FuzzerStopping event subscriber returned an error", err) } // Print our results on exit. @@ -640,7 +686,7 @@ func (f *Fuzzer) Start() error { if err == nil && f.config.Fuzzing.CorpusDirectory != "" { coverageReportPath := filepath.Join(f.config.Fuzzing.CorpusDirectory, "coverage_report.html") err = coverage.GenerateReport(f.compilations, f.corpus.CoverageMaps(), coverageReportPath) - fmt.Printf("coverage report saved to file: %v\n", coverageReportPath) + f.logger.Info("Coverage report saved to file: ", colors.Bold, coverageReportPath, colors.Reset) } // Return any encountered error. @@ -677,15 +723,12 @@ func (f *Fuzzer) printMetricsLoop() { secondsSinceLastUpdate := time.Since(lastPrintedTime).Seconds() // Print a metrics update - fmt.Printf( - "fuzz: elapsed: %s, call: %d (%d/sec), seq/s: %d, resets/s: %d, cov: %d\n", - time.Since(startTime).Round(time.Second), - callsTested, - uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate), - uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate), - uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate), - f.corpus.ActiveMutableSequenceCount(), - ) + f.logger.Info(colors.Bold, "fuzz: ", colors.Reset, + "elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset, + ", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset, + ", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset, + ", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset, + ", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset) // Update our delta tracking metrics lastPrintedTime = time.Now() @@ -696,7 +739,7 @@ func (f *Fuzzer) printMetricsLoop() { // If we reached our transaction threshold, halt testLimit := f.config.Fuzzing.TestLimit if testLimit > 0 && (!callsTested.IsUint64() || callsTested.Uint64() >= testLimit) { - fmt.Printf("transaction test limit reached, halting now ...\n") + f.logger.Info("Transaction test limit reached, halting now...") f.Stop() break } @@ -736,17 +779,9 @@ func (f *Fuzzer) printExitingResults() { ) // Print the results of each individual test case. - fmt.Printf("\n") - fmt.Printf("Fuzzer stopped, test results follow below ...\n") + f.logger.Info("Fuzzer stopped, test results follow below ...") for _, testCase := range f.testCases { - // Obtain the test case message. If it is a non-empty string, we format our output for it specially. - // Otherwise, we exclude it. - msg := strings.TrimSpace(testCase.Message()) - if msg != "" { - fmt.Printf("[%s] %s\n%s\n\n", testCase.Status(), strings.TrimSpace(testCase.Name()), msg) - } else { - fmt.Printf("[%s] %s\n", testCase.Status(), testCase.Name()) - } + f.logger.Info(testCase.LogMessage().Elements()...) // Tally our pass/fail count. if testCase.Status() == TestCaseStatusPassed { @@ -757,6 +792,5 @@ func (f *Fuzzer) printExitingResults() { } // Print our final tally of test statuses. - fmt.Printf("\n") - fmt.Printf("%d test(s) passed, %d test(s) failed\n", testCountPassed, testCountFailed) + f.logger.Info("Test summary: ", colors.GreenBold, testCountPassed, colors.Reset, " test(s) passed, ", colors.RedBold, testCountFailed, colors.Reset, " test(s) failed") } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 7a80786f..42ff13c5 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -144,8 +144,8 @@ func TestAssertionsAndProperties(t *testing.T) { }) } -// TestOptimizationsSolving runs a test to ensure that optimization mode works as expected -func TestOptimizationsSolving(t *testing.T) { +// TestOptimizationMode runs a test to ensure that optimization mode works as expected +func TestOptimizationMode(t *testing.T) { filePaths := []string{ "testdata/contracts/optimizations/optimize.sol", } @@ -407,7 +407,7 @@ func TestExecutionTraces(t *testing.T) { assert.NotNilf(t, lastCall.ExecutionTrace, "expected to have an execution trace attached to call sequence for this test") // Get the execution trace message - executionTraceMsg := lastCall.ExecutionTrace.String() + executionTraceMsg := lastCall.ExecutionTrace.Log().String() // Verify it contains all expected strings for _, expectedTraceMessage := range expectedTraceMessages { diff --git a/fuzzing/test_case.go b/fuzzing/test_case.go index d5e6a546..6be040e7 100644 --- a/fuzzing/test_case.go +++ b/fuzzing/test_case.go @@ -2,6 +2,7 @@ package fuzzing import ( "github.com/crytic/medusa/fuzzing/calls" + "github.com/crytic/medusa/logging" ) // TestCaseStatus defines the status of a TestCase as a string-represented enum. @@ -31,7 +32,11 @@ type TestCase interface { // Name describes the name of the test case. Name() string - // Message obtains a text-based printable message which describes the test result. + // LogMessage obtains a logging.LogBuffer that represents the result of the TestCase. This buffer can be passed to a logger for + // console or file logging. + LogMessage() *logging.LogBuffer + + // Message obtains a text-based printable message which describes the result of the AssertionTestCase. Message() string // ID obtains a unique identifier for a test result. If the same test fails, this ID should match for both diff --git a/fuzzing/test_case_assertion.go b/fuzzing/test_case_assertion.go index 354c2161..7cdf3bb3 100644 --- a/fuzzing/test_case_assertion.go +++ b/fuzzing/test_case_assertion.go @@ -2,6 +2,8 @@ package fuzzing import ( "fmt" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" "strings" "github.com/crytic/medusa/fuzzing/calls" @@ -37,18 +39,27 @@ func (t *AssertionTestCase) Name() string { return fmt.Sprintf("Assertion Test: %s.%s", t.targetContract.Name(), t.targetMethod.Sig) } -// Message obtains a text-based printable message which describes the test result. -func (t *AssertionTestCase) Message() string { +// LogMessage obtains a buffer that represents the result of the AssertionTestCase. This buffer can be passed to a logger for +// console or file logging. +func (t *AssertionTestCase) LogMessage() *logging.LogBuffer { // If the test failed, return a failure message. + buffer := logging.NewLogBuffer() if t.Status() == TestCaseStatusFailed { - return fmt.Sprintf( - "Test for method \"%s.%s\" failed after the following call sequence resulted in an assertion:\n%s", - t.targetContract.Name(), - t.targetMethod.Sig, - t.CallSequence().String(), - ) + buffer.Append(colors.RedBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset, "\n") + buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" resulted in an assertion failure after the following call sequence:\n", t.targetContract.Name(), t.targetMethod.Sig)) + buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") + buffer.Append(t.CallSequence().Log().Elements()...) + return buffer } - return "" + + buffer.Append(colors.GreenBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset) + return buffer +} + +// Message obtains a text-based printable message which describes the result of the AssertionTestCase. +func (t *AssertionTestCase) Message() string { + // Internally, we just call log message and convert it to a string. This can be useful for 3rd party apps + return t.LogMessage().String() } // ID obtains a unique identifier for a test result. diff --git a/fuzzing/test_case_optimization.go b/fuzzing/test_case_optimization.go index 3110c0ad..8b3f4e41 100644 --- a/fuzzing/test_case_optimization.go +++ b/fuzzing/test_case_optimization.go @@ -5,6 +5,8 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" "github.com/ethereum/go-ethereum/accounts/abi" "math/big" "strings" @@ -45,25 +47,29 @@ 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 +// LogMessage obtains a buffer that represents the result of the AssertionTestCase. This buffer can be passed to a logger for +// console or file logging. +func (t *OptimizationTestCase) LogMessage() *logging.LogBuffer { + buffer := logging.NewLogBuffer() + + // Note that optimization tests will always pass + buffer.Append(colors.GreenBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset, "\n") + buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" resulted in the maximum value: ", t.targetContract.Name(), t.targetMethod.Sig)) + buffer.Append(colors.Bold, t.value, colors.Reset, "\n") + buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") + buffer.Append(t.CallSequence().Log().Elements()...) + // If an execution trace is attached then add it to the message + if t.optimizationTestTrace != nil { + buffer.Append(colors.Bold, "[Optimization Test Execution Trace]", colors.Reset, "\n") + buffer.Append(t.optimizationTestTrace.Log().Elements()...) } - return "" + return buffer +} + +// Message obtains a text-based printable message which describes the result of the OptimizationTestCase. +func (t *OptimizationTestCase) Message() string { + // Internally, we just call log message and convert it to a string. This can be useful for 3rd party apps + return t.LogMessage().String() } // ID obtains a unique identifier for a test result. diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index 622aa155..ba5ab258 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -180,11 +180,6 @@ func (t *OptimizationTestCaseProvider) onFuzzerStopping(event FuzzerStoppingEven for _, testCase := range t.testCases { if testCase.status == TestCaseStatusRunning { testCase.status = TestCaseStatusPassed - // Since optimization tests do not really "finish", we will report that they are finished when the fuzzer - // stops. - if event.Fuzzer != nil { - event.Fuzzer.ReportTestCaseFinished(testCase) - } } } return nil diff --git a/fuzzing/test_case_property.go b/fuzzing/test_case_property.go index 9967b1ae..f20d0769 100644 --- a/fuzzing/test_case_property.go +++ b/fuzzing/test_case_property.go @@ -5,6 +5,8 @@ import ( "github.com/crytic/medusa/fuzzing/calls" fuzzerTypes "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" "github.com/ethereum/go-ethereum/accounts/abi" "strings" ) @@ -39,24 +41,33 @@ func (t *PropertyTestCase) Name() string { return fmt.Sprintf("Property Test: %s.%s", t.targetContract.Name(), t.targetMethod.Sig) } -// Message obtains a text-based printable message which describes the test result. -func (t *PropertyTestCase) Message() string { +// LogMessage obtains a buffer that represents the result of the PropertyTestCase. This buffer can be passed to a logger for +// console or file logging. +func (t *PropertyTestCase) LogMessage() *logging.LogBuffer { // If the test failed, return a failure message. + buffer := logging.NewLogBuffer() if t.Status() == TestCaseStatusFailed { - msg := fmt.Sprintf( - "Property test \"%s.%s\" failed after the following call sequence:\n%s", - t.targetContract.Name(), - t.targetMethod.Sig, - t.CallSequence().String(), - ) + buffer.Append(colors.RedBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset, "\n") + buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" failed after the following call sequence:\n", t.targetContract.Name(), t.targetMethod.Sig)) + buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") + buffer.Append(t.CallSequence().Log().Elements()...) + // If an execution trace is attached then add it to the message if t.propertyTestTrace != nil { - // TODO: Improve formatting in logging PR - msg += fmt.Sprintf("\nProperty test execution trace:\n%s", t.propertyTestTrace.String()) + buffer.Append(colors.Bold, "[Property Test Execution Trace]", colors.Reset, "\n") + buffer.Append(t.propertyTestTrace.Log().Elements()...) } - return msg + return buffer } - return "" + + buffer.Append(colors.GreenBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset) + return buffer +} + +// Message obtains a text-based printable message which describes the result of the PropertyTestCase. +func (t *PropertyTestCase) Message() string { + // Internally, we just call log message and convert it to a string. This can be useful for 3rd party apps + return t.LogMessage().String() } // ID obtains a unique identifier for a test result. diff --git a/fuzzing/valuegeneration/abi_values.go b/fuzzing/valuegeneration/abi_values.go index 5153dee1..56232e9d 100644 --- a/fuzzing/valuegeneration/abi_values.go +++ b/fuzzing/valuegeneration/abi_values.go @@ -3,6 +3,7 @@ package valuegeneration import ( "encoding/hex" "fmt" + "github.com/crytic/medusa/logging" "math/big" "reflect" "strconv" @@ -93,7 +94,10 @@ func GenerateAbiValue(generator ValueGenerator, inputType *abi.Type) any { // - Mappings cannot be used in public/external methods and must reference storage, so we shouldn't ever // see cases of it unless Solidity was updated in the future. // - FixedPoint types are currently unsupported. - panic(fmt.Sprintf("attempt to generate function argument of unsupported type: '%s'", inputType.String())) + + err := fmt.Errorf("attempt to generate function argument of unsupported type: '%s'", inputType.String()) + logging.GlobalLogger.Panic("Failed to generate abi value", err) + return nil } } diff --git a/go.mod b/go.mod index 3e2af559..ca1b0639 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/fxamacker/cbor v1.5.1 github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.29.0 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 @@ -43,6 +44,8 @@ require ( github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect diff --git a/go.sum b/go.sum index 7e235f86..fe8142b2 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,7 @@ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcju github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -101,6 +102,7 @@ github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -197,11 +199,16 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -271,6 +278,9 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -418,6 +428,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/logging/colors/color_funcs.go b/logging/colors/color_funcs.go new file mode 100644 index 00000000..4a201dbb --- /dev/null +++ b/logging/colors/color_funcs.go @@ -0,0 +1,107 @@ +package colors + +import "fmt" + +// ColorFunc is an alias type for a coloring function that accepts anything and returns a colorized string +type ColorFunc = func(s any) string + +// Reset is a ColorFunc that simply returns the input as a string. It is basically a no-op and is used for resetting the +// color context during complex logging operations. +func Reset(s any) string { + return fmt.Sprintf("%v", s) +} + +// Black is a ColorFunc that returns a black-colorized string of the provided input +func Black(s any) string { + return Colorize(s, BLACK) +} + +// BlackBold is a ColorFunc that returns a black-bold-colorized string of the provided input +func BlackBold(s any) string { + return Colorize(Colorize(s, BLACK), BOLD) +} + +// Red is a ColorFunc that returns a red-colorized string of the provided input +func Red(s any) string { + return Colorize(s, RED) +} + +// RedBold is a ColorFunc that returns a red-bold-colorized string of the provided input +func RedBold(s any) string { + return Colorize(Colorize(s, RED), BOLD) +} + +// Green is a ColorFunc that returns a green-colorized string of the provided input +func Green(s any) string { + return Colorize(s, GREEN) +} + +// GreenBold is a ColorFunc that returns a green-bold-colorized string of the provided input +func GreenBold(s any) string { + return Colorize(Colorize(s, GREEN), BOLD) +} + +// Yellow is a ColorFunc that returns a yellow-colorized string of the provided input +func Yellow(s any) string { + return Colorize(s, YELLOW) +} + +// YellowBold is a ColorFunc that returns a yellow-bold-colorized string of the provided input +func YellowBold(s any) string { + return Colorize(Colorize(s, YELLOW), BOLD) +} + +// Blue is a ColorFunc that returns a blue-colorized string of the provided input +func Blue(s any) string { + return Colorize(s, BLUE) +} + +// BlueBold is a ColorFunc that returns a blue-bold-colorized string of the provided input +func BlueBold(s any) string { + return Colorize(Colorize(s, BLUE), BOLD) +} + +// Magenta is a ColorFunc that returns a magenta-colorized string of the provided input +func Magenta(s any) string { + return Colorize(s, MAGENTA) +} + +// MagentaBold is a ColorFunc that returns a magenta-bold-colorized string of the provided input +func MagentaBold(s any) string { + return Colorize(Colorize(s, MAGENTA), BOLD) +} + +// Cyan is a ColorFunc that returns a cyan-colorized string of the provided input +func Cyan(s any) string { + return Colorize(s, CYAN) +} + +// CyanBold is a ColorFunc that returns a cyan-bold-colorized string of the provided input +func CyanBold(s any) string { + return Colorize(Colorize(s, CYAN), BOLD) +} + +// White is a ColorFunc that returns a white-colorized string of the provided input +func White(s any) string { + return Colorize(s, WHITE) +} + +// WhiteBold is a ColorFunc that returns a white-bold-colorized string of the provided input +func WhiteBold(s any) string { + return Colorize(Colorize(s, WHITE), BOLD) +} + +// Bold is a ColorFunc that returns a bolded string of the provided input +func Bold(s any) string { + return Colorize(s, BOLD) +} + +// DarkGray is a ColorFunc that returns a dark-gray-colorized string of the provided input +func DarkGray(s any) string { + return Colorize(s, DARK_GRAY) +} + +// DarkGrayBold is a ColorFunc that returns a dark-gray-bold-colorized string of the provided input +func DarkGrayBold(s any) string { + return Colorize(Colorize(s, DARK_GRAY), BOLD) +} diff --git a/logging/colors/colorize_unix.go b/logging/colors/colorize_unix.go new file mode 100644 index 00000000..89ea510d --- /dev/null +++ b/logging/colors/colorize_unix.go @@ -0,0 +1,15 @@ +//go:build !windows +// +build !windows + +package colors + +import "fmt" + +// EnableColor is a no-op function for non-windows systems because we know that they support ANSI escape codes +func EnableColor() {} + +// Colorize returns the string s wrapped in ANSI code c for non-windows systems +// Source: https://github.com/rs/zerolog/blob/4fff5db29c3403bc26dee9895e12a108aacc0203/console.go +func Colorize(s any, c Color) string { + return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s) +} diff --git a/logging/colors/colorize_windows.go b/logging/colors/colorize_windows.go new file mode 100644 index 00000000..87266688 --- /dev/null +++ b/logging/colors/colorize_windows.go @@ -0,0 +1,66 @@ +//go:build windows +// +build windows + +package colors + +import ( + "fmt" + "golang.org/x/sys/windows" + "os" +) + +var enabled bool + +// EnableColor will make a kernel call to enable ANSI escape codes on both stdout and stderr. Note that if enablement +// on either stream fails, then coloring will not be enabled. +func EnableColor() { + var mode uint32 + fds := []uintptr{os.Stdout.Fd(), os.Stderr.Fd()} + + // Iterate across each file descriptor and enable coloring + for _, fd := range fds { + // Obtain our current console mode. + consoleHandle := windows.Handle(fd) + err := windows.GetConsoleMode(consoleHandle, &mode) + if err != nil { + enabled = false + return + } + + // If color is not enabled, try to enable it. + if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0 { + err = windows.SetConsoleMode(consoleHandle, mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) + if err != nil { + enabled = false + return + } + } + + // Fetch the console mode once more + err = windows.GetConsoleMode(consoleHandle, &mode) + if err != nil { + enabled = false + return + } + + // Set our enabled status after trying to enable it. + enabled = mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 + + // If we failed to enable on this file descriptor, exit out + if !enabled { + return + } + } +} + +// Colorize returns the string s wrapped in ANSI code c assuming that ANSI is supported on the Windows version +// Source: https://github.com/rs/zerolog/blob/4fff5db29c3403bc26dee9895e12a108aacc0203/console.go +func Colorize(s any, c Color) string { + // If ANSI is not supported then just return the original string + if !enabled { + return fmt.Sprintf("%v", s) + } + + // Otherwise, returned an ANSI-wrapped string + return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s) +} diff --git a/logging/colors/constants.go b/logging/colors/constants.go new file mode 100644 index 00000000..c1a7657a --- /dev/null +++ b/logging/colors/constants.go @@ -0,0 +1,34 @@ +package colors + +type Color int + +// This is taken from zerolog's repo and will be used to colorize log output +// Source: https://github.com/rs/zerolog/blob/4fff5db29c3403bc26dee9895e12a108aacc0203/console.go +const ( + // BLACK is the ANSI code for black + BLACK Color = iota + 30 + // COLOR_RED is the ANSI code for red + RED + // GREEN is the ANSI code for green + GREEN + // YELLOW is the ANSI code for yellow + YELLOW + // BLUE is the ANSI code for blue + BLUE + // MAGENTA is the ANSI code for magenta + MAGENTA + // CYAN is the ANSI code for cyan + CYAN + // WHITE is the ANSI code for white + WHITE + // BOLD is the ANSI code for bold tet + BOLD = 1 + // DARK_GRAY is the ANSI code for dark gray + DARK_GRAY = 90 +) + +// This enum is to identify special unicode characters that will be used for pretty console output +const ( + // LEFT_ARROW is the unicode string for a left arrow glyph + LEFT_ARROW = "\u21fe" +) diff --git a/logging/colors/init.go b/logging/colors/init.go new file mode 100644 index 00000000..758c7d4f --- /dev/null +++ b/logging/colors/init.go @@ -0,0 +1,7 @@ +package colors + +// init will ensure that ANSI coloring is enabled on Windows and Unix systems. Note that ANSI coloring is enabled by +// default on Unix system and Windows needs specific kernel calls for enablement +func init() { + EnableColor() +} diff --git a/logging/log_buffer.go b/logging/log_buffer.go new file mode 100644 index 00000000..70abf4a4 --- /dev/null +++ b/logging/log_buffer.go @@ -0,0 +1,33 @@ +package logging + +// LogBuffer is a helper object that can be used to buffer log messages. A log buffer is effectively a list of arguments +// of any type. This object is especially useful when attempting to log complex objects (e.g. execution trace) that have +// complex coloring schemes and formatting. The LogBuffer can then be passed on to a Logger object to then log the buffer +// to console and any other writers (e.g. file). +type LogBuffer struct { + // elements describes the list of elements that eventually need to be concatenated together in the Logger + elements []any +} + +// NewLogBuffer creates a new LogBuffer object +func NewLogBuffer() *LogBuffer { + return &LogBuffer{ + elements: make([]any, 0), + } +} + +// Append appends a variadic set of elements to the list of elements +func (l *LogBuffer) Append(newElements ...any) { + l.elements = append(l.elements, newElements...) +} + +// Elements returns the list of elements stored in this LogBuffer +func (l *LogBuffer) Elements() []any { + return l.elements +} + +// String provides the non-colorized string representation of the LogBuffer +func (l LogBuffer) String() string { + _, msg, _, _ := buildMsgs(l.elements) + return msg +} diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 00000000..6385d182 --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,347 @@ +package logging + +import ( + "fmt" + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" + "io" + "os" + "strings" +) + +// GlobalLogger describes a Logger that is disabled by default and is instantiated when the fuzzer is created. Each module/package +// should create its own sub-logger. This allows to create unique logging instances depending on the use case. +var GlobalLogger = NewLogger(zerolog.Disabled, false, nil) + +// Logger describes a custom logging object that can log events to any arbitrary channel and can handle specialized +// output to console as well +type Logger struct { + // level describes the log level + level zerolog.Level + + // multiLogger describes a logger that will be used to output logs to any arbitrary channel(s) in either structured + // or unstructured format. + multiLogger zerolog.Logger + + // consoleLogger describes a logger that will be used to output unstructured output to console. + // We are creating a separate logger for console so that we can support specialized formatting / custom coloring. + consoleLogger zerolog.Logger + + // writers describes a list of io.Writer objects where log output will go. This writers list can be appended to / + // removed from. + writers []io.Writer +} + +// LogFormat describes what format to log in +type LogFormat string + +const ( + // STRUCTURED describes that logging should be done in structured JSON format + STRUCTURED LogFormat = "structured" + // UNSTRUCTRED describes that logging should be done in an unstructured format + UNSTRUCTURED LogFormat = "unstructured" +) + +// StructuredLogInfo describes a key-value mapping that can be used to log structured data +type StructuredLogInfo map[string]any + +// NewLogger will create a new Logger object with a specific log level. The Logger can output to console, if enabled, +// and output logs to any number of arbitrary io.Writer channels +func NewLogger(level zerolog.Level, consoleEnabled bool, writers ...io.Writer) *Logger { + // The two base loggers are effectively loggers that are disabled + // We are creating instances of them so that we do not get nil pointer dereferences down the line + baseMultiLogger := zerolog.New(os.Stdout).Level(zerolog.Disabled) + baseConsoleLogger := zerolog.New(os.Stdout).Level(zerolog.Disabled) + + // If we are provided a list of writers, update the multi logger + if len(writers) > 0 { + baseMultiLogger = zerolog.New(zerolog.MultiLevelWriter(writers...)).Level(level).With().Timestamp().Logger() + } + + // If console logging is enabled, update the console logger + if consoleEnabled { + consoleWriter := setupDefaultFormatting(zerolog.ConsoleWriter{Out: os.Stdout}, level) + baseConsoleLogger = zerolog.New(consoleWriter).Level(level) + } + + return &Logger{ + level: level, + multiLogger: baseMultiLogger, + consoleLogger: baseConsoleLogger, + writers: writers, + } +} + +// NewSubLogger will create a new Logger with unique context in the form of a key-value pair. The expected use of this +// function is for each package to have their own unique logger so that parsing of logs is "grep-able" based on some key +func (l *Logger) NewSubLogger(key string, value string) *Logger { + subFileLogger := l.multiLogger.With().Str(key, value).Logger() + subConsoleLonger := l.consoleLogger.With().Str(key, value).Logger() + return &Logger{ + level: l.level, + multiLogger: subFileLogger, + consoleLogger: subConsoleLonger, + writers: l.writers, + } +} + +// AddWriter will add a writer to the list of channels where log output will be sent. +func (l *Logger) AddWriter(writer io.Writer, format LogFormat) { + // Check to see if the writer is already in the array of writers + for _, w := range l.writers { + if writer == w { + return + } + } + + // If we want unstructured output, wrap the base writer object into a console writer so that we get unstructured output with no ANSI coloring + if format == UNSTRUCTURED { + writer = zerolog.ConsoleWriter{Out: writer, NoColor: true} + } + + // Add it to the list of writers and update the multi logger + l.writers = append(l.writers, writer) + l.multiLogger = zerolog.New(zerolog.MultiLevelWriter(l.writers...)).Level(l.level).With().Timestamp().Logger() +} + +// RemoveWriter will remove a writer from the list of writers that the logger manages. If the writer does not exist, this +// function is a no-op +func (l *Logger) RemoveWriter(writer io.Writer) { + // Iterate through the writers + for i, w := range l.writers { + if writer == w { + // Create a new slice without the writer at index i + l.writers = append(l.writers[:i], l.writers[i+1]) + } + } +} + +// Level will get the log level of the Logger +func (l *Logger) Level() zerolog.Level { + return l.level +} + +// SetLevel will update the log level of the Logger +func (l *Logger) SetLevel(level zerolog.Level) { + l.level = level + l.multiLogger = l.multiLogger.Level(level) + l.consoleLogger = l.consoleLogger.Level(level) +} + +// Trace is a wrapper function that will log a trace event +func (l *Logger) Trace(args ...any) { + // Build the messages and retrieve any error or associated structured log info + consoleMsg, multiMsg, err, info := buildMsgs(args...) + + // Instantiate log events + consoleLog := l.consoleLogger.Trace() + multiLog := l.multiLogger.Trace() + + // Chain the error + chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + + // Chain the structured log info and messages and send off the logs + chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) +} + +// Debug is a wrapper function that will log a debug event +func (l *Logger) Debug(args ...any) { + // Build the messages and retrieve any error or associated structured log info + consoleMsg, multiMsg, err, info := buildMsgs(args...) + + // Instantiate log events + consoleLog := l.consoleLogger.Debug() + multiLog := l.multiLogger.Debug() + + // Chain the error + chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + + // Chain the structured log info and messages and send off the logs + chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) +} + +// Info is a wrapper function that will log an info event +func (l *Logger) Info(args ...any) { + // Build the messages and retrieve any error or associated structured log info + consoleMsg, multiMsg, err, info := buildMsgs(args...) + + // Instantiate log events + consoleLog := l.consoleLogger.Info() + multiLog := l.multiLogger.Info() + + // Chain the error + chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + + // Chain the structured log info and messages and send off the logs + chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) +} + +// Warn is a wrapper function that will log a warning event both on console +func (l *Logger) Warn(args ...any) { + // Build the messages and retrieve any error or associated structured log info + consoleMsg, multiMsg, err, info := buildMsgs(args...) + + // Instantiate log events + consoleLog := l.consoleLogger.Warn() + multiLog := l.multiLogger.Warn() + + // Chain the error + chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + + // Chain the structured log info and messages and send off the logs + chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) +} + +// Error is a wrapper function that will log an error event. +func (l *Logger) Error(args ...any) { + // Build the messages and retrieve any error or associated structured log info + consoleMsg, multiMsg, err, info := buildMsgs(args...) + + // Instantiate log events + consoleLog := l.consoleLogger.Error() + multiLog := l.multiLogger.Error() + + // Chain the error + chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + + // Chain the structured log info and messages and send off the logs + chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) +} + +// Panic is a wrapper function that will log a panic event +func (l *Logger) Panic(args ...any) { + // Build the messages and retrieve any error or associated structured log info + consoleMsg, multiMsg, err, info := buildMsgs(args...) + + // Instantiate log events + consoleLog := l.consoleLogger.Panic() + multiLog := l.multiLogger.Panic() + + // Chain the error + chainError(consoleLog, multiLog, err, true) + + // Chain the structured log info and messages and send off the logs + chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) +} + +// buildMsgs describes a function that takes in a variadic list of arguments of any type and returns two strings and, +// optionally, an error and a StructuredLogInfo object. The first string will be a colorized-string that can be used for +// console logging while the second string will be a non-colorized one that can be used for file/structured logging. +// The error and the StructuredLogInfo can be used to add additional context to log messages +func buildMsgs(args ...any) (string, string, error, StructuredLogInfo) { + // Guard clause + if len(args) == 0 { + return "", "", nil, nil + } + + // Initialize the base color context, the string buffers and the structured log info object + colorCtx := colors.Reset + consoleOutput := make([]string, 0) + fileOutput := make([]string, 0) + var info StructuredLogInfo + var err error + + // Iterate through each argument in the list and switch on type + for _, arg := range args { + switch t := arg.(type) { + case colors.ColorFunc: + // If the argument is a color function, switch the current color context + colorCtx = t + case StructuredLogInfo: + // Note that only one structured log info can be provided for each log message + info = t + case error: + // Note that only one error can be provided for each log message + err = t + default: + // In the base case, append the object to the two string buffers. The console string buffer will have the + // current color context applied to it. + consoleOutput = append(consoleOutput, colorCtx(t)) + fileOutput = append(fileOutput, fmt.Sprintf("%v", t)) + } + } + + return strings.Join(consoleOutput, ""), strings.Join(fileOutput, ""), err, info +} + +// chainError is a helper function that takes in a *zerolog.Event for console and multi-log output and chains an error +// to both events. If debug is true, then a stack trace is added to both events as well. +func chainError(consoleLog *zerolog.Event, multiLog *zerolog.Event, err error, debug bool) { + // First append the errors to each event. Note that even if err is nil, there will not be a panic here + consoleLog.Err(err) + multiLog.Err(err) + + // If we are in debug mode or below, then we will add the stack traces as well for debugging + if debug { + consoleLog.Stack() + multiLog.Stack() + } +} + +// chainStructuredLogInfoAndMsgs is a helper function that takes in a *zerolog.Event for console and multi-log output, +// chains any StructuredLogInfo provided to it, adds the associated messages, and sends out the logs to their respective +// channels. +func chainStructuredLogInfoAndMsgs(consoleLog *zerolog.Event, multiLog *zerolog.Event, info StructuredLogInfo, consoleMsg string, multiMsg string) { + // If we are provided a structured log info object, add that as a key-value pair to the events + if info != nil { + consoleLog.Any("info", info) + multiLog.Any("info", info) + } + + // Append the messages to each event. This will also result in the log events being sent out to their respective + // streams. Note that we are deferring the msg to multi logger in case we are logging a panic and want to make sure that + // all channels receive the panic log + defer multiLog.Msg(multiMsg) + consoleLog.Msg(consoleMsg) +} + +// setupDefaultFormatting will update the console logger's formatting to the medusa standard +func setupDefaultFormatting(writer zerolog.ConsoleWriter, level zerolog.Level) zerolog.ConsoleWriter { + // Get rid of the timestamp for console output + writer.FormatTimestamp = func(i interface{}) string { + return "" + } + + // We will define a custom format for each level + writer.FormatLevel = func(i any) string { + // Create a level object for better switch logic + level, err := zerolog.ParseLevel(i.(string)) + if err != nil { + panic(fmt.Sprintf("unable to parse the log level: %v", err)) + } + + // Switch on the level and return a custom, colored string + switch level { + case zerolog.TraceLevel: + // Return a bold, cyan "trace" string + return colors.CyanBold(zerolog.LevelTraceValue) + case zerolog.DebugLevel: + // Return a bold, blue "debug" string + return colors.BlueBold(zerolog.LevelDebugValue) + case zerolog.InfoLevel: + // Return a bold, green left arrow + return colors.GreenBold(colors.LEFT_ARROW) + case zerolog.WarnLevel: + // Return a bold, yellow "warn" string + return colors.YellowBold(zerolog.LevelWarnValue) + case zerolog.ErrorLevel: + // Return a bold, red "err" string + return colors.RedBold(zerolog.LevelErrorValue) + case zerolog.FatalLevel: + // Return a bold, red "fatal" string + return colors.RedBold(zerolog.LevelFatalValue) + case zerolog.PanicLevel: + // Return a bold, red "panic" string + return colors.RedBold(zerolog.LevelPanicValue) + default: + return i.(string) + } + } + + // If we are above debug level, we want to get rid of the `module` component when logging to console + if level > zerolog.DebugLevel { + writer.FieldsExclude = []string{"module"} + } + + return writer +} diff --git a/main.go b/main.go index 2c8c7295..4902eaeb 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "github.com/crytic/medusa/cmd" "os" ) @@ -10,10 +9,7 @@ func main() { // Run our root CLI command, which contains all underlying command logic and will handle parsing/invocation. err := cmd.Execute() - // Print any error we encountered if err != nil { - // TODO: Replace this when we have an appropriate logger in place. - fmt.Printf("ERROR:\n%s", err.Error()) os.Exit(1) } } diff --git a/utils/fs_utils.go b/utils/fs_utils.go index f515b243..df656bc6 100644 --- a/utils/fs_utils.go +++ b/utils/fs_utils.go @@ -2,11 +2,38 @@ package utils import ( "fmt" + "github.com/pkg/errors" "io" "os" "path/filepath" ) +// CreateFile will create a file at the given path and file name combination. If the path is the empty string, the +// file will be created in the current working directory +func CreateFile(path string, fileName string) (*os.File, error) { + // By default, the path will be the name of the file + filePath := fileName + + // Check to see if the file needs to be created in another directory or the working directory + if path != "" { + // Make the directory, if it does not exist already + err := MakeDirectory(path) + if err != nil { + return nil, err + } + // Since the path is non-empty, concatenate the path with the name of the file + filePath = filepath.Join(path, fileName) + } + + // Create the file + file, err := os.Create(filePath) + if err != nil { + return nil, errors.WithStack(err) + } + + return file, nil +} + // CopyFile copies a file from a source path to a destination path. File permissions are retained. Returns an error // if one occurs. func CopyFile(sourcePath string, targetPath string) error { diff --git a/utils/randomutils/fork_random.go b/utils/randomutils/fork_random.go index 071da2d3..192b68d6 100644 --- a/utils/randomutils/fork_random.go +++ b/utils/randomutils/fork_random.go @@ -2,6 +2,7 @@ package randomutils import ( "encoding/binary" + "github.com/crytic/medusa/logging" "math/rand" ) @@ -13,7 +14,7 @@ func ForkRandomProvider(randomProvider *rand.Rand) *rand.Rand { b := make([]byte, 8) _, err := randomProvider.Read(b) if err != nil { - panic(err) + logging.GlobalLogger.Panic("Failed to fork random child provider", err) } // Return a new random provider with our derived seed. diff --git a/utils/reflectionutils/reflected_type_utils.go b/utils/reflectionutils/reflected_type_utils.go index a3ffd48e..34e277b5 100644 --- a/utils/reflectionutils/reflected_type_utils.go +++ b/utils/reflectionutils/reflected_type_utils.go @@ -2,6 +2,7 @@ package reflectionutils import ( "fmt" + "github.com/crytic/medusa/logging" "reflect" ) @@ -54,7 +55,9 @@ func CopyReflectedType(reflectedValue reflect.Value) reflect.Value { } return newStruct } - panic("failed to copy reflected value, type not supported") + + logging.GlobalLogger.Panic("Failed to copy reflected value", fmt.Errorf("type not supported")) + return reflectedValue } // GetReflectedArrayValues obtains the values of each element of a reflected array or slice variable. @@ -71,7 +74,9 @@ func GetReflectedArrayValues(reflectedArray reflect.Value) []any { } return values } - panic("failed to get reflected array values, type not supported") + + logging.GlobalLogger.Panic("Failed to get reflected array values", fmt.Errorf("type not supported")) + return nil } // SetReflectedArrayValues takes an array or slice of the same length as the values provided, and sets each element @@ -93,5 +98,7 @@ func SetReflectedArrayValues(reflectedArray reflect.Value, values []any) error { } return nil } - panic("failed to set reflected array values, type not supported") + + logging.GlobalLogger.Panic("Failed to set reflected array values", fmt.Errorf("type not supported")) + return nil } From f58c60a86afe367cee7e34576cba8f31612fb56f Mon Sep 17 00:00:00 2001 From: anishnaik Date: Mon, 17 Jul 2023 16:43:27 -0400 Subject: [PATCH 018/109] Update version to 0.1.1 (#178) --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index cfe2a15e..9ec81398 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "io" ) -const version = "0.1.0" +const version = "0.1.1" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From 9b2e19a613bfb728538d2c130da8a4e52b018079 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 1 Aug 2023 14:59:04 -0400 Subject: [PATCH 019/109] Fix nil dereference when calling `SetTarget` (#187) Fix bug with `medusa fuzz` command where compilation platform changes after the call to `medusa init` --- compilation/compilation_config.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/compilation/compilation_config.go b/compilation/compilation_config.go index 9f676f29..6f56bc88 100644 --- a/compilation/compilation_config.go +++ b/compilation/compilation_config.go @@ -52,11 +52,6 @@ func NewCompilationConfigFromPlatformConfig(platformConfig platforms.PlatformCon // is then used to compile the underlying targets. Returns a list of compilations returned by the platform provider or // an error. Command-line input may also be returned in either case., func (c *CompilationConfig) Compile() ([]types.Compilation, string, error) { - // Verify the platform is valid - if !IsSupportedCompilationPlatform(c.Platform) { - return nil, "", fmt.Errorf("could not compile from configs: platform '%s' is unsupported", c.Platform) - } - // Get the platform config platformConfig, err := c.GetPlatformConfig() if err != nil { @@ -69,6 +64,16 @@ func (c *CompilationConfig) Compile() ([]types.Compilation, string, error) { // GetPlatformConfig will return the de-serialized version of platforms.PlatformConfig for a given CompilationConfig func (c *CompilationConfig) GetPlatformConfig() (platforms.PlatformConfig, error) { + // Ensure that the platform is non-empty + if c.Platform == "" { + return nil, fmt.Errorf("must specify a platform for compilation") + } + + // Ensure that the platform is supported + if !IsSupportedCompilationPlatform(c.Platform) { + return nil, fmt.Errorf("compilation platform '%v' is unsupported", c.Platform) + } + // Allocate a platform config given our platform string in our compilation config // It is necessary to do so as json.Unmarshal needs a concrete structure to populate platformConfig := GetDefaultPlatformConfig(c.Platform) @@ -87,7 +92,10 @@ func (c *CompilationConfig) SetPlatformConfig(platformConfig platforms.PlatformC return errors.New("platformConfig must be non-nil") } - // Update platform + // Update platform, assuming the platform is supported + if !IsSupportedCompilationPlatform(platformConfig.Platform()) { + return fmt.Errorf("compilation platform '%v' is unsupported", platformConfig.Platform()) + } c.Platform = platformConfig.Platform() // Serialize From 2917efcf985a46311960668ec60f8dbac41b6074 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 3 Aug 2023 15:13:35 -0400 Subject: [PATCH 020/109] update log level for invalid corpus item (#194) --- fuzzing/corpus/corpus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 827a3f58..d8c4d479 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -222,7 +222,7 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe } c.unexecutedCallSequences = append(c.unexecutedCallSequences, sequence) } else { - c.logger.Warn("Corpus item ", colors.Bold, sequenceFileData.fileName, colors.Reset, " disabled due to error when replaying it", sequenceInvalidError) + c.logger.Debug("Corpus item ", colors.Bold, sequenceFileData.fileName, colors.Reset, " disabled due to error when replaying it", sequenceInvalidError) } // Revert chain state to our starting point to test the next sequence. From cd8605e4be0391ab2260cee000045c935dd80091 Mon Sep 17 00:00:00 2001 From: David Pokora Date: Fri, 11 Aug 2023 10:58:38 -0400 Subject: [PATCH 021/109] Update to go-ethereum 1.12.0 (#199) * Updated CallMessage to be compliant with core.Message made in new go-ethereum --------- Co-authored-by: anishnaik --- chain/test_chain.go | 18 +- chain/test_chain_test.go | 64 ++++++- chain/types/block.go | 4 +- chain/vendored/apply_transaction.go | 4 +- compilation/types/source_maps.go | 2 +- fuzzing/calls/call_message.go | 197 +++++++++++--------- fuzzing/calls/call_sequence.go | 16 +- fuzzing/calls/call_sequence_execution.go | 2 +- fuzzing/calls/gen_call_message_json.go | 113 ++++++----- fuzzing/corpus/corpus.go | 8 +- fuzzing/corpus/corpus_test.go | 18 +- fuzzing/executiontracer/execution_tracer.go | 2 +- fuzzing/fuzzer.go | 3 +- fuzzing/fuzzer_worker.go | 23 ++- fuzzing/fuzzer_worker_sequence_generator.go | 8 +- fuzzing/test_case_optimization_provider.go | 4 +- fuzzing/test_case_property_provider.go | 4 +- go.mod | 13 +- go.sum | 56 +----- utils/message_transaction_utils.go | 14 +- 20 files changed, 313 insertions(+), 260 deletions(-) diff --git a/chain/test_chain.go b/chain/test_chain.go index ec60979c..a4466cc7 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -89,6 +89,14 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh return nil, err } + // TODO: go-ethereum doesn't set shanghai start time for THEIR test `ChainConfig` struct. + // Note: We have our own `TestChainConfig` definition that is different (second argument in this function). + // We should allow the user to provide a go-ethereum `ChainConfig` to do custom fork selection, inside of our + // `TestChainConfig` definition. Or we should wrap it in our own struct to simplify the options and not pollute + // our overall medusa project config. + shanghaiTime := uint64(0) + chainConfig.ShanghaiTime = &shanghaiTime + // Create our genesis definition with our default chain config. genesisDefinition := &core.Genesis{ Config: chainConfig, @@ -527,7 +535,7 @@ func (t *TestChain) RevertToBlockNumber(blockNumber uint64) error { // It takes an optional state argument, which is the state to execute the message over. If not provided, the // current pending state (or committed state if none is pending) will be used instead. // The state executed over may be a pending block state. -func (t *TestChain) CallContract(msg core.Message, state *state.StateDB, additionalTracers ...vm.EVMLogger) (*core.ExecutionResult, error) { +func (t *TestChain) CallContract(msg *core.Message, state *state.StateDB, additionalTracers ...vm.EVMLogger) (*core.ExecutionResult, error) { // If our provided state is nil, use our current chain state. if state == nil { state = t.state @@ -537,7 +545,7 @@ func (t *TestChain) CallContract(msg core.Message, state *state.StateDB, additio snapshot := state.Snapshot() // Set infinite balance to the fake caller account - from := state.GetOrNewStateObject(msg.From()) + from := state.GetOrNewStateObject(msg.From) from.SetBalance(math.MaxBig256) // Create our transaction and block contexts for the vm @@ -552,7 +560,7 @@ func (t *TestChain) CallContract(msg core.Message, state *state.StateDB, additio // Create our EVM instance. evm := vm.NewEVM(blockContext, txContext, state, t.chainConfig, vm.Config{ - Debug: true, + //Debug: true, Tracer: extendedTracerRouter, NoBaseFee: true, ConfigExtensions: t.vmConfigExtensions, @@ -672,7 +680,7 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi // PendingBlockAddTx takes a message (internal txs) and adds it to the current pending block, updating the header // with relevant execution information. If a pending block was not created, an error is returned. // Returns the constructed block, or an error if one occurred. -func (t *TestChain) PendingBlockAddTx(message core.Message) error { +func (t *TestChain) PendingBlockAddTx(message *core.Message) error { // If we don't have a pending block, return an error if t.pendingBlock == nil { return errors.New("could not add tx to the chain's pending block because no pending block was created") @@ -692,7 +700,7 @@ func (t *TestChain) PendingBlockAddTx(message core.Message) error { // Create our EVM instance. evm := vm.NewEVM(blockContext, core.NewEVMTxContext(message), t.state, t.chainConfig, vm.Config{ - Debug: true, + //Debug: true, Tracer: t.transactionTracerRouter, NoBaseFee: true, ConfigExtensions: t.vmConfigExtensions, diff --git a/chain/test_chain_test.go b/chain/test_chain_test.go index bb049d1c..5c8e6449 100644 --- a/chain/test_chain_test.go +++ b/chain/test_chain_test.go @@ -241,14 +241,26 @@ func TestChainDynamicDeployments(t *testing.T) { // Deploy the currently indexed contract next // Create a message to represent our contract deployment. - msg := types.NewMessage(senders[0], nil, chain.State().GetNonce(senders[0]), big.NewInt(0), chain.BlockGasLimit, big.NewInt(1), big.NewInt(0), big.NewInt(0), contract.InitBytecode, nil, false) + msg := core.Message{ + To: nil, + From: senders[0], + Nonce: chain.State().GetNonce(senders[0]), + Value: big.NewInt(0), + GasLimit: chain.BlockGasLimit, + GasPrice: big.NewInt(1), + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + Data: contract.InitBytecode, + AccessList: nil, + SkipAccountChecks: false, + } // Create a new pending block we'll commit to chain block, err := chain.PendingBlockCreate() assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(msg) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -354,14 +366,26 @@ func TestChainDeploymentWithArgs(t *testing.T) { assert.NoError(t, err) // Create a message to represent our contract deployment. - msg := types.NewMessage(senders[0], nil, chain.State().GetNonce(senders[0]), big.NewInt(0), chain.BlockGasLimit, big.NewInt(1), big.NewInt(0), big.NewInt(0), msgData, nil, false) + msg := core.Message{ + To: nil, + From: senders[0], + Nonce: chain.State().GetNonce(senders[0]), + Value: big.NewInt(0), + GasLimit: chain.BlockGasLimit, + GasPrice: big.NewInt(1), + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + Data: msgData, + AccessList: nil, + SkipAccountChecks: false, + } // Create a new pending block we'll commit to chain block, err := chain.PendingBlockCreate() assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(msg) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -451,14 +475,26 @@ func TestChainCloning(t *testing.T) { // Deploy the currently indexed contract next // Create a message to represent our contract deployment. - msg := types.NewMessage(senders[0], nil, chain.State().GetNonce(senders[0]), big.NewInt(0), chain.BlockGasLimit, big.NewInt(1), big.NewInt(0), big.NewInt(0), contract.InitBytecode, nil, false) + msg := core.Message{ + To: nil, + From: senders[0], + Nonce: chain.State().GetNonce(senders[0]), + Value: big.NewInt(0), + GasLimit: chain.BlockGasLimit, + GasPrice: big.NewInt(1), + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + Data: contract.InitBytecode, + AccessList: nil, + SkipAccountChecks: false, + } // Create a new pending block we'll commit to chain block, err := chain.PendingBlockCreate() assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(msg) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -533,14 +569,26 @@ func TestChainCallSequenceReplayMatchSimple(t *testing.T) { if len(contract.Abi.Constructor.Inputs) == 0 { for i := 0; i < 10; i++ { // Create a message to represent our contract deployment. - msg := types.NewMessage(senders[0], nil, chain.State().GetNonce(senders[0]), big.NewInt(0), chain.BlockGasLimit, big.NewInt(1), big.NewInt(0), big.NewInt(0), contract.InitBytecode, nil, false) + msg := core.Message{ + To: nil, + From: senders[0], + Nonce: chain.State().GetNonce(senders[0]), + Value: big.NewInt(0), + GasLimit: chain.BlockGasLimit, + GasPrice: big.NewInt(1), + GasFeeCap: big.NewInt(0), + GasTipCap: big.NewInt(0), + Data: contract.InitBytecode, + AccessList: nil, + SkipAccountChecks: false, + } // Create a new pending block we'll commit to chain block, err := chain.PendingBlockCreate() assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(msg) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. diff --git a/chain/types/block.go b/chain/types/block.go index 47638e19..754f81ff 100644 --- a/chain/types/block.go +++ b/chain/types/block.go @@ -18,7 +18,7 @@ type Block struct { // of a transaction occurs and can be thought of as an internal EVM transaction. It contains typical transaction // fields plainly (e.g., no transaction signature is included, the sender is derived and simply supplied as a field // in a message). - Messages []core.Message + Messages []*core.Message // MessageResults represents the results recorded while executing transactions. MessageResults []*MessageResults @@ -30,7 +30,7 @@ func NewBlock(header *types.Header) *Block { block := &Block{ Hash: header.Hash(), Header: header, - Messages: make([]core.Message, 0), + Messages: make([]*core.Message, 0), MessageResults: make([]*MessageResults, 0), } return block diff --git a/chain/vendored/apply_transaction.go b/chain/vendored/apply_transaction.go index e9fab99e..2044bf02 100644 --- a/chain/vendored/apply_transaction.go +++ b/chain/vendored/apply_transaction.go @@ -35,7 +35,7 @@ import ( // This executes on an underlying EVM and returns a transaction receipt, or an error if one occurs. // Additional changes: // - Exposed core.ExecutionResult as a return value. -func EVMApplyTransaction(msg Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, *ExecutionResult, error) { +func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, *ExecutionResult, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) @@ -67,7 +67,7 @@ func EVMApplyTransaction(msg Message, config *params.ChainConfig, author *common receipt.GasUsed = result.UsedGas // If the transaction created a contract, store the creation address in the receipt. - if msg.To() == nil { + if msg.To == nil { receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) } diff --git a/compilation/types/source_maps.go b/compilation/types/source_maps.go index 9804733d..c5a92ad0 100644 --- a/compilation/types/source_maps.go +++ b/compilation/types/source_maps.go @@ -170,7 +170,7 @@ func (s SourceMap) GetInstructionIndexToOffsetLookup(bytecode []byte) ([]int, er operandCount := 0 if op.IsPush() { if op == vm.PUSH0 { - operandCount = 1 + operandCount = 0 } else { operandCount = int(op) - int(vm.PUSH1) + 1 } diff --git a/fuzzing/calls/call_message.go b/fuzzing/calls/call_message.go index de3a856f..47dc0263 100644 --- a/fuzzing/calls/call_message.go +++ b/fuzzing/calls/call_message.go @@ -5,6 +5,7 @@ import ( "github.com/crytic/medusa/logging" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" coreTypes "github.com/ethereum/go-ethereum/core/types" "golang.org/x/exp/slices" "math/big" @@ -15,91 +16,113 @@ import ( //go:generate go get github.com/fjl/gencodec //go:generate go run github.com/fjl/gencodec -type CallMessage -field-override callMessageMarshaling -out gen_call_message_json.go -// CallMessage implements Ethereum's coreTypes.Message, used to apply EVM/state updates. +// CallMessage implements and extends Ethereum's coreTypes.Message, used to apply EVM/state updates. type CallMessage struct { - // MsgFrom represents a core.Message's from parameter (sender), indicating who sent a transaction/message to the + // From represents a core.Message's from parameter (sender), indicating who sent a transaction/message to the // Ethereum core to apply a state update. - MsgFrom common.Address `json:"from"` + From common.Address `json:"from"` - // MsgTo represents the receiving address for a given core.Message. - MsgTo *common.Address `json:"to"` + // To represents the receiving address for a given core.Message. + To *common.Address `json:"to"` - // MsgNonce represents the core.Message sender's nonce - MsgNonce uint64 `json:"nonce"` + // Nonce represents the core.Message sender's nonce + Nonce uint64 `json:"nonce"` - // MsgValue represents ETH value to be sent to the receiver of the message. - MsgValue *big.Int `json:"value"` + // Value represents ETH value to be sent to the receiver of the message. + Value *big.Int `json:"value"` - // MsgGas represents the maximum amount of gas the sender is willing to spend to cover the cost of executing the + // GasLimit represents the maximum amount of gas the sender is willing to spend to cover the cost of executing the // message or transaction. - MsgGas uint64 `json:"gas"` + GasLimit uint64 `json:"gasLimit"` - // MsgGasPrice represents the price which the sender is willing to pay for each unit of gas used during execution + // GasPrice represents the price which the sender is willing to pay for each unit of gas used during execution // of the message. - MsgGasPrice *big.Int `json:"gasPrice"` + GasPrice *big.Int `json:"gasPrice"` - // MsgGasFeeCap represents the maximum fee to enforce for gas related costs (related to 1559 transaction executed). + // GasFeeCap represents the maximum fee to enforce for gas related costs (related to 1559 transaction executed). // The use of nil here indicates that the gas price oracle should be relied on instead. - MsgGasFeeCap *big.Int `json:"gasFeeCap"` + GasFeeCap *big.Int `json:"gasFeeCap"` - // MsgGasTipCap represents the fee cap to use for 1559 transaction. The use of nil here indicates that the gas price + // GasTipCap represents the fee cap to use for 1559 transaction. The use of nil here indicates that the gas price // oracle should be relied on instead. - MsgGasTipCap *big.Int `json:"gasTipCap"` + GasTipCap *big.Int `json:"gasTipCap"` - // MsgData represents the underlying message data to be sent to the receiver. If the receiver is a smart contract, + // Data represents the underlying message data to be sent to the receiver. If the receiver is a smart contract, // this will likely house your call parameters and other serialized data. If MsgDataAbiValues is non-nil, this // value is not used. - MsgData []byte `json:"data,omitempty"` + Data []byte `json:"data,omitempty"` - // MsgDataAbiValues represents the underlying message data to be sent to the receiver. If the receiver is a smart - // contract, this will likely house your call parameters and other serialized data. This overrides MsgData if it is + // DataAbiValues represents the underlying message data to be sent to the receiver. If the receiver is a smart + // contract, this will likely house your call parameters and other serialized data. This overrides Data if it is // set, allowing Data to be sourced from method ABI input arguments instead. - MsgDataAbiValues *CallMessageDataAbiValues `json:"dataAbiValues,omitempty"` + DataAbiValues *CallMessageDataAbiValues `json:"dataAbiValues,omitempty"` + + // AccessList represents a core.Message's AccessList parameter which represents the storage slots and contracts + // that will be accessed during the execution of this message. + AccessList coreTypes.AccessList + + // SkipAccountChecks represents a core.Message's SkipAccountChecks. If it is set to true, then the message nonce + // is not checked against the account nonce in state and will not verify if the sender is an EOA. + SkipAccountChecks bool } // callMessageMarshaling is a structure that overrides field types during JSON marshaling. It allows CallMessage to // have its custom marshaling methods auto-generated and will handle type conversions for serialization purposes. // For example, this enables serialization of big.Int but specifying a different field type to control serialization. type callMessageMarshaling struct { - MsgValue *hexutil.Big - MsgGasPrice *hexutil.Big - MsgGasFeeCap *hexutil.Big - MsgGasTipCap *hexutil.Big - MsgData hexutil.Bytes + Value *hexutil.Big + GasPrice *hexutil.Big + GasFeeCap *hexutil.Big + GasTipCap *hexutil.Big + Data hexutil.Bytes } // NewCallMessage instantiates a new call message from a given set of parameters, with call data set from bytes. func NewCallMessage(from common.Address, to *common.Address, nonce uint64, value *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte) *CallMessage { // Construct and return a new message from our given parameters. return &CallMessage{ - MsgFrom: from, - MsgTo: to, - MsgNonce: nonce, - MsgValue: value, - MsgGas: gasLimit, - MsgGasPrice: gasPrice, - MsgGasFeeCap: gasFeeCap, - MsgGasTipCap: gasTipCap, - MsgData: data, - MsgDataAbiValues: nil, + From: from, + To: to, + Nonce: nonce, + Value: value, + GasLimit: gasLimit, + GasPrice: gasPrice, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Data: data, + DataAbiValues: nil, + AccessList: nil, + SkipAccountChecks: false, } } // NewCallMessageWithAbiValueData instantiates a new call message from a given set of parameters, with call data set // from method ABI specified inputs. -func NewCallMessageWithAbiValueData(from common.Address, to *common.Address, nonce uint64, value *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data *CallMessageDataAbiValues) *CallMessage { +func NewCallMessageWithAbiValueData(from common.Address, to *common.Address, nonce uint64, value *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, abiData *CallMessageDataAbiValues) *CallMessage { + // Pack the ABI value data + var data []byte + var err error + if abiData != nil { + data, err = abiData.Pack() + if err != nil { + logging.GlobalLogger.Panic("Failed to pack call message ABI values", err) + } + } + // Construct and return a new message from our given parameters. return &CallMessage{ - MsgFrom: from, - MsgTo: to, - MsgNonce: nonce, - MsgValue: value, - MsgGas: gasLimit, - MsgGasPrice: gasPrice, - MsgGasFeeCap: gasFeeCap, - MsgGasTipCap: gasTipCap, - MsgData: nil, - MsgDataAbiValues: data, + From: from, + To: to, + Nonce: nonce, + Value: value, + GasLimit: gasLimit, + GasPrice: gasPrice, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Data: data, + DataAbiValues: abiData, + AccessList: nil, + SkipAccountChecks: false, } } @@ -107,68 +130,62 @@ func NewCallMessageWithAbiValueData(from common.Address, to *common.Address, non // underlying test chain properties if they are not yet set. func (m *CallMessage) FillFromTestChainProperties(chain *chain.TestChain) { // Set our nonce for this - m.MsgNonce = chain.State().GetNonce(m.MsgFrom) + m.Nonce = chain.State().GetNonce(m.From) // If a gas limit was not provided, allow the entire block gas limit to be used for this message. - if m.MsgGas == 0 { - m.MsgGas = chain.BlockGasLimit + if m.GasLimit == 0 { + m.GasLimit = chain.BlockGasLimit } // If a gas price was not provided, we use 1 as a default. - if m.MsgGasPrice == nil { - m.MsgGasPrice = big.NewInt(1) + if m.GasPrice == nil { + m.GasPrice = big.NewInt(1) } // Setting fee and tip cap to zero alongside the NoBaseFee for the vm.Config will bypass base fee validation. // TODO: Set this appropriately for newer transaction types. - m.MsgGasFeeCap = big.NewInt(0) - m.MsgGasTipCap = big.NewInt(0) + m.GasFeeCap = big.NewInt(0) + m.GasTipCap = big.NewInt(0) } -func (m *CallMessage) From() common.Address { return m.MsgFrom } -func (m *CallMessage) To() *common.Address { return m.MsgTo } -func (m *CallMessage) GasPrice() *big.Int { return m.MsgGasPrice } -func (m *CallMessage) GasFeeCap() *big.Int { return m.MsgGasFeeCap } -func (m *CallMessage) GasTipCap() *big.Int { return m.MsgGasTipCap } -func (m *CallMessage) Value() *big.Int { return m.MsgValue } -func (m *CallMessage) Gas() uint64 { return m.MsgGas } -func (m *CallMessage) Nonce() uint64 { return m.MsgNonce } -func (m *CallMessage) Data() []byte { - // If we have message data derived from ABI values, pack them and return the data. - if m.MsgDataAbiValues != nil { - data, err := m.MsgDataAbiValues.Pack() - if err != nil { - logging.GlobalLogger.Panic("Failed to pack call message ABI values", err) - } - return data - } - - // Otherwise we return our message data set from bytes. - return m.MsgData -} -func (m *CallMessage) AccessList() coreTypes.AccessList { return nil } -func (m *CallMessage) IsFake() bool { return true } - // Clone creates a copy of the given message and its underlying components, or an error if one occurs. func (m *CallMessage) Clone() (*CallMessage, error) { // Clone our underlying ABI values data if we have any. - clonedAbiValues, err := m.MsgDataAbiValues.Clone() + clonedAbiValues, err := m.DataAbiValues.Clone() if err != nil { return nil, err } // Create a message with the same data copied over. clone := &CallMessage{ - MsgFrom: m.MsgFrom, - MsgTo: m.MsgTo, // this value should be read-only, so we re-use it rather than cloning. - MsgNonce: m.MsgNonce, - MsgValue: new(big.Int).Set(m.MsgValue), - MsgGas: m.MsgGas, - MsgGasPrice: new(big.Int).Set(m.MsgGasPrice), - MsgGasFeeCap: new(big.Int).Set(m.MsgGasFeeCap), - MsgGasTipCap: new(big.Int).Set(m.MsgGasTipCap), - MsgData: slices.Clone(m.MsgData), - MsgDataAbiValues: clonedAbiValues, + From: m.From, + To: m.To, // this value should be read-only, so we re-use it rather than cloning. + Nonce: m.Nonce, + Value: new(big.Int).Set(m.Value), + GasLimit: m.GasLimit, + GasPrice: new(big.Int).Set(m.GasPrice), + GasFeeCap: new(big.Int).Set(m.GasFeeCap), + GasTipCap: new(big.Int).Set(m.GasTipCap), + Data: slices.Clone(m.Data), + DataAbiValues: clonedAbiValues, + AccessList: m.AccessList, + SkipAccountChecks: m.SkipAccountChecks, } return clone, nil } + +func (m *CallMessage) ToCoreMessage() *core.Message { + return &core.Message{ + To: m.To, + From: m.From, + Nonce: m.Nonce, + Value: new(big.Int).Set(m.Value), + GasLimit: m.GasLimit, + GasPrice: new(big.Int).Set(m.GasPrice), + GasFeeCap: new(big.Int).Set(m.GasFeeCap), + GasTipCap: new(big.Int).Set(m.GasTipCap), + Data: slices.Clone(m.Data), + AccessList: m.AccessList, + SkipAccountChecks: m.SkipAccountChecks, + } +} diff --git a/fuzzing/calls/call_sequence.go b/fuzzing/calls/call_sequence.go index 41ae2ccc..53ae874e 100644 --- a/fuzzing/calls/call_sequence.go +++ b/fuzzing/calls/call_sequence.go @@ -118,7 +118,7 @@ func (cs CallSequence) Hash() (common.Hash, error) { // Try to obtain a hash for the message/call. If this fails, we will replace it in the deferred panic // recovery. - messageHashData = utils.MessageToTransaction(cse.Call).Hash().Bytes() + messageHashData = utils.MessageToTransaction(cse.Call.ToCoreMessage()).Hash().Bytes() }() // Hash the message hash data. @@ -205,7 +205,7 @@ func (cse *CallSequenceElement) Method() (*abi.Method, error) { if cse.Contract == nil { return nil, nil } - return cse.Contract.CompiledContract().Abi.MethodById(cse.Call.Data()) + return cse.Contract.CompiledContract().Abi.MethodById(cse.Call.Data) } // String returns a displayable string representing the CallSequenceElement. @@ -224,7 +224,7 @@ func (cse *CallSequenceElement) String() string { } // Next decode our arguments (we jump four bytes to skip the function selector) - args, err := method.Inputs.Unpack(cse.Call.Data()[4:]) + args, err := method.Inputs.Unpack(cse.Call.Data[4:]) argsText := "" if err == nil { argsText, err = valuegeneration.EncodeABIArgumentsToString(method.Inputs, args) @@ -249,10 +249,10 @@ func (cse *CallSequenceElement) String() string { argsText, blockNumberStr, blockTimeStr, - cse.Call.Gas(), - cse.Call.GasPrice().String(), - cse.Call.Value().String(), - cse.Call.From(), + cse.Call.GasLimit, + cse.Call.GasPrice.String(), + cse.Call.Value.String(), + cse.Call.From, ) } @@ -273,7 +273,7 @@ func (cse *CallSequenceElement) AttachExecutionTrace(chain *chain.TestChain, con } // Perform our call with the given trace - _, cse.ExecutionTrace, err = executiontracer.CallWithExecutionTrace(chain, contractDefinitions, cse.Call, state) + _, cse.ExecutionTrace, err = executiontracer.CallWithExecutionTrace(chain, contractDefinitions, cse.Call.ToCoreMessage(), state) if err != nil { return fmt.Errorf("failed to resolve execution trace due to error replaying the call: %v", err) } diff --git a/fuzzing/calls/call_sequence_execution.go b/fuzzing/calls/call_sequence_execution.go index 8007bb6a..824c9c8e 100644 --- a/fuzzing/calls/call_sequence_execution.go +++ b/fuzzing/calls/call_sequence_execution.go @@ -84,7 +84,7 @@ func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc Exe } // Try to add our transaction to this block. - err = chain.PendingBlockAddTx(callSequenceElement.Call) + err = chain.PendingBlockAddTx(callSequenceElement.Call.ToCoreMessage()) if err != nil { // If we encountered a block gas limit error, this tx is too expensive to fit in this block. // If there are other transactions in the block, this makes sense. The block is "full". diff --git a/fuzzing/calls/gen_call_message_json.go b/fuzzing/calls/gen_call_message_json.go index 5b3583bf..26662c91 100644 --- a/fuzzing/calls/gen_call_message_json.go +++ b/fuzzing/calls/gen_call_message_json.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" ) var _ = (*callMessageMarshaling)(nil) @@ -15,78 +16,90 @@ var _ = (*callMessageMarshaling)(nil) // MarshalJSON marshals as JSON. func (c CallMessage) MarshalJSON() ([]byte, error) { type CallMessage struct { - MsgFrom common.Address `json:"from"` - MsgTo *common.Address `json:"to"` - MsgNonce uint64 `json:"nonce"` - MsgValue *hexutil.Big `json:"value"` - MsgGas uint64 `json:"gas"` - MsgGasPrice *hexutil.Big `json:"gasPrice"` - MsgGasFeeCap *hexutil.Big `json:"gasFeeCap"` - MsgGasTipCap *hexutil.Big `json:"gasTipCap"` - MsgData hexutil.Bytes `json:"data,omitempty"` - MsgDataAbiValues *CallMessageDataAbiValues `json:"dataAbiValues,omitempty"` + From common.Address `json:"from"` + To *common.Address `json:"to"` + Nonce uint64 `json:"nonce"` + Value *hexutil.Big `json:"value"` + GasLimit uint64 `json:"gasLimit"` + GasPrice *hexutil.Big `json:"gasPrice"` + GasFeeCap *hexutil.Big `json:"gasFeeCap"` + GasTipCap *hexutil.Big `json:"gasTipCap"` + Data hexutil.Bytes `json:"data,omitempty"` + DataAbiValues *CallMessageDataAbiValues `json:"dataAbiValues,omitempty"` + AccessList types.AccessList + SkipAccountChecks bool } var enc CallMessage - enc.MsgFrom = c.MsgFrom - enc.MsgTo = c.MsgTo - enc.MsgNonce = c.MsgNonce - enc.MsgValue = (*hexutil.Big)(c.MsgValue) - enc.MsgGas = c.MsgGas - enc.MsgGasPrice = (*hexutil.Big)(c.MsgGasPrice) - enc.MsgGasFeeCap = (*hexutil.Big)(c.MsgGasFeeCap) - enc.MsgGasTipCap = (*hexutil.Big)(c.MsgGasTipCap) - enc.MsgData = c.MsgData - enc.MsgDataAbiValues = c.MsgDataAbiValues + enc.From = c.From + enc.To = c.To + enc.Nonce = c.Nonce + enc.Value = (*hexutil.Big)(c.Value) + enc.GasLimit = c.GasLimit + enc.GasPrice = (*hexutil.Big)(c.GasPrice) + enc.GasFeeCap = (*hexutil.Big)(c.GasFeeCap) + enc.GasTipCap = (*hexutil.Big)(c.GasTipCap) + enc.Data = c.Data + enc.DataAbiValues = c.DataAbiValues + enc.AccessList = c.AccessList + enc.SkipAccountChecks = c.SkipAccountChecks return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (c *CallMessage) UnmarshalJSON(input []byte) error { type CallMessage struct { - MsgFrom *common.Address `json:"from"` - MsgTo *common.Address `json:"to"` - MsgNonce *uint64 `json:"nonce"` - MsgValue *hexutil.Big `json:"value"` - MsgGas *uint64 `json:"gas"` - MsgGasPrice *hexutil.Big `json:"gasPrice"` - MsgGasFeeCap *hexutil.Big `json:"gasFeeCap"` - MsgGasTipCap *hexutil.Big `json:"gasTipCap"` - MsgData *hexutil.Bytes `json:"data,omitempty"` - MsgDataAbiValues *CallMessageDataAbiValues `json:"dataAbiValues,omitempty"` + From *common.Address `json:"from"` + To *common.Address `json:"to"` + Nonce *uint64 `json:"nonce"` + Value *hexutil.Big `json:"value"` + GasLimit *uint64 `json:"gasLimit"` + GasPrice *hexutil.Big `json:"gasPrice"` + GasFeeCap *hexutil.Big `json:"gasFeeCap"` + GasTipCap *hexutil.Big `json:"gasTipCap"` + Data *hexutil.Bytes `json:"data,omitempty"` + DataAbiValues *CallMessageDataAbiValues `json:"dataAbiValues,omitempty"` + AccessList *types.AccessList + SkipAccountChecks *bool } var dec CallMessage if err := json.Unmarshal(input, &dec); err != nil { return err } - if dec.MsgFrom != nil { - c.MsgFrom = *dec.MsgFrom + if dec.From != nil { + c.From = *dec.From } - if dec.MsgTo != nil { - c.MsgTo = dec.MsgTo + if dec.To != nil { + c.To = dec.To } - if dec.MsgNonce != nil { - c.MsgNonce = *dec.MsgNonce + if dec.Nonce != nil { + c.Nonce = *dec.Nonce } - if dec.MsgValue != nil { - c.MsgValue = (*big.Int)(dec.MsgValue) + if dec.Value != nil { + c.Value = (*big.Int)(dec.Value) } - if dec.MsgGas != nil { - c.MsgGas = *dec.MsgGas + if dec.GasLimit != nil { + c.GasLimit = *dec.GasLimit } - if dec.MsgGasPrice != nil { - c.MsgGasPrice = (*big.Int)(dec.MsgGasPrice) + if dec.GasPrice != nil { + c.GasPrice = (*big.Int)(dec.GasPrice) } - if dec.MsgGasFeeCap != nil { - c.MsgGasFeeCap = (*big.Int)(dec.MsgGasFeeCap) + if dec.GasFeeCap != nil { + c.GasFeeCap = (*big.Int)(dec.GasFeeCap) } - if dec.MsgGasTipCap != nil { - c.MsgGasTipCap = (*big.Int)(dec.MsgGasTipCap) + if dec.GasTipCap != nil { + c.GasTipCap = (*big.Int)(dec.GasTipCap) } - if dec.MsgData != nil { - c.MsgData = *dec.MsgData + if dec.Data != nil { + c.Data = *dec.Data } - if dec.MsgDataAbiValues != nil { - c.MsgDataAbiValues = dec.MsgDataAbiValues + if dec.DataAbiValues != nil { + c.DataAbiValues = dec.DataAbiValues + } + if dec.AccessList != nil { + c.AccessList = *dec.AccessList + } + if dec.SkipAccountChecks != nil { + c.SkipAccountChecks = *dec.SkipAccountChecks } return nil } diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index d8c4d479..9cc4cda6 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -171,21 +171,21 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe // If we are deploying a contract and not targeting one with this call, there should be no work to do. currentSequenceElement := sequence[currentIndex] - if currentSequenceElement.Call.MsgTo == nil { + if currentSequenceElement.Call.To == nil { return currentSequenceElement, nil } // We are calling a contract with this call, ensure we can resolve the contract call is targeting. - resolvedContract, resolvedContractExists := deployedContracts[*currentSequenceElement.Call.MsgTo] + resolvedContract, resolvedContractExists := deployedContracts[*currentSequenceElement.Call.To] if !resolvedContractExists { - sequenceInvalidError = fmt.Errorf("contract at address '%v' could not be resolved", currentSequenceElement.Call.MsgTo.String()) + sequenceInvalidError = fmt.Errorf("contract at address '%v' could not be resolved", currentSequenceElement.Call.To.String()) return nil, nil } currentSequenceElement.Contract = resolvedContract // Next, if our sequence element uses ABI values to produce call data, our deserialized data is not yet // sufficient for runtime use, until we use it to resolve runtime references. - callAbiValues := currentSequenceElement.Call.MsgDataAbiValues + callAbiValues := currentSequenceElement.Call.DataAbiValues if callAbiValues != nil { sequenceInvalidError = callAbiValues.Resolve(currentSequenceElement.Contract.CompiledContract().Abi) if sequenceInvalidError != nil { diff --git a/fuzzing/corpus/corpus_test.go b/fuzzing/corpus/corpus_test.go index 3f32eefe..c49c904e 100644 --- a/fuzzing/corpus/corpus_test.go +++ b/fuzzing/corpus/corpus_test.go @@ -55,15 +55,15 @@ func getMockCallSequenceElement() *calls.CallSequenceElement { func getMockCallSequenceElementCall() *calls.CallMessage { to := common.BigToAddress(big.NewInt(rand.Int63())) txn := calls.CallMessage{ - MsgFrom: common.BigToAddress(big.NewInt(rand.Int63())), - MsgTo: &to, - MsgNonce: rand.Uint64(), - MsgValue: big.NewInt(int64(rand.Int())), - MsgGas: rand.Uint64(), - MsgGasPrice: big.NewInt(int64(rand.Int())), - MsgGasFeeCap: big.NewInt(int64(rand.Int())), - MsgGasTipCap: big.NewInt(int64(rand.Int())), - MsgData: []byte{uint8(rand.Uint64()), uint8(rand.Uint64()), uint8(rand.Uint64()), uint8(rand.Uint64())}, + From: common.BigToAddress(big.NewInt(rand.Int63())), + To: &to, + Nonce: rand.Uint64(), + Value: big.NewInt(int64(rand.Int())), + GasLimit: rand.Uint64(), + GasPrice: big.NewInt(int64(rand.Int())), + GasFeeCap: big.NewInt(int64(rand.Int())), + GasTipCap: big.NewInt(int64(rand.Int())), + Data: []byte{uint8(rand.Uint64()), uint8(rand.Uint64()), uint8(rand.Uint64()), uint8(rand.Uint64())}, } return &txn } diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index fdf6c08d..ed96dbe1 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -14,7 +14,7 @@ import ( // CallWithExecutionTrace obtains an execution trace for a given call, on the provided chain, using the state // provided. If a nil state is provided, the current chain state will be used. // Returns the ExecutionTrace for the call or an error if one occurs. -func CallWithExecutionTrace(chain *chain.TestChain, contractDefinitions contracts.Contracts, msg core.Message, state *state.StateDB) (*core.ExecutionResult, *ExecutionTrace, error) { +func CallWithExecutionTrace(chain *chain.TestChain, contractDefinitions contracts.Contracts, msg *core.Message, state *state.StateDB) (*core.ExecutionResult, *ExecutionTrace, error) { // Create an execution tracer executionTracer := NewExecutionTracer(contractDefinitions, chain.CheatCodeContracts()) diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index e64a7ba2..8c0269c1 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -376,7 +376,8 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro } // Add our transaction to the block - err = testChain.PendingBlockAddTx(msg) + // Add our transaction to the block + err = testChain.PendingBlockAddTx(msg.ToCoreMessage()) if err != nil { return err } diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index f347c682..d8f01bed 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -323,6 +323,14 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // shrink verifier. Chain state is reverted to the testing base prior to returning. // Returns a boolean indicating if the shrunken call sequence is valid for a given shrink request, or an error if one occurred. func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.CallSequence, shrinkRequest ShrinkCallSequenceRequest) (bool, error) { + // After testing the sequence, we'll want to rollback changes to reset our testing state. + var err error + defer func() { + if err == nil { + err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber) + } + }() + // Our "fetch next call method" method will simply fetch and fix the call message in case any fields are not correct due to shrinking. fetchElementFunc := func(currentIndex int) (*calls.CallSequenceElement, error) { // If we are at the end of our sequence, return nil indicating we should stop executing. @@ -340,9 +348,9 @@ func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.Ca executionCheckFunc := func(currentlyExecutedSequence calls.CallSequence) (bool, error) { // Check for updates to coverage and corpus (using only the section of the sequence we tested so far). // If we detect coverage changes, add this sequence. - err := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) - if err != nil { - return true, err + seqErr := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true) + if seqErr != nil { + return true, seqErr } // If our fuzzer context is done, exit out immediately without results. @@ -354,7 +362,7 @@ func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.Ca } // Execute our call sequence. - _, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) + _, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) if err != nil { return false, err } @@ -372,11 +380,6 @@ func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.Ca return false, err } } - - // After testing the sequence, we'll want to rollback changes to reset our testing state. - if err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber); err != nil { - return false, err - } return validShrunkSequence, nil } @@ -430,7 +433,7 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri possibleShrunkSequence, _ := optimizedSequence.Clone() // Loop for each argument in the currently indexed call to mutate it. - abiValuesMsgData := possibleShrunkSequence[i].Call.MsgDataAbiValues + abiValuesMsgData := possibleShrunkSequence[i].Call.DataAbiValues for j := 0; j < len(abiValuesMsgData.InputValues); j++ { mutatedInput, err := valuegeneration.MutateAbiValue(fw.sequenceGenerator.config.ValueGenerator, fw.shrinkingValueMutator, &abiValuesMsgData.Method.Inputs[j].Type, abiValuesMsgData.InputValues[j]) if err != nil { diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index 8892cd3b..2b9358a4 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -260,6 +260,9 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement } } + // Update the element with the current nonce for the associated chain. + element.Call.FillFromTestChainProperties(g.worker.chain) + // Update our base sequence, advance our position, and return the processed element from this round. g.baseSequence[g.fetchIndex] = element g.fetchIndex++ @@ -301,7 +304,6 @@ func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement Method: &selectedMethod.Method, InputValues: args, }) - msg.FillFromTestChainProperties(g.worker.chain) // Determine our delay values for this element blockNumberDelay := uint64(0) @@ -441,12 +443,12 @@ func callSeqGenFuncInterleaveAtRandom(sequenceGenerator *CallSequenceGenerator, // Returns an error if one occurs. func prefetchModifyCallFuncMutate(sequenceGenerator *CallSequenceGenerator, element *calls.CallSequenceElement) error { // If this element has no ABI value based call data, exit early. - if element.Call == nil || element.Call.MsgDataAbiValues == nil { + if element.Call == nil || element.Call.DataAbiValues == nil { return nil } // Loop for each input value and mutate it - abiValuesMsgData := element.Call.MsgDataAbiValues + abiValuesMsgData := element.Call.DataAbiValues for i := 0; i < len(abiValuesMsgData.InputValues); i++ { mutatedInput, err := valuegeneration.MutateAbiValue(sequenceGenerator.config.ValueGenerator, sequenceGenerator.config.ValueMutator, &abiValuesMsgData.Method.Inputs[i].Type, abiValuesMsgData.InputValues[i]) if err != nil { diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index ba5ab258..424db7e5 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -96,10 +96,10 @@ func (t *OptimizationTestCaseProvider) runOptimizationTest(worker *FuzzerWorker, var executionTrace *executiontracer.ExecutionTrace if trace { executionTracer := executiontracer.NewExecutionTracer(worker.fuzzer.contractDefinitions, worker.chain.CheatCodeContracts()) - executionResult, err = worker.Chain().CallContract(msg, nil, executionTracer) + executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil, executionTracer) executionTrace = executionTracer.Trace() } else { - executionResult, err = worker.Chain().CallContract(msg, nil) + executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil) } if err != nil { return nil, nil, fmt.Errorf("failed to call optimization test method: %v", err) diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index ca8be15c..10399cc5 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -99,10 +99,10 @@ func (t *PropertyTestCaseProvider) checkPropertyTestFailed(worker *FuzzerWorker, var executionTrace *executiontracer.ExecutionTrace if trace { executionTracer := executiontracer.NewExecutionTracer(worker.fuzzer.contractDefinitions, worker.chain.CheatCodeContracts()) - executionResult, err = worker.Chain().CallContract(msg, nil, executionTracer) + executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil, executionTracer) executionTrace = executionTracer.Trace() } else { - executionResult, err = worker.Chain().CallContract(msg, nil) + executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil) } if err != nil { return false, nil, fmt.Errorf("failed to call property test method: %v", err) diff --git a/go.mod b/go.mod index ca1b0639..81250f0e 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/Masterminds/semver v1.5.0 - github.com/ethereum/go-ethereum v1.11.1 + github.com/ethereum/go-ethereum v1.12.0 github.com/fxamacker/cbor v1.5.1 github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 @@ -15,6 +15,7 @@ require ( golang.org/x/crypto v0.11.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/net v0.12.0 + golang.org/x/sys v0.10.0 ) require ( @@ -33,13 +34,13 @@ require ( github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.1 // indirect + github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -54,7 +55,6 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.39.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/prometheus/tsdb v0.10.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -63,11 +63,10 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/sys v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/ethereum/go-ethereum v1.11.1 => github.com/crytic/medusa-geth v0.0.0-20230221190257-777a77b25150 +replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20230811005223-cee04520a2f9 diff --git a/go.sum b/go.sum index fe8142b2..1db673f4 100644 --- a/go.sum +++ b/go.sum @@ -8,26 +8,20 @@ github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwS github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE= github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -52,8 +46,8 @@ github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crytic/medusa-geth v0.0.0-20230221190257-777a77b25150 h1:Helt4ysP5N0cJzvhBsx4JcAOte5gD4whzamaXWpg37M= -github.com/crytic/medusa-geth v0.0.0-20230221190257-777a77b25150/go.mod h1:DuefStAgaxoaYGLR0FueVcVbehmn5n9QUcVrMCuOvuc= +github.com/crytic/medusa-geth v0.0.0-20230811005223-cee04520a2f9 h1:BUuL3h23IdVSmyq3I7LVUYRInKgXShMLKElYaFgn4RM= +github.com/crytic/medusa-geth v0.0.0-20230811005223-cee04520a2f9/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -64,9 +58,7 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -89,23 +81,19 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -127,9 +115,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -149,12 +137,10 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gto8n9hAmRxKxoar0iE6BLucjw= -github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.2.1 h1:XRtyuda/zw2l+Bq/38n5XUoEF72aSOu/77Thd9pPp2o= -github.com/holiman/uint256 v1.2.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c h1:DZfsyhDK1hnSS5lH8l+JggqzEleHteTYfutAiVlSUM8= +github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= @@ -169,7 +155,6 @@ github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrO github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= @@ -183,8 +168,6 @@ github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -193,6 +176,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -213,7 +197,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= @@ -225,14 +208,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -247,30 +228,20 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic= -github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -289,10 +260,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -304,7 +273,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -343,7 +311,6 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -368,7 +335,6 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -398,9 +364,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -486,7 +450,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -501,7 +464,6 @@ gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/utils/message_transaction_utils.go b/utils/message_transaction_utils.go index cfd54b25..57fd68ed 100644 --- a/utils/message_transaction_utils.go +++ b/utils/message_transaction_utils.go @@ -6,13 +6,13 @@ import ( ) // MessageToTransaction derives a types.Transaction from a types.Message. -func MessageToTransaction(msg core.Message) *types.Transaction { +func MessageToTransaction(msg *core.Message) *types.Transaction { return types.NewTx(&types.LegacyTx{ - Nonce: msg.Nonce(), - GasPrice: msg.GasPrice(), - Gas: msg.Gas(), - To: msg.To(), - Value: msg.Value(), - Data: msg.Data(), + Nonce: msg.Nonce, + GasPrice: msg.GasPrice, + Gas: msg.GasLimit, + To: msg.To, + Value: msg.Value, + Data: msg.Data, }) } From 4de61e80c823ea76fa0cd0f1f39448c5792c7c71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:28:36 -0400 Subject: [PATCH 022/109] Bump golang.org/x/net from 0.12.0 to 0.14.0 (#196) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.12.0 to 0.14.0. - [Commits](https://github.com/golang/net/compare/v0.12.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 81250f0e..1aec9600 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,10 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.11.0 + golang.org/x/crypto v0.12.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/net v0.12.0 - golang.org/x/sys v0.10.0 + golang.org/x/net v0.14.0 + golang.org/x/sys v0.11.0 ) require ( @@ -63,7 +63,7 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 1db673f4..1c621d8b 100644 --- a/go.sum +++ b/go.sum @@ -319,8 +319,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -351,8 +351,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -394,8 +394,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -403,8 +403,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 7d2e16caeff98133e4c763c6ceb04abc1af674dd Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 15 Aug 2023 15:54:16 -0400 Subject: [PATCH 023/109] `console.log` go brrr (#193) Introduce `console.log` functionality with string formatting capabilities. --------- Co-authored-by: David Pokora --- chain/cheat_code_contract.go | 27 +- chain/console_log_cheat_code_contract.go | 124 ++ ...des.go => standard_cheat_code_contract.go} | 24 +- compilation/abiutils/solidity_errors.go | 20 +- fuzzing/executiontracer/execution_trace.go | 77 +- fuzzing/fuzzer_test.go | 56 +- .../cheat_codes/console_log/console_log.sol | 1568 +++++++++++++++++ logging/colors/constants.go | 3 + utils/combinatorial_utils.go | 46 + 9 files changed, 1898 insertions(+), 47 deletions(-) create mode 100644 chain/console_log_cheat_code_contract.go rename chain/{cheat_codes.go => standard_cheat_code_contract.go} (94%) create mode 100644 fuzzing/testdata/contracts/cheat_codes/console_log/console_log.sol create mode 100644 utils/combinatorial_utils.go diff --git a/chain/cheat_code_contract.go b/chain/cheat_code_contract.go index 0b644cfc..08ceb6ea 100644 --- a/chain/cheat_code_contract.go +++ b/chain/cheat_code_contract.go @@ -54,6 +54,30 @@ type cheatCodeRawReturnData struct { Err error } +// getCheatCodeProviders obtains a cheatCodeTracer (used to power cheat code analysis) and associated CheatCodeContract +// objects linked to the tracer (providing on-chain callable methods as an entry point). These objects are attached to +// the TestChain to enable cheat code functionality. +// Returns the tracer and associated pre-compile contracts, or an error, if one occurred. +func getCheatCodeProviders() (*cheatCodeTracer, []*CheatCodeContract, error) { + // Create a cheat code tracer and attach it to the chain. + tracer := newCheatCodeTracer() + + // Obtain our standard cheat code pre-compile + stdCheatCodeContract, err := getStandardCheatCodeContract(tracer) + if err != nil { + return nil, nil, err + } + + // Obtain the console.log pre-compile + consoleCheatCodeContract, err := getConsoleLogCheatCodeContract(tracer) + if err != nil { + return nil, nil, err + } + + // Return the tracer and precompiles + return tracer, []*CheatCodeContract{stdCheatCodeContract, consoleCheatCodeContract}, nil +} + // newCheatCodeContract returns a new precompiledContract which uses the attached cheatCodeTracer for execution // context. func newCheatCodeContract(tracer *cheatCodeTracer, address common.Address, name string) *CheatCodeContract { @@ -98,7 +122,7 @@ func (c *CheatCodeContract) Abi() *abi.ABI { } // addMethod adds a new method to the precompiled contract. -// Returns an error if one occurred. +// Throws a panic if either the name is the empty string or the handler is nil. func (c *CheatCodeContract) addMethod(name string, inputs abi.Arguments, outputs abi.Arguments, handler cheatCodeMethodHandler) { // Verify a method name was provided if name == "" { @@ -117,7 +141,6 @@ func (c *CheatCodeContract) addMethod(name string, inputs abi.Arguments, outputs method: method, handler: handler, } - // Add the method to the ABI. // Note: Normally the key here should be the method name, not sig. But cheat code contracts have duplicate // method names with different parameter types, so we use this so they don't override. diff --git a/chain/console_log_cheat_code_contract.go b/chain/console_log_cheat_code_contract.go new file mode 100644 index 00000000..1c20dedb --- /dev/null +++ b/chain/console_log_cheat_code_contract.go @@ -0,0 +1,124 @@ +package chain + +import ( + "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "strconv" +) + +// ConsoleLogContractAddress is the address for the console.log precompile contract +var ConsoleLogContractAddress = common.HexToAddress("0x000000000000000000636F6e736F6c652e6c6f67") + +// getConsoleLogCheatCodeContract obtains a CheatCodeContract which implements the console.log functions. +// Returns the precompiled contract, or an error if there is one. +func getConsoleLogCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, error) { + // Create a new precompile to add methods to. + contract := newCheatCodeContract(tracer, ConsoleLogContractAddress, "Console") + + // Define all the ABI types needed for console.log functions + typeUint256, err := abi.NewType("uint256", "", nil) + if err != nil { + return nil, err + } + typeInt256, err := abi.NewType("int256", "", nil) + if err != nil { + return nil, err + } + typeString, err := abi.NewType("string", "", nil) + if err != nil { + return nil, err + } + typeBool, err := abi.NewType("bool", "", nil) + if err != nil { + return nil, err + } + typeAddress, err := abi.NewType("address", "", nil) + if err != nil { + return nil, err + } + typeBytes, err := abi.NewType("bytes", "", nil) + if err != nil { + return nil, err + } + + // We will store all the fixed byte (e.g. byte1, byte2) in a mapping + const numFixedByteTypes = 32 + fixedByteTypes := make(map[int]abi.Type, numFixedByteTypes) + for i := 1; i <= numFixedByteTypes; i++ { + byteString := "bytes" + strconv.FormatInt(int64(i), 10) + fixedByteTypes[i], err = abi.NewType(byteString, "", nil) + if err != nil { + return nil, err + } + } + + // We have a few special log function signatures outside all the permutations of (string, uint256, bool, address). + // These include log(int256), log(bytes), log(bytesX), and log(string, uint256). So, we will manually create these + // signatures and then programmatically iterate through all the permutations. + + // Note that none of the functions actually do anything - they just have to be callable so that the execution + // traces can show the arguments that the user wants to log! + + // log(int256): Log an int256 + contract.addMethod("log", abi.Arguments{{Type: typeInt256}}, abi.Arguments{}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + return nil, nil + }, + ) + + // log(bytes): Log bytes + contract.addMethod("log", abi.Arguments{{Type: typeBytes}}, abi.Arguments{}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + return nil, nil + }, + ) + + // Now, we will add the logBytes1, logBytes2, and so on in a loop + for i := 1; i <= numFixedByteTypes; i++ { + // Create local copy of abi argument + fixedByteType := fixedByteTypes[i] + + // Add the method + contract.addMethod("log", abi.Arguments{{Type: fixedByteType}}, abi.Arguments{}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + return nil, nil + }, + ) + } + + // log(string, int256): Log string with an int where the string could be formatted + contract.addMethod("log", abi.Arguments{{Type: typeString}, {Type: typeInt256}}, abi.Arguments{}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + return nil, nil + }, + ) + + // These are the four parameter types that console.log() accepts + choices := abi.Arguments{{Type: typeUint256}, {Type: typeString}, {Type: typeBool}, {Type: typeAddress}} + + // Create all possible permutations (with repetition) where the number of choices increases from 1...len(choices) + permutations := make([]abi.Arguments, 0) + for n := 1; n <= len(choices); n++ { + nextSetOfPermutations := utils.PermutationsWithRepetition(choices, n) + for _, permutation := range nextSetOfPermutations { + permutations = append(permutations, permutation) + } + } + + // Iterate across each permutation to add their associated event and function handler + for i := 0; i < len(permutations); i++ { + // Make a local copy of the current permutation + permutation := permutations[i] + + // Create the function handler + contract.addMethod("log", permutation, abi.Arguments{}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + return nil, nil + }, + ) + } + + // Return our precompile contract information. + return contract, nil +} diff --git a/chain/cheat_codes.go b/chain/standard_cheat_code_contract.go similarity index 94% rename from chain/cheat_codes.go rename to chain/standard_cheat_code_contract.go index 49bd4ae3..141bf204 100644 --- a/chain/cheat_codes.go +++ b/chain/standard_cheat_code_contract.go @@ -14,30 +14,14 @@ import ( "strings" ) -// getCheatCodeProviders obtains a cheatCodeTracer (used to power cheat code analysis) and associated CheatCodeContract -// objects linked to the tracer (providing on-chain callable methods as an entry point). These objects are attached to -// the TestChain to enable cheat code functionality. -// Returns the tracer and associated pre-compile contracts, or an error, if one occurred. -func getCheatCodeProviders() (*cheatCodeTracer, []*CheatCodeContract, error) { - // Create a cheat code tracer and attach it to the chain. - tracer := newCheatCodeTracer() - - // Obtain our cheat code pre-compiles - stdCheatCodeContract, err := getStandardCheatCodeContract(tracer) - if err != nil { - return nil, nil, err - } - - // Return the tracer and precompiles - return tracer, []*CheatCodeContract{stdCheatCodeContract}, nil -} +// StandardCheatcodeContractAddress is the address for the standard cheatcode contract +var StandardCheatcodeContractAddress = common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D") // getStandardCheatCodeContract obtains a CheatCodeContract which implements common cheat codes. // Returns the precompiled contract, or an error if one occurs. func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, error) { - // Define our address for this precompile contract, then create a new precompile to add methods to. - contractAddress := common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D") - contract := newCheatCodeContract(tracer, contractAddress, "StdCheats") + // Create a new precompile to add methods to. + contract := newCheatCodeContract(tracer, StandardCheatcodeContractAddress, "StdCheats") // Define some basic ABI argument types typeAddress, err := abi.NewType("address", "", nil) diff --git a/compilation/abiutils/solidity_errors.go b/compilation/abiutils/solidity_errors.go index 384a8fda..fd848f99 100644 --- a/compilation/abiutils/solidity_errors.go +++ b/compilation/abiutils/solidity_errors.go @@ -113,25 +113,25 @@ func GetPanicReason(panicCode uint64) string { // Switch on panic code switch panicCode { case PanicCodeCompilerInserted: - return "compiler inserted panic" + return "panic: compiler inserted panic" case PanicCodeAssertFailed: - return "assertion failed" + return "panic: assertion failed" case PanicCodeArithmeticUnderOverflow: - return "arithmetic underflow" + return "panic: arithmetic underflow" case PanicCodeDivideByZero: - return "division by zero" + return "panic: division by zero" case PanicCodeEnumTypeConversionOutOfBounds: - return "enum access out of bounds" + return "panic: enum access out of bounds" case PanicCodeIncorrectStorageAccess: - return "incorrect storage access" + return "panic: incorrect storage access" case PanicCodePopEmptyArray: - return "pop on empty array" + return "panic: pop on empty array" case PanicCodeOutOfBoundsArrayAccess: - return "out of bounds array access" + return "panic: out of bounds array access" case PanicCodeAllocateTooMuchMemory: - return "overallocation of memory" + return "panic; overallocation of memory" case PanicCodeCallUninitializedVariable: - return "call on uninitialized variable" + return "panic: call on uninitialized variable" default: return fmt.Sprintf("unknown panic code(%v)", panicCode) } diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index a021de9d..287ce52c 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -3,6 +3,7 @@ package executiontracer import ( "encoding/hex" "fmt" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/valuegeneration" @@ -11,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" coreTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "regexp" "strings" ) @@ -36,10 +38,12 @@ func newExecutionTrace(contracts contracts.Contracts) *ExecutionTrace { // generateCallFrameEnterElements generates a list of elements describing top level information about this call frame. // This list of elements will hold information about what kind of call it is, wei sent, what method is called, and more. -// Additionally, the list may also hold formatting options for console output. -func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) []any { - // Create list of elements +// Additionally, the list may also hold formatting options for console output. This function also returns a non-empty +// string in case this call frame represents a call to the console.log precompile contract. +func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) ([]any, string) { + // Create list of elements and console log string elements := make([]any, 0) + var consoleLogString string // Define some strings and objects that represent our current call frame var ( @@ -95,10 +99,30 @@ func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) [] // Unpack our input values and obtain a string to represent them inputValues, err := method.Inputs.Unpack(abiDataInputBuffer) if err == nil { + // Encode the ABI arguments into strings encodedInputString, err := valuegeneration.EncodeABIArgumentsToString(method.Inputs, inputValues) if err == nil { inputArgumentsDisplayText = &encodedInputString } + + // If the call was made to the console log precompile address, let's retrieve the log and format it + if callFrame.ToAddress == chain.ConsoleLogContractAddress { + // First, attempt to do string formatting if the first element is a string and has a percent sign in it + exp := regexp.MustCompile(`%`) + stringInput, isString := inputValues[0].(string) + if isString && exp.MatchString(stringInput) { + // Format the string and add it to the list of logs + consoleLogString = fmt.Sprintf(inputValues[0].(string), inputValues[1:]...) + } else { + // The string does not need to be formatted, and we can just use the encoded input string + consoleLogString = encodedInputString + } + + // Add a bullet point before the string and a new line after the string + if len(consoleLogString) > 0 { + consoleLogString = colors.BULLET_POINT + " " + consoleLogString + "\n" + } + } } } @@ -120,7 +144,11 @@ func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) [] } } else { if callFrame.ExecutedCode { - callInfo = fmt.Sprintf("%v.%v(%v) (addr=%v, value=%v, sender=%v)", codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + if callFrame.ToAddress == chain.ConsoleLogContractAddress { + callInfo = fmt.Sprintf("%v.%v(%v)", codeContractName, methodName, *inputArgumentsDisplayText) + } else { + callInfo = fmt.Sprintf("%v.%v(%v) (addr=%v, value=%v, sender=%v)", codeContractName, methodName, *inputArgumentsDisplayText, callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) + } } else { callInfo = fmt.Sprintf("(addr=%v, value=%v, sender=%v)", callFrame.ToAddress.String(), callFrame.CallValue, callFrame.SenderAddress.String()) } @@ -129,7 +157,7 @@ func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) [] // Add call information to the elements elements = append(elements, callInfo, "\n") - return elements + return elements, consoleLogString } // generateCallFrameExitElements generates a list of elements describing the return data of the call frame (e.g. @@ -263,11 +291,13 @@ func (t *ExecutionTrace) generateEventEmittedElements(callFrame *CallFrame, even return elements } -// generateElementsForCallFrame generates a list of elements for a given call frame and its children. Additionally, -// the list may also hold formatting options for console output. -func (t *ExecutionTrace) generateElementsForCallFrame(currentDepth int, callFrame *CallFrame) []any { - // Create list of elements +// generateElementsAndLogsForCallFrame generates a list of elements and logs for a given call frame and its children. +// The list of elements may also hold formatting options for console output. The list of logs represent calls to the +// console.log precompile contract. +func (t *ExecutionTrace) generateElementsAndLogsForCallFrame(currentDepth int, callFrame *CallFrame) ([]any, []any) { + // Create list of elements and logs elements := make([]any, 0) + consoleLogs := make([]any, 0) // Create our current call line prefix (indented by call depth) prefix := strings.Repeat("\t", currentDepth) + " => " @@ -278,8 +308,14 @@ func (t *ExecutionTrace) generateElementsForCallFrame(currentDepth int, callFram } // Add the call frame enter header elements + newElements, consoleLogString := t.generateCallFrameEnterElements(callFrame) elements = append(elements, prefix) - elements = append(elements, t.generateCallFrameEnterElements(callFrame)...) + elements = append(elements, newElements...) + + // If this call frame was a console.log contract call, add the string to the list of logs + if len(consoleLogString) > 0 { + consoleLogs = append(consoleLogs, consoleLogString) + } // Now that the header has been printed, create our indent level to express everything that // happened under it. @@ -293,8 +329,9 @@ func (t *ExecutionTrace) generateElementsForCallFrame(currentDepth int, callFram for _, operation := range callFrame.Operations { if childCallFrame, ok := operation.(*CallFrame); ok { // If this is a call frame being entered, generate information recursively. - childOutputLines := t.generateElementsForCallFrame(currentDepth+1, childCallFrame) + childOutputLines, childConsoleLogStrings := t.generateElementsAndLogsForCallFrame(currentDepth+1, childCallFrame) elements = append(elements, childOutputLines...) + consoleLogs = append(consoleLogs, childConsoleLogStrings...) } else if eventLog, ok := operation.(*coreTypes.Log); ok { // If an event log was emitted, add a message for it. elements = append(elements, prefix) @@ -304,23 +341,35 @@ func (t *ExecutionTrace) generateElementsForCallFrame(currentDepth int, callFram // If we self-destructed, add a message for it before our footer. if callFrame.SelfDestructed { - elements = append(elements, prefix, colors.MagentaBold, "[selfdestruct]", colors.Reset, "\n") + elements = append(elements, prefix, colors.RedBold, "[selfdestruct]", colors.Reset, "\n") } // Add the call frame exit footer elements = append(elements, prefix) elements = append(elements, t.generateCallFrameExitElements(callFrame)...) + } // Return our elements - return elements + return elements, consoleLogs } // Log returns a logging.LogBuffer that represents this execution trace. This buffer will be passed to the underlying // logger which will format it accordingly for console or file. func (t *ExecutionTrace) Log() *logging.LogBuffer { + // Create a buffer buffer := logging.NewLogBuffer() - buffer.Append(t.generateElementsForCallFrame(0, t.TopLevelCallFrame)...) + + // First, add the elements that make up the overarching execution trace + elements, logs := t.generateElementsAndLogsForCallFrame(0, t.TopLevelCallFrame) + buffer.Append(elements...) + + // If we captured any logs during tracing, add them to the overarching execution trace + if len(logs) > 0 { + buffer.Append(colors.Bold, "[Logs]", colors.Reset, "\n") + buffer.Append(logs...) + } + return buffer } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 42ff13c5..978ecbc0 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -258,6 +258,60 @@ func TestCheatCodes(t *testing.T) { } } +// TestConsoleLog tests the console.log precompile contract by logging a variety of different primitive types and +// then failing. The execution trace for the failing call sequence should hold the various logs. +func TestConsoleLog(t *testing.T) { + // These are the logs that should show up in the execution trace + expectedLogs := []string{ + "2", + "hello world", + "byte", + "i is 2", + "% bool is true, addr is 0x0000000000000000000000000000000000000000, u is 100", + } + + filePaths := []string{ + "testdata/contracts/cheat_codes/console_log/console_log.sol", + } + for _, filePath := range filePaths { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: filePath, + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TestLimit = 10000 + // enable assertion testing only + config.Fuzzing.Testing.PropertyTesting.Enabled = true + config.Fuzzing.Testing.AssertionTesting.Enabled = true + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + // Check for failed assertion tests. + failedTestCase := f.fuzzer.TestCasesWithStatus(TestCaseStatusFailed) + assert.NotEmpty(t, failedTestCase, "expected to have failed test cases") + + // Obtain our first failed test case, get the message, and verify it contains our assertion failed. + failingSequence := *failedTestCase[0].CallSequence() + assert.NotEmpty(t, failingSequence, "expected to have calls in the call sequence failing an assertion test") + + // Obtain the last call + lastCall := failingSequence[len(failingSequence)-1] + assert.NotNilf(t, lastCall.ExecutionTrace, "expected to have an execution trace attached to call sequence for this test") + + // Get the execution trace message + executionTraceMsg := lastCall.ExecutionTrace.Log().String() + + // Verify it contains all expected logs + for _, expectedLog := range expectedLogs { + assert.Contains(t, executionTraceMsg, expectedLog) + } + }, + }) + } +} + // TestDeploymentsInnerDeployments runs tests to ensure dynamically deployed contracts are detected by the Fuzzer and // their properties are tested appropriately. func TestDeploymentsInnerDeployments(t *testing.T) { @@ -379,7 +433,7 @@ func TestExecutionTraces(t *testing.T) { "testdata/contracts/execution_tracing/proxy_call.sol": {"TestContract -> InnerDeploymentContract.setXY", "Hello from proxy call args!"}, "testdata/contracts/execution_tracing/revert_custom_error.sol": {"CustomError", "Hello from a custom error!"}, "testdata/contracts/execution_tracing/revert_reasons.sol": {"RevertingContract was called and reverted."}, - "testdata/contracts/execution_tracing/self_destruct.sol": {"[selfdestruct]", "[assertion failed]"}, + "testdata/contracts/execution_tracing/self_destruct.sol": {"[selfdestruct]", "[panic: assertion failed]"}, } for filePath, expectedTraceMessages := range expectedMessagesPerTest { runFuzzerTest(t, &fuzzerSolcFileTest{ diff --git a/fuzzing/testdata/contracts/cheat_codes/console_log/console_log.sol b/fuzzing/testdata/contracts/cheat_codes/console_log/console_log.sol new file mode 100644 index 00000000..ddf5ffe4 --- /dev/null +++ b/fuzzing/testdata/contracts/cheat_codes/console_log/console_log.sol @@ -0,0 +1,1568 @@ +// Test console.log capabilities to make sure logging and string formatting are happening as expected +contract TestContract { + + function testConsoleLog() public { + // Log an int256 + int256 i = 2; + console.log(i); + + // Log bytes + bytes memory byteSlice = "hello world"; + console.logBytes(byteSlice); + + // Log fixed bytes + bytes4 fixedBytes = "byte"; + console.logBytes4(fixedBytes); + + // Log a string and int256 while testing string formatting + string memory str = "i is %d"; + console.log(str, i); + + // Test the permutation logic by logging a random permutation and also string formatting + bool b = true; + address addr = address(0); + uint256 u = 100; + str = "%% bool is %t, addr is %s, u is %d"; + console.log(str, b, addr, u); + assert(false); + } +} + +library console { + address constant CONSOLE_ADDRESS = address(0x000000000000000000636F6e736F6c652e6c6f67); + + function _sendLogPayload(bytes memory payload) private view { + uint256 payloadLength = payload.length; + address consoleAddress = CONSOLE_ADDRESS; + /// @solidity memory-safe-assembly + assembly { + let payloadStart := add(payload, 32) + let r := staticcall(gas(), consoleAddress, payloadStart, payloadLength, 0, 0) + } + } + + function log() internal view { + _sendLogPayload(abi.encodeWithSignature("log()")); + } + + function logInt(int256 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(int256)", p0)); + } + + function logUint(uint256 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256)", p0)); + } + + function logString(string memory p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string)", p0)); + } + + function logBool(bool p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool)", p0)); + } + + function logAddress(address p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address)", p0)); + } + + function logBytes(bytes memory p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes)", p0)); + } + + function logBytes1(bytes1 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes1)", p0)); + } + + function logBytes2(bytes2 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes2)", p0)); + } + + function logBytes3(bytes3 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes3)", p0)); + } + + function logBytes4(bytes4 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes4)", p0)); + } + + function logBytes5(bytes5 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes5)", p0)); + } + + function logBytes6(bytes6 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes6)", p0)); + } + + function logBytes7(bytes7 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes7)", p0)); + } + + function logBytes8(bytes8 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes8)", p0)); + } + + function logBytes9(bytes9 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes9)", p0)); + } + + function logBytes10(bytes10 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes10)", p0)); + } + + function logBytes11(bytes11 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes11)", p0)); + } + + function logBytes12(bytes12 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes12)", p0)); + } + + function logBytes13(bytes13 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes13)", p0)); + } + + function logBytes14(bytes14 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes14)", p0)); + } + + function logBytes15(bytes15 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes15)", p0)); + } + + function logBytes16(bytes16 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes16)", p0)); + } + + function logBytes17(bytes17 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes17)", p0)); + } + + function logBytes18(bytes18 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes18)", p0)); + } + + function logBytes19(bytes19 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes19)", p0)); + } + + function logBytes20(bytes20 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes20)", p0)); + } + + function logBytes21(bytes21 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes21)", p0)); + } + + function logBytes22(bytes22 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes22)", p0)); + } + + function logBytes23(bytes23 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes23)", p0)); + } + + function logBytes24(bytes24 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes24)", p0)); + } + + function logBytes25(bytes25 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes25)", p0)); + } + + function logBytes26(bytes26 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes26)", p0)); + } + + function logBytes27(bytes27 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes27)", p0)); + } + + function logBytes28(bytes28 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes28)", p0)); + } + + function logBytes29(bytes29 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes29)", p0)); + } + + function logBytes30(bytes30 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes30)", p0)); + } + + function logBytes31(bytes31 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes31)", p0)); + } + + function logBytes32(bytes32 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bytes32)", p0)); + } + + function log(uint256 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256)", p0)); + } + + function log(int256 p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(int256)", p0)); + } + + function log(string memory p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string)", p0)); + } + + function log(bool p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool)", p0)); + } + + function log(address p0) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address)", p0)); + } + + function log(uint256 p0, uint256 p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256)", p0, p1)); + } + + function log(uint256 p0, string memory p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string)", p0, p1)); + } + + function log(uint256 p0, bool p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool)", p0, p1)); + } + + function log(uint256 p0, address p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address)", p0, p1)); + } + + function log(string memory p0, uint256 p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256)", p0, p1)); + } + + function log(string memory p0, int256 p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,int256)", p0, p1)); + } + + function log(string memory p0, string memory p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string)", p0, p1)); + } + + function log(string memory p0, bool p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool)", p0, p1)); + } + + function log(string memory p0, address p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address)", p0, p1)); + } + + function log(bool p0, uint256 p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256)", p0, p1)); + } + + function log(bool p0, string memory p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string)", p0, p1)); + } + + function log(bool p0, bool p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool)", p0, p1)); + } + + function log(bool p0, address p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address)", p0, p1)); + } + + function log(address p0, uint256 p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256)", p0, p1)); + } + + function log(address p0, string memory p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string)", p0, p1)); + } + + function log(address p0, bool p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool)", p0, p1)); + } + + function log(address p0, address p1) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address)", p0, p1)); + } + + function log(uint256 p0, uint256 p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,uint256)", p0, p1, p2)); + } + + function log(uint256 p0, uint256 p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,string)", p0, p1, p2)); + } + + function log(uint256 p0, uint256 p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,bool)", p0, p1, p2)); + } + + function log(uint256 p0, uint256 p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,address)", p0, p1, p2)); + } + + function log(uint256 p0, string memory p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,uint256)", p0, p1, p2)); + } + + function log(uint256 p0, string memory p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,string)", p0, p1, p2)); + } + + function log(uint256 p0, string memory p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,bool)", p0, p1, p2)); + } + + function log(uint256 p0, string memory p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,address)", p0, p1, p2)); + } + + function log(uint256 p0, bool p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,uint256)", p0, p1, p2)); + } + + function log(uint256 p0, bool p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,string)", p0, p1, p2)); + } + + function log(uint256 p0, bool p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,bool)", p0, p1, p2)); + } + + function log(uint256 p0, bool p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,address)", p0, p1, p2)); + } + + function log(uint256 p0, address p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,uint256)", p0, p1, p2)); + } + + function log(uint256 p0, address p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,string)", p0, p1, p2)); + } + + function log(uint256 p0, address p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,bool)", p0, p1, p2)); + } + + function log(uint256 p0, address p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,address)", p0, p1, p2)); + } + + function log(string memory p0, uint256 p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,uint256)", p0, p1, p2)); + } + + function log(string memory p0, uint256 p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,string)", p0, p1, p2)); + } + + function log(string memory p0, uint256 p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,bool)", p0, p1, p2)); + } + + function log(string memory p0, uint256 p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,address)", p0, p1, p2)); + } + + function log(string memory p0, string memory p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,uint256)", p0, p1, p2)); + } + + function log(string memory p0, string memory p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,string)", p0, p1, p2)); + } + + function log(string memory p0, string memory p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,bool)", p0, p1, p2)); + } + + function log(string memory p0, string memory p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,address)", p0, p1, p2)); + } + + function log(string memory p0, bool p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint256)", p0, p1, p2)); + } + + function log(string memory p0, bool p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,string)", p0, p1, p2)); + } + + function log(string memory p0, bool p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool)", p0, p1, p2)); + } + + function log(string memory p0, bool p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,address)", p0, p1, p2)); + } + + function log(string memory p0, address p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,uint256)", p0, p1, p2)); + } + + function log(string memory p0, address p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,string)", p0, p1, p2)); + } + + function log(string memory p0, address p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,bool)", p0, p1, p2)); + } + + function log(string memory p0, address p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,address)", p0, p1, p2)); + } + + function log(bool p0, uint256 p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,uint256)", p0, p1, p2)); + } + + function log(bool p0, uint256 p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,string)", p0, p1, p2)); + } + + function log(bool p0, uint256 p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,bool)", p0, p1, p2)); + } + + function log(bool p0, uint256 p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,address)", p0, p1, p2)); + } + + function log(bool p0, string memory p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint256)", p0, p1, p2)); + } + + function log(bool p0, string memory p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,string)", p0, p1, p2)); + } + + function log(bool p0, string memory p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool)", p0, p1, p2)); + } + + function log(bool p0, string memory p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,address)", p0, p1, p2)); + } + + function log(bool p0, bool p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint256)", p0, p1, p2)); + } + + function log(bool p0, bool p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string)", p0, p1, p2)); + } + + function log(bool p0, bool p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool)", p0, p1, p2)); + } + + function log(bool p0, bool p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address)", p0, p1, p2)); + } + + function log(bool p0, address p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint256)", p0, p1, p2)); + } + + function log(bool p0, address p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,string)", p0, p1, p2)); + } + + function log(bool p0, address p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool)", p0, p1, p2)); + } + + function log(bool p0, address p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,address)", p0, p1, p2)); + } + + function log(address p0, uint256 p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,uint256)", p0, p1, p2)); + } + + function log(address p0, uint256 p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,string)", p0, p1, p2)); + } + + function log(address p0, uint256 p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,bool)", p0, p1, p2)); + } + + function log(address p0, uint256 p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,address)", p0, p1, p2)); + } + + function log(address p0, string memory p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,uint256)", p0, p1, p2)); + } + + function log(address p0, string memory p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,string)", p0, p1, p2)); + } + + function log(address p0, string memory p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,bool)", p0, p1, p2)); + } + + function log(address p0, string memory p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,address)", p0, p1, p2)); + } + + function log(address p0, bool p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint256)", p0, p1, p2)); + } + + function log(address p0, bool p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,string)", p0, p1, p2)); + } + + function log(address p0, bool p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool)", p0, p1, p2)); + } + + function log(address p0, bool p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,address)", p0, p1, p2)); + } + + function log(address p0, address p1, uint256 p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,uint256)", p0, p1, p2)); + } + + function log(address p0, address p1, string memory p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,string)", p0, p1, p2)); + } + + function log(address p0, address p1, bool p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,bool)", p0, p1, p2)); + } + + function log(address p0, address p1, address p2) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,address)", p0, p1, p2)); + } + + function log(uint256 p0, uint256 p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,uint256,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,uint256,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,uint256,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,string,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,string,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,string,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,string,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,bool,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,bool,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,bool,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,bool,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,address,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,address,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,address,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, uint256 p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,uint256,address,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,uint256,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,uint256,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,uint256,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,string,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,string,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,string,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,string,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,bool,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,bool,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,bool,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,bool,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,address,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,address,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,address,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, string memory p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,string,address,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,uint256,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,uint256,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,uint256,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,string,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,string,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,string,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,string,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,bool,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,bool,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,bool,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,bool,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,address,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,address,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,address,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, bool p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,bool,address,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,uint256,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,uint256,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,uint256,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,string,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,string,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,string,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,string,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,bool,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,bool,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,bool,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,bool,address)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,address,uint256)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,address,string)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,address,bool)", p0, p1, p2, p3)); + } + + function log(uint256 p0, address p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(uint256,address,address,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,uint256,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,uint256,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,uint256,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,string,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,string,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,string,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,string,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,bool,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,bool,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,bool,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,bool,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,address,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,address,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,address,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, uint256 p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,uint256,address,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,uint256,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,uint256,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,uint256,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,string,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,string,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,string,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,string,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,bool,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,address,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,address,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,address,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, string memory p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,string,address,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint256,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint256,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,uint256,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,string,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,bool,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, bool p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,bool,address,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,uint256,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,uint256,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,uint256,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,string,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,string,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,string,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,string,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,bool,address)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,address,uint256)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,address,string)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,address,bool)", p0, p1, p2, p3)); + } + + function log(string memory p0, address p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(string,address,address,address)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,uint256,string)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,uint256,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,uint256,address)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,string,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,string,string)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,string,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,string,address)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,bool,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,bool,string)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,bool,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,bool,address)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,address,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,address,string)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,address,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, uint256 p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,uint256,address,address)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint256,string)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint256,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,uint256,address)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,string)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,string,address)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,string)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,bool,address)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,string)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, string memory p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,string,address,address)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint256,string)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint256,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,uint256,address)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,string)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,string,address)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,string)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,bool,address)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,string)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, bool p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,bool,address,address)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint256,string)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint256,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,uint256,address)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,string)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,string,address)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,string)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,bool,address)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,uint256)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,string)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,bool)", p0, p1, p2, p3)); + } + + function log(bool p0, address p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(bool,address,address,address)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,uint256,string)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,uint256,bool)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,uint256,address)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,string,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,string,string)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,string,bool)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,string,address)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,bool,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,bool,string)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,bool,bool)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,bool,address)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,address,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,address,string)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,address,bool)", p0, p1, p2, p3)); + } + + function log(address p0, uint256 p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,uint256,address,address)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,uint256,string)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,uint256,bool)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,uint256,address)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,string,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,string,string)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,string,bool)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,string,address)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,string)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,bool)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,bool,address)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,address,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,address,string)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,address,bool)", p0, p1, p2, p3)); + } + + function log(address p0, string memory p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,string,address,address)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint256,string)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint256,bool)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,uint256,address)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,string)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,bool)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,string,address)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,string)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,bool)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,bool,address)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,string)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,bool)", p0, p1, p2, p3)); + } + + function log(address p0, bool p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,bool,address,address)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, uint256 p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,uint256,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, uint256 p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,uint256,string)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, uint256 p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,uint256,bool)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, uint256 p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,uint256,address)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, string memory p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,string,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, string memory p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,string,string)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, string memory p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,string,bool)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, string memory p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,string,address)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, bool p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, bool p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,string)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, bool p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,bool)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, bool p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,bool,address)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, address p2, uint256 p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,address,uint256)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, address p2, string memory p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,address,string)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, address p2, bool p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,address,bool)", p0, p1, p2, p3)); + } + + function log(address p0, address p1, address p2, address p3) internal view { + _sendLogPayload(abi.encodeWithSignature("log(address,address,address,address)", p0, p1, p2, p3)); + } + +} \ No newline at end of file diff --git a/logging/colors/constants.go b/logging/colors/constants.go index c1a7657a..4f3f555a 100644 --- a/logging/colors/constants.go +++ b/logging/colors/constants.go @@ -31,4 +31,7 @@ const ( const ( // LEFT_ARROW is the unicode string for a left arrow glyph LEFT_ARROW = "\u21fe" + + // BULLET_POINT is the unicode string for a triangular bullet point + BULLET_POINT = "\u2023" ) diff --git a/utils/combinatorial_utils.go b/utils/combinatorial_utils.go new file mode 100644 index 00000000..7eea59d4 --- /dev/null +++ b/utils/combinatorial_utils.go @@ -0,0 +1,46 @@ +package utils + +// PermutationsWithRepetition will take in an array and an integer, n, where n represents how many items need to +// be selected from the array. The function returns an array of all permutations of size n +func PermutationsWithRepetition[T any](choices []T, n int) [][]T { + numChoices := len(choices) + + // At each iteration of the for loop below, one of the indices in counter + // increments by one. Here is what selector looks like over a few iterations + // [0, 0, 0, 0] -> [1, 0, 0, 0] -> ... -> [2, 1, 0, 0] -> ... -> [4, 3, 1, 0] and so on until we reach back to + // [0, 0, 0, 0] which means all permutations have been enumerated. + counter := make([]int, n) + permutations := make([][]T, 0) + for { + // The counter will determine the order of the current permutation. The i-th value of the permutation is equal to + // the x-th index in the choices array. + permutation := make([]T, n) + for i, x := range counter { + permutation[i] = choices[x] + } + + // Add the permutation to the list of permutations + permutations = append(permutations, permutation) + + // This for loop will determine the next value of the counter array + for i := 0; ; { + // Increment the i-th index + counter[i]++ + // If we haven't updated the i-th index of counter up to numChoices - 1, we increment that index + if counter[i] < numChoices { + break + } + + // Once the i-th index is equal to numChoices, we reset counter[i] back to 0 and move on to the next index + // with i++ + counter[i] = 0 + i++ + + // Once we reach the length of the counter array, we are done with enumerating all permutations since all + // indices in the counter array have been reset back to 0 + if i == n { + return permutations + } + } + } +} From 45e26dea33b10fbe89a0678a4e9c60854d1d6b02 Mon Sep 17 00:00:00 2001 From: David Pokora Date: Fri, 18 Aug 2023 14:32:47 -0400 Subject: [PATCH 024/109] Added denomination parsing to AST value extraction (#202) * Added denomination parsing to AST constant mining * Added tests for AST value extraction, removed print from experimental code --------- Co-authored-by: anishnaik --- fuzzing/fuzzer.go | 24 +++++-- fuzzing/fuzzer_test.go | 71 +++++++++++++++++++ .../value_generation/ast_value_extraction.sol | 48 +++++++++++++ fuzzing/valuegeneration/value_set.go | 30 ++++++++ fuzzing/valuegeneration/value_set_from_ast.go | 56 ++++++++++++++- go.mod | 1 + go.sum | 2 + 7 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 8c0269c1..cb9692cd 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -12,6 +12,7 @@ import ( "math/big" "math/rand" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -723,13 +724,24 @@ func (f *Fuzzer) printMetricsLoop() { // Calculate time elapsed since the last update secondsSinceLastUpdate := time.Since(lastPrintedTime).Seconds() + // Obtain memory usage stats + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + memoryUsedMB := memStats.Alloc / 1024 / 1024 + memoryTotalMB := memStats.Sys / 1024 / 1024 + // Print a metrics update - f.logger.Info(colors.Bold, "fuzz: ", colors.Reset, - "elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset, - ", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset, - ", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset, - ", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset, - ", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset) + logBuffer := logging.NewLogBuffer() + logBuffer.Append(colors.Bold, "fuzz: ", colors.Reset) + 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) + if f.logger.Level() <= zerolog.DebugLevel { + 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) + } + f.logger.Info(logBuffer.Elements()...) // Update our delta tracking metrics lastPrintedTime = time.Now() diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 978ecbc0..55c845fd 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -1,11 +1,13 @@ package fuzzing import ( + "encoding/hex" "github.com/crytic/medusa/chain" "github.com/crytic/medusa/events" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/common" "math/big" "math/rand" "testing" @@ -595,6 +597,75 @@ func TestValueGenerationSolving(t *testing.T) { } } +// TestASTValueExtraction runs a test to ensure appropriate AST values can be mined out of a compiled source's AST. +func TestASTValueExtraction(t *testing.T) { + // Define our expected values to be mined. + expectedAddresses := []common.Address{ + common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D"), + common.HexToAddress("0x1234567890123456789012345678901234567890"), + } + expectedIntegers := []string{ + // Unsigned integer tests + "111", // no denomination + "1", // 1 wei (base unit) + "2000000000", // 2 gwei + "5000000000000000000", // 5 ether + "6", // 6 seconds (base unit) + "420", // 7 minutes + "28800", // 8 hours + "777600", // 9 days + "6048000", // 10 weeks + + // Signed integer tests + "-111", // no denomination + "-1", // 1 wei (base unit) + "-2000000000", // 2 gwei + "-5000000000000000000", // 5 ether + "-6", // 6 seconds (base unit) + "-420", // 7 minutes + "-28800", // 8 hours + "-777600", // 9 days + "-6048000", // 10 weeks + } + expectedStrings := []string{ + "testString", + "testString2", + } + expectedByteSequences := make([][]byte, 0) // no tests yet + + // Run the fuzzer test + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/value_generation/ast_value_extraction.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TestLimit = 1 // stop immediately to simply see what values were mined. + config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.PropertyTesting.Enabled = false + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + // Verify all of our expected values exist + valueSet := f.fuzzer.BaseValueSet() + for _, expectedAddr := range expectedAddresses { + assert.True(t, valueSet.ContainsAddress(expectedAddr), "Value set did not contain expected address: %v", expectedAddr.String()) + } + for _, expectedIntegerStr := range expectedIntegers { + expectedInteger, ok := new(big.Int).SetString(expectedIntegerStr, 10) + assert.True(t, ok, "Could not parse provided expected integer string in test: \"%v\"", expectedIntegerStr) + assert.True(t, valueSet.ContainsInteger(expectedInteger), "Value set did not contain expected integer: %v", expectedInteger.String()) + } + for _, expectedString := range expectedStrings { + assert.True(t, valueSet.ContainsString(expectedString), "Value set did not contain expected string: \"%v\"", expectedString) + } + for _, expectedByteSequence := range expectedByteSequences { + assert.True(t, valueSet.ContainsBytes(expectedByteSequence), "Value set did not contain expected bytes: \"%v\"", hex.EncodeToString(expectedByteSequence)) + } + }, + }) +} + // TestVMCorrectness runs tests to ensure block properties are reported consistently within the EVM, as it's configured // by the chain.TestChain. func TestVMCorrectness(t *testing.T) { diff --git a/fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol b/fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol new file mode 100644 index 00000000..02f2e99c --- /dev/null +++ b/fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol @@ -0,0 +1,48 @@ +// This contract verifies the fuzzer can extract AST literals of different subdenominations from the file. +contract TestContract { + function addressValues() public { + address x = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + assert(x != address(0x1234567890123456789012345678901234567890)); + } + function uintValues() public { + // Use all integer denoms + uint x = 111; + x = 1 wei; + x = 2 gwei; + //x = 3 szabo; + //x = 4 finney; + x = 5 ether; + x = 6 seconds; + x = 7 minutes; + x = 8 hours; + x = 9 days; + x = 10 weeks; + //x = 11 years; + + // Dummy assertion that should always pass. + assert(x != 0); + } + function intValues() public { + // Use all integer denoms + int x = -111; + x = -1 wei; + x = -2 gwei; + //x = -3 szabo; + //x = -4 finney; + x = -5 ether; + x = -6 seconds; + x = -7 minutes; + x = -8 hours; + x = -9 days; + x = -10 weeks; + //x = -11 years; + + // Dummy assertion that should always pass. + assert(x != 0); + } + function stringValues() public { + string memory s = "testString"; + s = "testString2"; + assert(true); + } +} diff --git a/fuzzing/valuegeneration/value_set.go b/fuzzing/valuegeneration/value_set.go index 77e78b99..883aab73 100644 --- a/fuzzing/valuegeneration/value_set.go +++ b/fuzzing/valuegeneration/value_set.go @@ -64,6 +64,12 @@ func (vs *ValueSet) AddAddress(a common.Address) { vs.addresses[a] = nil } +// ContainsAddress checks if an address is contained in the ValueSet. +func (vs *ValueSet) ContainsAddress(a common.Address) bool { + _, contains := vs.addresses[a] + return contains +} + // RemoveAddress removes an address item from the ValueSet. func (vs *ValueSet) RemoveAddress(a common.Address) { delete(vs.addresses, a) @@ -85,6 +91,12 @@ func (vs *ValueSet) AddInteger(b *big.Int) { vs.integers[b.String()] = b } +// ContainsInteger checks if an integer is contained in the ValueSet. +func (vs *ValueSet) ContainsInteger(b *big.Int) bool { + _, contains := vs.integers[b.String()] + return contains +} + // RemoveInteger removes an integer item from the ValueSet. func (vs *ValueSet) RemoveInteger(b *big.Int) { delete(vs.integers, b.String()) @@ -106,6 +118,12 @@ func (vs *ValueSet) AddString(s string) { vs.strings[s] = nil } +// ContainsString checks if a string is contained in the ValueSet. +func (vs *ValueSet) ContainsString(s string) bool { + _, contains := vs.strings[s] + return contains +} + // RemoveString removes a string item from the ValueSet. func (vs *ValueSet) RemoveString(s string) { delete(vs.strings, s) @@ -133,6 +151,18 @@ func (vs *ValueSet) AddBytes(b []byte) { vs.bytes[hashStr] = b } +// ContainsBytes checks if a byte sequence is contained in the ValueSet. +func (vs *ValueSet) ContainsBytes(b []byte) bool { + // Calculate hash and reset our hash provider + vs.hashProvider.Write(b) + hashStr := hex.EncodeToString(vs.hashProvider.Sum(nil)) + vs.hashProvider.Reset() + + // Check if the key exists in our lookup + _, contains := vs.bytes[hashStr] + return contains +} + // RemoveBytes removes a byte sequence item from the ValueSet. func (vs *ValueSet) RemoveBytes(b []byte) { // Calculate hash and reset our hash provider diff --git a/fuzzing/valuegeneration/value_set_from_ast.go b/fuzzing/valuegeneration/value_set_from_ast.go index 11eb9c5a..0051956c 100644 --- a/fuzzing/valuegeneration/value_set_from_ast.go +++ b/fuzzing/valuegeneration/value_set_from_ast.go @@ -2,6 +2,7 @@ package valuegeneration import ( "github.com/ethereum/go-ethereum/common" + "github.com/shopspring/decimal" "math/big" "strings" ) @@ -20,8 +21,16 @@ func (vs *ValueSet) SeedFromAst(ast any) { return // fail silently to continue walking } + // Extract the subdenomination type + tempSubdenomination, obtainedSubdenomination := node["subdenomination"].(string) + var literalSubdenomination *string + if obtainedSubdenomination { + literalSubdenomination = &tempSubdenomination + } + // Seed ValueSet with literals if literalKind == "number" { + // If it has a 0x prefix, it won't have decimals if strings.HasPrefix(literalValue, "0x") { if b, ok := big.NewInt(0).SetString(literalValue[2:], 16); ok { vs.AddInteger(b) @@ -29,7 +38,8 @@ func (vs *ValueSet) SeedFromAst(ast any) { vs.AddAddress(common.BigToAddress(b)) } } else { - if b, ok := big.NewInt(0).SetString(literalValue, 10); ok { + if decValue, err := decimal.NewFromString(literalValue); err == nil { + b := getAbsoluteValueFromDenominatedValue(decValue, literalSubdenomination) vs.AddInteger(b) vs.AddInteger(new(big.Int).Neg(b)) vs.AddAddress(common.BigToAddress(b)) @@ -42,6 +52,50 @@ func (vs *ValueSet) SeedFromAst(ast any) { }) } +// getAbsoluteValueFromDenominatedValue converts a given decimal number in a provided denomination to a big.Int +// that represents its actual calculated value. +// Note: Decimals must be used as big.Float is prone to similar mantissa-related precision issues as float32/float64. +// Returns the calculated value given the floating point number in a given denomination. +func getAbsoluteValueFromDenominatedValue(number decimal.Decimal, denomination *string) *big.Int { + // If the denomination is nil, we do nothing + if denomination == nil { + return number.BigInt() + } + + // Otherwise, switch on the type and obtain a multiplier + var multiplier decimal.Decimal + switch *denomination { + case "wei": + multiplier = decimal.NewFromFloat32(1) + case "gwei": + multiplier = decimal.NewFromFloat32(1e9) + case "szabo": + multiplier = decimal.NewFromFloat32(1e12) + case "finney": + multiplier = decimal.NewFromFloat32(1e15) + case "ether": + multiplier = decimal.NewFromFloat32(1e18) + case "seconds": + multiplier = decimal.NewFromFloat32(1) + case "minutes": + multiplier = decimal.NewFromFloat32(60) + case "hours": + multiplier = decimal.NewFromFloat32(60 * 60) + case "days": + multiplier = decimal.NewFromFloat32(60 * 60 * 24) + case "weeks": + multiplier = decimal.NewFromFloat32(60 * 60 * 24 * 7) + case "years": + multiplier = decimal.NewFromFloat32(60 * 60 * 24 * 7 * 365) + default: + multiplier = decimal.NewFromFloat32(1) + } + + // Obtain the transformed number as an integer. + transformedValue := number.Mul(multiplier) + return transformedValue.BigInt() +} + // walkAstNodes walks/iterates across an AST for each node, calling the provided walk function with each discovered node // as an argument. func walkAstNodes(ast any, walkFunc func(node map[string]any)) { diff --git a/go.mod b/go.mod index 1aec9600..83b9d462 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.29.0 + github.com/shopspring/decimal v1.3.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 1c621d8b..7721ec5e 100644 --- a/go.sum +++ b/go.sum @@ -259,6 +259,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= From fb09d620f42a0d4def31dabbbaebd12884e7fa40 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Tue, 22 Aug 2023 23:09:49 +0200 Subject: [PATCH 025/109] Update README.md (#184) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b4fc3fa..c4597293 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,10 @@ It provides parallelized fuzz testing of smart contracts through CLI, or its Go ### Precompiled binaries -To use `medusa`, first ensure you have [crytic-compile](https://github.com/crytic/crytic-compile) and a suitable compilation framework (e.g. `solc`, `hardhat`) installed on your machine. +To use `medusa`, ensure you have: + +- [crytic-compile](https://github.com/crytic/crytic-compile) (`pip3 install crytic-compile`) +- a suitable compilation framework (e.g. `solc`, `hardhat`) installed on your machine. We recommend [solc-select](https://github.com/crytic/solc-select) to quickly switch between Solidity compiler versions. You can then fetch the latest binaries for your platform from our [GitHub Releases](https://github.com/crytic/medusa/releases) page. From 928311f729aa4b1578b87f55ec0ea471ea0901c3 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 22 Aug 2023 17:54:11 -0400 Subject: [PATCH 026/109] Multi-channel and multi-error log support (#180) --- cmd/root.go | 10 +- fuzzing/fuzzer.go | 14 +- logging/init.go | 16 ++ logging/logger.go | 467 ++++++++++++++++++++++++++--------------- logging/logger_test.go | 47 +++++ 5 files changed, 375 insertions(+), 179 deletions(-) create mode 100644 logging/init.go create mode 100644 logging/logger_test.go diff --git a/cmd/root.go b/cmd/root.go index 9ec81398..37b1e941 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,7 +4,7 @@ import ( "github.com/crytic/medusa/logging" "github.com/rs/zerolog" "github.com/spf13/cobra" - "io" + "os" ) const version = "0.1.1" @@ -18,11 +18,13 @@ var rootCmd = &cobra.Command{ } // cmdLogger is the logger that will be used for the cmd package -var cmdLogger = logging.NewLogger(zerolog.InfoLevel, true, make([]io.Writer, 0)...) +var cmdLogger = logging.NewLogger(zerolog.InfoLevel) -// Execute provides an exportable function to invoke the CLI. -// Returns an error if one was encountered. +// Execute provides an exportable function to invoke the CLI. Returns an error if one was encountered. func Execute() error { + // Add stdout as an unstructured, colorized output stream for the command logger + cmdLogger.AddWriter(os.Stdout, logging.UNSTRUCTURED, true) + rootCmd.CompletionOptions.DisableDefaultCmd = true return rootCmd.Execute() } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index cb9692cd..b69df568 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -7,10 +7,9 @@ import ( "github.com/crytic/medusa/logging" "github.com/crytic/medusa/logging/colors" "github.com/rs/zerolog" - "github.com/rs/zerolog/pkgerrors" - "io" "math/big" "math/rand" + "os" "path/filepath" "runtime" "sort" @@ -89,12 +88,11 @@ type Fuzzer struct { // NewFuzzer returns an instance of a new Fuzzer provided a project configuration, or an error if one is encountered // while initializing the code. func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { - // Create the global logger, set some global logging parameters, and enable terminal coloring - logging.GlobalLogger = logging.NewLogger(config.Logging.Level, true, make([]io.Writer, 0)...) - zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + // Create the global logger and add stdout as an unstructured, colored output stream + logging.GlobalLogger = logging.NewLogger(config.Logging.Level) + logging.GlobalLogger.AddWriter(os.Stdout, logging.UNSTRUCTURED, true) - // If the log directory is a non-empty string, create a file for file logging + // If the log directory is a non-empty string, create a file for unstructured, un-colorized file logging if config.Logging.LogDirectory != "" { // Filename will be the "log-current_unix_timestamp.log" filename := "log-" + strconv.FormatInt(time.Now().Unix(), 10) + ".log" @@ -104,7 +102,7 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { logging.GlobalLogger.Error("Failed to create log file", err) return nil, err } - logging.GlobalLogger.AddWriter(file, logging.UNSTRUCTURED) + logging.GlobalLogger.AddWriter(file, logging.UNSTRUCTURED, false) } // Get the fuzzer's custom sub-logger diff --git a/logging/init.go b/logging/init.go new file mode 100644 index 00000000..0f47cf81 --- /dev/null +++ b/logging/init.go @@ -0,0 +1,16 @@ +package logging + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" +) + +// init will instantiate the global logger and set up some global parameters from the zerolog package. +func init() { + // Instantiate the global logger + GlobalLogger = NewLogger(zerolog.Disabled) + + // Setup stack trace support and set the timestamp format to UNIX + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix +} diff --git a/logging/logger.go b/logging/logger.go index 6385d182..48d5c539 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -11,25 +11,31 @@ import ( // GlobalLogger describes a Logger that is disabled by default and is instantiated when the fuzzer is created. Each module/package // should create its own sub-logger. This allows to create unique logging instances depending on the use case. -var GlobalLogger = NewLogger(zerolog.Disabled, false, nil) +var GlobalLogger *Logger -// Logger describes a custom logging object that can log events to any arbitrary channel and can handle specialized -// output to console as well +// Logger describes a custom logging object that can log events to any arbitrary channel in structured, unstructured with colors, +// and unstructured formats. type Logger struct { // level describes the log level level zerolog.Level - // multiLogger describes a logger that will be used to output logs to any arbitrary channel(s) in either structured - // or unstructured format. - multiLogger zerolog.Logger + // structuredLogger describes a logger that will be used to output structured logs to any arbitrary channel. + structuredLogger zerolog.Logger - // consoleLogger describes a logger that will be used to output unstructured output to console. - // We are creating a separate logger for console so that we can support specialized formatting / custom coloring. - consoleLogger zerolog.Logger + // structuredWriters describes the various channels that the output from the structuredLogger will go to. + structuredWriters []io.Writer - // writers describes a list of io.Writer objects where log output will go. This writers list can be appended to / - // removed from. - writers []io.Writer + // unstructuredLogger describes a logger that will be used to stream un-colorized, unstructured output to any arbitrary channel. + unstructuredLogger zerolog.Logger + + // unstructuredWriters describes the various channels that the output from the unstructuredLogger will go to. + unstructuredWriters []io.Writer + + // unstructuredColorLogger describes a logger that will be used to stream colorized, unstructured output to any arbitrary channel. + unstructuredColorLogger zerolog.Logger + + // unstructuredColorWriters describes the various channels that the output from the unstructuredColoredLogger will go to. + unstructuredColorWriters []io.Writer } // LogFormat describes what format to log in @@ -45,73 +51,144 @@ const ( // StructuredLogInfo describes a key-value mapping that can be used to log structured data type StructuredLogInfo map[string]any -// NewLogger will create a new Logger object with a specific log level. The Logger can output to console, if enabled, -// and output logs to any number of arbitrary io.Writer channels -func NewLogger(level zerolog.Level, consoleEnabled bool, writers ...io.Writer) *Logger { - // The two base loggers are effectively loggers that are disabled - // We are creating instances of them so that we do not get nil pointer dereferences down the line - baseMultiLogger := zerolog.New(os.Stdout).Level(zerolog.Disabled) - baseConsoleLogger := zerolog.New(os.Stdout).Level(zerolog.Disabled) - - // If we are provided a list of writers, update the multi logger - if len(writers) > 0 { - baseMultiLogger = zerolog.New(zerolog.MultiLevelWriter(writers...)).Level(level).With().Timestamp().Logger() - } - - // If console logging is enabled, update the console logger - if consoleEnabled { - consoleWriter := setupDefaultFormatting(zerolog.ConsoleWriter{Out: os.Stdout}, level) - baseConsoleLogger = zerolog.New(consoleWriter).Level(level) - } - +// NewLogger will create a new Logger object with a specific log level. By default, a logger that is instantiated +// with this function is not usable until a log channel is added. To add or remove channels that the logger +// streams logs to, call the Logger.AddWriter and Logger.RemoveWriter functions. +func NewLogger(level zerolog.Level) *Logger { return &Logger{ - level: level, - multiLogger: baseMultiLogger, - consoleLogger: baseConsoleLogger, - writers: writers, + level: level, + structuredLogger: zerolog.New(os.Stdout).Level(zerolog.Disabled), + structuredWriters: make([]io.Writer, 0), + unstructuredLogger: zerolog.New(os.Stdout).Level(zerolog.Disabled), + unstructuredWriters: make([]io.Writer, 0), + unstructuredColorLogger: zerolog.New(os.Stdout).Level(zerolog.Disabled), + unstructuredColorWriters: make([]io.Writer, 0), } } // NewSubLogger will create a new Logger with unique context in the form of a key-value pair. The expected use of this -// function is for each package to have their own unique logger so that parsing of logs is "grep-able" based on some key +// is for each module or component of the system to create their own contextualized logs. The key can be used to search +// for logs from a specific module or component. func (l *Logger) NewSubLogger(key string, value string) *Logger { - subFileLogger := l.multiLogger.With().Str(key, value).Logger() - subConsoleLonger := l.consoleLogger.With().Str(key, value).Logger() + // Create the sub-loggers with the new key-value context + subStructuredLogger := l.structuredLogger.With().Str(key, value).Logger() + subUnstructuredColoredLogger := l.unstructuredColorLogger.With().Str(key, value).Logger() + subUnstructuredLogger := l.unstructuredLogger.With().Str(key, value).Logger() + + // Create new slices for the writers since we want to make a deep copy for each one + subStructuredWriters := make([]io.Writer, len(l.structuredWriters)) + copy(subStructuredWriters, l.structuredWriters) + + subUnstructuredColorWriters := make([]io.Writer, len(l.unstructuredColorWriters)) + copy(subUnstructuredColorWriters, l.unstructuredColorWriters) + + subUnstructuredWriters := make([]io.Writer, len(l.unstructuredWriters)) + copy(subUnstructuredWriters, l.unstructuredWriters) + + // Return a new logger return &Logger{ - level: l.level, - multiLogger: subFileLogger, - consoleLogger: subConsoleLonger, - writers: l.writers, + level: l.level, + structuredLogger: subStructuredLogger, + structuredWriters: subStructuredWriters, + unstructuredColorLogger: subUnstructuredColoredLogger, + unstructuredColorWriters: subUnstructuredColorWriters, + unstructuredLogger: subUnstructuredLogger, + unstructuredWriters: subUnstructuredWriters, } } -// AddWriter will add a writer to the list of channels where log output will be sent. -func (l *Logger) AddWriter(writer io.Writer, format LogFormat) { - // Check to see if the writer is already in the array of writers - for _, w := range l.writers { - if writer == w { - return +// AddWriter will add a writer to which log output will go to. If the format is structured then the writer will get +// structured output. If the writer is unstructured, then the writer has the choice to either receive colored or un-colored +// output. Note that unstructured writers will be converted into a zerolog.ConsoleWriter to maintain the same format +// across all unstructured output streams. +func (l *Logger) AddWriter(writer io.Writer, format LogFormat, colored bool) { + // First, try to add the writer to the list of channels that want structured logs + if format == STRUCTURED { + for _, w := range l.structuredWriters { + if w == writer { + // Writer already exists, return + return + } } + // Add the writer and recreate the logger + l.structuredWriters = append(l.structuredWriters, writer) + l.structuredLogger = zerolog.New(zerolog.MultiLevelWriter(l.structuredWriters...)).Level(l.level).With().Timestamp().Logger() + return } - // If we want unstructured output, wrap the base writer object into a console writer so that we get unstructured output with no ANSI coloring - if format == UNSTRUCTURED { - writer = zerolog.ConsoleWriter{Out: writer, NoColor: true} + // Now that we know we are going to create an unstructured writer, we will create an unstructured writer with(out) coloring + // using zerolog's console writer object. + unstructuredWriter := formatUnstructuredWriter(writer, l.level, colored) + + // Now, try to add the writer to the list of channels that want unstructured, colored logs + if format == UNSTRUCTURED && colored { + for _, w := range l.unstructuredColorWriters { + // We must convert the writer to a console writer to correctly check for existence within the list + if w.(zerolog.ConsoleWriter).Out == writer { + // Writer already exists, return + return + } + } + // Add the unstructured writer and recreate the logger + l.unstructuredColorWriters = append(l.unstructuredColorWriters, unstructuredWriter) + l.unstructuredColorLogger = zerolog.New(zerolog.MultiLevelWriter(l.unstructuredColorWriters...)).Level(l.level).With().Timestamp().Logger() } - // Add it to the list of writers and update the multi logger - l.writers = append(l.writers, writer) - l.multiLogger = zerolog.New(zerolog.MultiLevelWriter(l.writers...)).Level(l.level).With().Timestamp().Logger() + // Otherwise, try to add the writer to the list of channels that want unstructured, un-colored logs + if format == UNSTRUCTURED && !colored { + for _, w := range l.unstructuredWriters { + // We must convert the writer to a console writer to correctly check for existence within the list + if w.(zerolog.ConsoleWriter).Out == writer { + // Writer already exists, return + return + } + } + // Add the unstructured writer and recreate the logger + l.unstructuredWriters = append(l.unstructuredWriters, unstructuredWriter) + l.unstructuredLogger = zerolog.New(zerolog.MultiLevelWriter(l.unstructuredWriters...)).Level(l.level).With().Timestamp().Logger() + } } -// RemoveWriter will remove a writer from the list of writers that the logger manages. If the writer does not exist, this -// function is a no-op -func (l *Logger) RemoveWriter(writer io.Writer) { - // Iterate through the writers - for i, w := range l.writers { - if writer == w { - // Create a new slice without the writer at index i - l.writers = append(l.writers[:i], l.writers[i+1]) +// RemoveWriter will remove a writer from the list of writers that the logger manages. The writer will be either removed +// from the list of structured, unstructured and colored, or unstructured and un-colored writers. If the same writer +// is receiving multiple types of log output (e.g. structured and unstructured with color) then this function must be called +// multiple times. If the writer does not exist in any list, then this function is a no-op. +func (l *Logger) RemoveWriter(writer io.Writer, format LogFormat, colored bool) { + // First, try to remove the writer from the list of structured writers + if format == STRUCTURED { + // Check for writer existence + for i, w := range l.structuredWriters { + if w == writer { + // Remove the writer and recreate the logger + l.structuredWriters = append(l.structuredWriters[:i], l.structuredWriters[i+1:]...) + l.structuredLogger = zerolog.New(zerolog.MultiLevelWriter(l.structuredWriters...)).Level(l.level).With().Timestamp().Logger() + } + } + } + + // Now, try to remove the writer from the list of unstructured, colored writers + if format == UNSTRUCTURED && colored { + // Check for writer existence + for i, w := range l.unstructuredColorWriters { + // We must convert the writer to a console writer to correctly check for existence within the list + if w.(zerolog.ConsoleWriter).Out == writer { + // Remove the writer and recreate the logger + l.unstructuredColorWriters = append(l.unstructuredColorWriters[:i], l.unstructuredColorWriters[i+1:]...) + l.unstructuredColorLogger = zerolog.New(zerolog.MultiLevelWriter(l.unstructuredColorWriters...)).Level(l.level).With().Timestamp().Logger() + } + } + } + + // Otherwise, try to remove the writer from the list of unstructured, un-colored writers + if format == UNSTRUCTURED && !colored { + // Check for writer existence + for i, w := range l.unstructuredWriters { + // We must convert the writer to a console writer to correctly check for existence within the list + if w.(zerolog.ConsoleWriter).Out == writer { + // Remove the writer and recreate the logger + l.unstructuredWriters = append(l.unstructuredWriters[:i], l.unstructuredWriters[i+1:]...) + l.unstructuredLogger = zerolog.New(zerolog.MultiLevelWriter(l.unstructuredWriters...)).Level(l.level).With().Timestamp().Logger() + } } } } @@ -124,111 +201,104 @@ func (l *Logger) Level() zerolog.Level { // SetLevel will update the log level of the Logger func (l *Logger) SetLevel(level zerolog.Level) { l.level = level - l.multiLogger = l.multiLogger.Level(level) - l.consoleLogger = l.consoleLogger.Level(level) + + // Update the level of each underlying logger + l.structuredLogger = l.structuredLogger.Level(level) + l.unstructuredColorLogger = l.unstructuredColorLogger.Level(level) + l.unstructuredLogger = l.unstructuredLogger.Level(level) } // Trace is a wrapper function that will log a trace event func (l *Logger) Trace(args ...any) { - // Build the messages and retrieve any error or associated structured log info - consoleMsg, multiMsg, err, info := buildMsgs(args...) + // Build the messages and retrieve any errors or associated structured log info + colorMsg, noColorMsg, errs, info := buildMsgs(args...) // Instantiate log events - consoleLog := l.consoleLogger.Trace() - multiLog := l.multiLogger.Trace() - - // Chain the error - chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + structuredLog := l.structuredLogger.Trace() + unstructuredColoredLog := l.unstructuredColorLogger.Trace() + unstructuredLog := l.unstructuredLogger.Trace() - // Chain the structured log info and messages and send off the logs - chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) + // Chain the structured log info, errors, and messages and send off the logs + chainStructuredLogInfoErrorsAndMsgs(structuredLog, unstructuredColoredLog, unstructuredLog, info, errs, colorMsg, noColorMsg) } // Debug is a wrapper function that will log a debug event func (l *Logger) Debug(args ...any) { - // Build the messages and retrieve any error or associated structured log info - consoleMsg, multiMsg, err, info := buildMsgs(args...) + // Build the messages and retrieve any errors or associated structured log info + colorMsg, noColorMsg, errs, info := buildMsgs(args...) // Instantiate log events - consoleLog := l.consoleLogger.Debug() - multiLog := l.multiLogger.Debug() + structuredLog := l.structuredLogger.Debug() + unstructuredColoredLog := l.unstructuredColorLogger.Debug() + unstructuredLog := l.unstructuredLogger.Debug() - // Chain the error - chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) - - // Chain the structured log info and messages and send off the logs - chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) + // Chain the structured log info, errors, and messages and send off the logs + chainStructuredLogInfoErrorsAndMsgs(structuredLog, unstructuredColoredLog, unstructuredLog, info, errs, colorMsg, noColorMsg) } // Info is a wrapper function that will log an info event func (l *Logger) Info(args ...any) { - // Build the messages and retrieve any error or associated structured log info - consoleMsg, multiMsg, err, info := buildMsgs(args...) + // Build the messages and retrieve any errors or associated structured log info + colorMsg, noColorMsg, errs, info := buildMsgs(args...) // Instantiate log events - consoleLog := l.consoleLogger.Info() - multiLog := l.multiLogger.Info() - - // Chain the error - chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + structuredLog := l.structuredLogger.Info() + unstructuredColoredLog := l.unstructuredColorLogger.Info() + unstructuredLog := l.unstructuredLogger.Info() - // Chain the structured log info and messages and send off the logs - chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) + // Chain the structured log info, errors, and messages and send off the logs + chainStructuredLogInfoErrorsAndMsgs(structuredLog, unstructuredColoredLog, unstructuredLog, info, errs, colorMsg, noColorMsg) } // Warn is a wrapper function that will log a warning event both on console func (l *Logger) Warn(args ...any) { - // Build the messages and retrieve any error or associated structured log info - consoleMsg, multiMsg, err, info := buildMsgs(args...) + // Build the messages and retrieve any errors or associated structured log info + colorMsg, noColorMsg, errs, info := buildMsgs(args...) // Instantiate log events - consoleLog := l.consoleLogger.Warn() - multiLog := l.multiLogger.Warn() - - // Chain the error - chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) + structuredLog := l.structuredLogger.Warn() + unstructuredColoredLog := l.unstructuredColorLogger.Warn() + unstructuredLog := l.unstructuredLogger.Warn() - // Chain the structured log info and messages and send off the logs - chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) + // Chain the structured log info, errors, and messages and send off the logs + chainStructuredLogInfoErrorsAndMsgs(structuredLog, unstructuredColoredLog, unstructuredLog, info, errs, colorMsg, noColorMsg) } // Error is a wrapper function that will log an error event. func (l *Logger) Error(args ...any) { - // Build the messages and retrieve any error or associated structured log info - consoleMsg, multiMsg, err, info := buildMsgs(args...) + // Build the messages and retrieve any errors or associated structured log info + colorMsg, noColorMsg, errs, info := buildMsgs(args...) // Instantiate log events - consoleLog := l.consoleLogger.Error() - multiLog := l.multiLogger.Error() + structuredLog := l.structuredLogger.Error() + unstructuredColoredLog := l.unstructuredColorLogger.Error() + unstructuredLog := l.unstructuredLogger.Error() - // Chain the error - chainError(consoleLog, multiLog, err, l.level <= zerolog.DebugLevel) - - // Chain the structured log info and messages and send off the logs - chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) + // Chain the structured log info, errors, and messages and send off the logs + chainStructuredLogInfoErrorsAndMsgs(structuredLog, unstructuredColoredLog, unstructuredLog, info, errs, colorMsg, noColorMsg) } // Panic is a wrapper function that will log a panic event func (l *Logger) Panic(args ...any) { - // Build the messages and retrieve any error or associated structured log info - consoleMsg, multiMsg, err, info := buildMsgs(args...) + // Build the messages and retrieve any errors or associated structured log info + colorMsg, noColorMsg, errs, info := buildMsgs(args...) // Instantiate log events - consoleLog := l.consoleLogger.Panic() - multiLog := l.multiLogger.Panic() - - // Chain the error - chainError(consoleLog, multiLog, err, true) + structuredLog := l.structuredLogger.Panic() + unstructuredColoredLog := l.unstructuredColorLogger.Panic() + unstructuredLog := l.unstructuredLogger.Panic() - // Chain the structured log info and messages and send off the logs - chainStructuredLogInfoAndMsgs(consoleLog, multiLog, info, consoleMsg, multiMsg) + // Chain the structured log info, errors, and messages and send off the logs + chainStructuredLogInfoErrorsAndMsgs(structuredLog, unstructuredColoredLog, unstructuredLog, info, errs, colorMsg, noColorMsg) } // buildMsgs describes a function that takes in a variadic list of arguments of any type and returns two strings and, -// optionally, an error and a StructuredLogInfo object. The first string will be a colorized-string that can be used for -// console logging while the second string will be a non-colorized one that can be used for file/structured logging. -// The error and the StructuredLogInfo can be used to add additional context to log messages -func buildMsgs(args ...any) (string, string, error, StructuredLogInfo) { +// optionally, a list of errors and a StructuredLogInfo object. The first string will be a colorized-message while the +// second string will be a non-colorized one. Colors are applied if one or more of the input arguments are of type +// colors.ColorFunc. The colorized message can be used for channels that request unstructured, colorized log output +// while the non-colorized one can be used for structured streams and unstructured streams that don't want color. The +// errors and the StructuredLogInfo can be used to add additional context to log messages. +func buildMsgs(args ...any) (string, string, []error, StructuredLogInfo) { // Guard clause if len(args) == 0 { return "", "", nil, nil @@ -236,10 +306,10 @@ func buildMsgs(args ...any) (string, string, error, StructuredLogInfo) { // Initialize the base color context, the string buffers and the structured log info object colorCtx := colors.Reset - consoleOutput := make([]string, 0) - fileOutput := make([]string, 0) + colorMsg := make([]string, 0) + noColorMsg := make([]string, 0) + errs := make([]error, 0) var info StructuredLogInfo - var err error // Iterate through each argument in the list and switch on type for _, arg := range args { @@ -251,86 +321,154 @@ func buildMsgs(args ...any) (string, string, error, StructuredLogInfo) { // Note that only one structured log info can be provided for each log message info = t case error: - // Note that only one error can be provided for each log message - err = t + // Append error to the list of errors + errs = append(errs, t) default: - // In the base case, append the object to the two string buffers. The console string buffer will have the + // In the base case, append the object to the two string buffers. The colored string buffer will have the // current color context applied to it. - consoleOutput = append(consoleOutput, colorCtx(t)) - fileOutput = append(fileOutput, fmt.Sprintf("%v", t)) + colorMsg = append(colorMsg, colorCtx(t)) + noColorMsg = append(noColorMsg, fmt.Sprintf("%v", t)) } } - return strings.Join(consoleOutput, ""), strings.Join(fileOutput, ""), err, info + return strings.Join(colorMsg, ""), strings.Join(noColorMsg, ""), errs, info } -// chainError is a helper function that takes in a *zerolog.Event for console and multi-log output and chains an error -// to both events. If debug is true, then a stack trace is added to both events as well. -func chainError(consoleLog *zerolog.Event, multiLog *zerolog.Event, err error, debug bool) { - // First append the errors to each event. Note that even if err is nil, there will not be a panic here - consoleLog.Err(err) - multiLog.Err(err) - - // If we are in debug mode or below, then we will add the stack traces as well for debugging - if debug { - consoleLog.Stack() - multiLog.Stack() +// chainStructuredLogInfoErrorsAndMsgs describes a function that takes in a *zerolog.Event for the structured, unstructured +// with color, and unstructured without colors log streams, chains any StructuredLogInfo and errors provided to it, +// adds the associated messages, and sends out the logs to their respective channels. Note that the StructuredLogInfo object +// is only appended to the structured log event and not to the unstructured ones. Additionally, note that errors are appended as a +// formatted bulleted list for unstructured logging while for the structured logger they get appended as a key-value pair. +func chainStructuredLogInfoErrorsAndMsgs(structuredLog *zerolog.Event, unstructuredColoredLog *zerolog.Event, unstructuredLog *zerolog.Event, info StructuredLogInfo, errs []error, colorMsg string, noColorMsg string) { + // First, we need to create a formatted error string for unstructured output + var errStr string + for _, err := range errs { + // To make the formatting a little nicer, we will add a tab after each new line in the error so that + // errors can be better differentiated on unstructured channels + lines := make([]string, 0) + for i, line := range strings.Split(err.Error(), "\n") { + // Add a tab to the line only after the first new line in the error message + if i != 0 { + line = "\t" + line + } + lines = append(lines, line) + } + + // Update the error string to be based on the tabbed lines array + if len(lines) > 0 { + err = fmt.Errorf("%v", strings.Join(lines, "\n")) + } + + // Append a bullet point and the formatted error to the error string + errStr += "\n" + colors.BULLET_POINT + " " + err.Error() + } + + // Add structured error element to the multi-log output and append the error string to the console message + // TODO: Add support for stack traces in the future + if len(errs) != 0 { + structuredLog.Errs("errors", errs) + } + + // The structured message will be the one without any potential errors appended to it since the errors will be provided + // as a key-value pair + structuredMsg := noColorMsg + + // Add the colorized and non-colorized version of the error string to the colorized and non-colorized messages, respectively. + if len(errStr) > 0 { + colorMsg += colors.Red(errStr) + noColorMsg += errStr } -} -// chainStructuredLogInfoAndMsgs is a helper function that takes in a *zerolog.Event for console and multi-log output, -// chains any StructuredLogInfo provided to it, adds the associated messages, and sends out the logs to their respective -// channels. -func chainStructuredLogInfoAndMsgs(consoleLog *zerolog.Event, multiLog *zerolog.Event, info StructuredLogInfo, consoleMsg string, multiMsg string) { - // If we are provided a structured log info object, add that as a key-value pair to the events + // If we are provided a structured log info object, add that as a key-value pair to the structured log event if info != nil { - consoleLog.Any("info", info) - multiLog.Any("info", info) + structuredLog.Any("info", info) } // Append the messages to each event. This will also result in the log events being sent out to their respective - // streams. Note that we are deferring the msg to multi logger in case we are logging a panic and want to make sure that - // all channels receive the panic log - defer multiLog.Msg(multiMsg) - consoleLog.Msg(consoleMsg) + // streams. Note that we are deferring the message to two of the three loggers multi logger in case we are logging a panic + // and want to make sure that all channels receive the panic log. + defer func() { + structuredLog.Msg(structuredMsg) + unstructuredLog.Msg(noColorMsg) + }() + unstructuredColoredLog.Msg(colorMsg) } -// setupDefaultFormatting will update the console logger's formatting to the medusa standard -func setupDefaultFormatting(writer zerolog.ConsoleWriter, level zerolog.Level) zerolog.ConsoleWriter { - // Get rid of the timestamp for console output - writer.FormatTimestamp = func(i interface{}) string { +// formatUnstructuredWriter will create a custom-formatted zerolog.ConsoleWriter from an arbitrary io.Writer. A zerolog.ConsoleWriter is +// what is used under-the-hood to support unstructured log output. Custom formatting is applied to specific fields, +// timestamps, and the log level strings. If requested, coloring may be applied to the log level strings. +func formatUnstructuredWriter(writer io.Writer, level zerolog.Level, colored bool) zerolog.ConsoleWriter { + // Create the console writer + consoleWriter := zerolog.ConsoleWriter{Out: writer, NoColor: !colored} + + // Get rid of the timestamp for unstructured output + consoleWriter.FormatTimestamp = func(i interface{}) string { return "" } - // We will define a custom format for each level - writer.FormatLevel = func(i any) string { + // If we are above debug level, we want to get rid of the `module` component when logging to unstructured streams + if level > zerolog.DebugLevel { + consoleWriter.FieldsExclude = []string{"module"} + } + + // If coloring is enabled, we will return a custom, colored string for each log severity level + // Otherwise, we will just return a non-colorized string for each log severity level + consoleWriter.FormatLevel = func(i any) string { // Create a level object for better switch logic level, err := zerolog.ParseLevel(i.(string)) if err != nil { panic(fmt.Sprintf("unable to parse the log level: %v", err)) } - // Switch on the level and return a custom, colored string + // Switch on the level switch level { case zerolog.TraceLevel: + if !colored { + // No coloring for "trace" string + return zerolog.LevelTraceValue + } // Return a bold, cyan "trace" string return colors.CyanBold(zerolog.LevelTraceValue) case zerolog.DebugLevel: + if !colored { + // No coloring for "debug" string + return zerolog.LevelDebugValue + } // Return a bold, blue "debug" string return colors.BlueBold(zerolog.LevelDebugValue) case zerolog.InfoLevel: + if !colored { + // Return a left arrow without any coloring + return colors.LEFT_ARROW + } // Return a bold, green left arrow return colors.GreenBold(colors.LEFT_ARROW) case zerolog.WarnLevel: + if !colored { + // No coloring for "warn" string + return zerolog.LevelWarnValue + } // Return a bold, yellow "warn" string return colors.YellowBold(zerolog.LevelWarnValue) case zerolog.ErrorLevel: + if !colored { + // No coloring for "err" string + return zerolog.LevelErrorValue + } // Return a bold, red "err" string return colors.RedBold(zerolog.LevelErrorValue) case zerolog.FatalLevel: + if !colored { + // No coloring for "fatal" string + return zerolog.LevelFatalValue + } // Return a bold, red "fatal" string return colors.RedBold(zerolog.LevelFatalValue) case zerolog.PanicLevel: + if !colored { + // No coloring for "panic" string + return zerolog.LevelPanicValue + } // Return a bold, red "panic" string return colors.RedBold(zerolog.LevelPanicValue) default: @@ -338,10 +476,5 @@ func setupDefaultFormatting(writer zerolog.ConsoleWriter, level zerolog.Level) z } } - // If we are above debug level, we want to get rid of the `module` component when logging to console - if level > zerolog.DebugLevel { - writer.FieldsExclude = []string{"module"} - } - - return writer + return consoleWriter } diff --git a/logging/logger_test.go b/logging/logger_test.go new file mode 100644 index 00000000..15404b83 --- /dev/null +++ b/logging/logger_test.go @@ -0,0 +1,47 @@ +package logging + +import ( + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +// TestAddAndRemoveWriter will test to Logger.AddWriter and Logger.RemoveWriter functions to ensure that they work as expected. +func TestAddAndRemoveWriter(t *testing.T) { + // Create a base logger + logger := NewLogger(zerolog.InfoLevel) + + // Add three types of writers + // 1. Unstructured and colorized output to stdout + logger.AddWriter(os.Stdout, UNSTRUCTURED, true) + // 2. Unstructured and non-colorized output to stderr + logger.AddWriter(os.Stderr, UNSTRUCTURED, false) + // 3. Structured output to stdin + logger.AddWriter(os.Stdin, STRUCTURED, false) + + // We should expect the underlying data structures are correctly updated + assert.Equal(t, len(logger.unstructuredWriters), 1) + assert.Equal(t, len(logger.unstructuredColorWriters), 1) + assert.Equal(t, len(logger.structuredWriters), 1) + + // Try to add duplicate writers + logger.AddWriter(os.Stdout, UNSTRUCTURED, true) + logger.AddWriter(os.Stderr, UNSTRUCTURED, false) + logger.AddWriter(os.Stdin, STRUCTURED, false) + + // Ensure that the lengths of the lists have not changed + assert.Equal(t, len(logger.unstructuredWriters), 1) + assert.Equal(t, len(logger.unstructuredColorWriters), 1) + assert.Equal(t, len(logger.structuredWriters), 1) + + // Remove each writer + logger.RemoveWriter(os.Stdout, UNSTRUCTURED, true) + logger.RemoveWriter(os.Stderr, UNSTRUCTURED, false) + logger.RemoveWriter(os.Stdin, STRUCTURED, false) + + // We should expect the underlying data structures are correctly updated + assert.Equal(t, len(logger.unstructuredWriters), 0) + assert.Equal(t, len(logger.unstructuredColorWriters), 0) + assert.Equal(t, len(logger.structuredWriters), 0) +} From ac99e78ee38df86a8afefb21f105be9e4eae46ee Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 23 Aug 2023 13:22:57 -0400 Subject: [PATCH 027/109] Update version (#210) --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 37b1e941..ae03d15d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.1" +const version = "0.1.2" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From d0509afedefeacac287869b28395d5b8b9881bf3 Mon Sep 17 00:00:00 2001 From: Exca-DK Date: Mon, 25 Sep 2023 16:03:25 +0200 Subject: [PATCH 028/109] Support NoColor and --no-color (#222) * Console output with optional coloring * optional Colorize * LF and CRLF line endings * update no-color flag description * code review changes --------- Authored-by: Exca-DK --- cmd/fuzz_flags.go | 12 +++++++++++ fuzzing/config/config.go | 6 +++++- fuzzing/config/config_defaults.go | 1 + fuzzing/fuzzer.go | 17 ++++++++++------ logging/colors/colorize_unix.go | 14 +++++++++++-- logging/colors/colorize_windows.go | 6 +++++- logging/logger_test.go | 32 ++++++++++++++++++++++++++++-- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index 02300ecb..e1999804 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -68,6 +68,10 @@ func addFuzzFlags() error { // Trace all fuzzCmd.Flags().Bool("trace-all", false, fmt.Sprintf("print the execution trace for every element in a shrunken call sequence instead of only the last element (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.TraceAll)) + + // Logging color + fuzzCmd.Flags().Bool("no-color", false, "disabled colored terminal output") + return nil } @@ -176,5 +180,13 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. return err } } + + // Update logging color mode + if cmd.Flags().Changed("no-color") { + projectConfig.Logging.NoColor, err = cmd.Flags().GetBool("no-color") + if err != nil { + return err + } + } return nil } diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 283e5329..eac6c371 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -3,9 +3,10 @@ package config import ( "encoding/json" "errors" + "os" + "github.com/crytic/medusa/chain/config" "github.com/rs/zerolog" - "os" "github.com/crytic/medusa/compilation" "github.com/crytic/medusa/utils" @@ -189,6 +190,9 @@ type LoggingConfig struct { // LogDirectory describes what directory log files should be outputted in/ LogDirectory being a non-empty string is // equivalent to enabling file logging. LogDirectory string `json:"logDirectory"` + + // NoColor indicates whether or not log messages should be displayed with colored formatting. + NoColor bool `json:"noColor"` } // ConsoleLoggingConfig describes the configuration options for logging to console. Note that this not being used right now diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 2a6e92f0..e82f1a1a 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -83,6 +83,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { Logging: LoggingConfig{ Level: zerolog.InfoLevel, LogDirectory: "", + NoColor: false, }, } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index b69df568..4686eba8 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -3,10 +3,6 @@ package fuzzing import ( "context" "fmt" - "github.com/crytic/medusa/fuzzing/coverage" - "github.com/crytic/medusa/logging" - "github.com/crytic/medusa/logging/colors" - "github.com/rs/zerolog" "math/big" "math/rand" "os" @@ -18,6 +14,11 @@ import ( "sync" "time" + "github.com/crytic/medusa/fuzzing/coverage" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/core/types" @@ -88,9 +89,13 @@ type Fuzzer struct { // NewFuzzer returns an instance of a new Fuzzer provided a project configuration, or an error if one is encountered // while initializing the code. func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { - // Create the global logger and add stdout as an unstructured, colored output stream + // Disable colors if requested + if config.Logging.NoColor { + colors.DisableColor() + } + // Create the global logger and add stdout as an unstructured output stream logging.GlobalLogger = logging.NewLogger(config.Logging.Level) - logging.GlobalLogger.AddWriter(os.Stdout, logging.UNSTRUCTURED, true) + logging.GlobalLogger.AddWriter(os.Stdout, logging.UNSTRUCTURED, !config.Logging.NoColor) // If the log directory is a non-empty string, create a file for unstructured, un-colorized file logging if config.Logging.LogDirectory != "" { diff --git a/logging/colors/colorize_unix.go b/logging/colors/colorize_unix.go index 89ea510d..ba404662 100644 --- a/logging/colors/colorize_unix.go +++ b/logging/colors/colorize_unix.go @@ -5,11 +5,21 @@ package colors import "fmt" -// EnableColor is a no-op function for non-windows systems because we know that they support ANSI escape codes -func EnableColor() {} +var enabled = true + +// EnableColor enables the use of colors for non-windows systems. +func EnableColor() { enabled = true } + +// DisableColor disables the use of colors for non-windows systems. +func DisableColor() { enabled = false } // Colorize returns the string s wrapped in ANSI code c for non-windows systems // Source: https://github.com/rs/zerolog/blob/4fff5db29c3403bc26dee9895e12a108aacc0203/console.go func Colorize(s any, c Color) string { + // Return original string if explicitly disabled + if !enabled { + return fmt.Sprintf("%v", s) + } + return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s) } diff --git a/logging/colors/colorize_windows.go b/logging/colors/colorize_windows.go index 87266688..4e57c269 100644 --- a/logging/colors/colorize_windows.go +++ b/logging/colors/colorize_windows.go @@ -5,8 +5,9 @@ package colors import ( "fmt" - "golang.org/x/sys/windows" "os" + + "golang.org/x/sys/windows" ) var enabled bool @@ -53,6 +54,9 @@ func EnableColor() { } } +// DisableColor will disable colors +func DisableColor() { enabled = false } + // Colorize returns the string s wrapped in ANSI code c assuming that ANSI is supported on the Windows version // Source: https://github.com/rs/zerolog/blob/4fff5db29c3403bc26dee9895e12a108aacc0203/console.go func Colorize(s any, c Color) string { diff --git a/logging/logger_test.go b/logging/logger_test.go index 15404b83..540e78e3 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -1,10 +1,15 @@ package logging import ( - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" + "bytes" + "fmt" "os" + "strings" "testing" + + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" ) // TestAddAndRemoveWriter will test to Logger.AddWriter and Logger.RemoveWriter functions to ensure that they work as expected. @@ -45,3 +50,26 @@ func TestAddAndRemoveWriter(t *testing.T) { assert.Equal(t, len(logger.unstructuredColorWriters), 0) assert.Equal(t, len(logger.structuredWriters), 0) } + +// TestDisabledColors verifies the behavior of the unstructured colored logger when colors are disabled, +// ensuring that it does not output colors when the color feature is turned off. +func TestDisabledColors(t *testing.T) { + // Create a base logger + logger := NewLogger(zerolog.InfoLevel) + + // Add colorized logger + var buf bytes.Buffer + logger.AddWriter(&buf, UNSTRUCTURED, true) + + // We should expect the underlying data structures are correctly updated + assert.Equal(t, len(logger.unstructuredColorWriters), 1) + + // Disable colors and log msg + colors.DisableColor() + logger.Info("foo") + + // Ensure that msg doesn't include colors afterwards + prefix := fmt.Sprintf("%s %s", colors.LEFT_ARROW, "foo") + _, ok := strings.CutPrefix(buf.String(), prefix) + assert.True(t, ok) +} From a292978781a5675da1f1136076c0ba78527855e3 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Mon, 25 Sep 2023 16:52:19 +0200 Subject: [PATCH 029/109] Update CONTRIBUTING.md (#185) Co-authored-by: anishnaik --- CONTRIBUTING.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7df2230d..deceb304 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,47 @@ If any of these requirements are violated, you should expect your pull request t Pull request reviewers have a responsibility to uphold these standards. Even if a pull request is compliant with these requirements, a reviewer which identifies an opportunity to document some caveat (such as a `// TODO: ` comment) should request it be added prior to pull request approval. +### Linters + +Several linters and security checkers are run on the PRs. + +#### Go + +To install + +- `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest` + +To run + +- `go fmt ./...` +- `golangci-lint run --timeout 5m0s` + +#### Markdown/Json/Yaml + +To install + +- `npm install -g prettier` +- `npm install -g markdown-link-check@3.10.3` + +To run + +- `prettier '**.json' '**/*.md' '**/*.yml' '!(pkg)'` +- `markdown-link-check --config .github/workflows/resources/markdown_link_check.json ./*.md` + +To format (overwrite files) + +- `prettier '**.json' '**/*.md' '**/*.yml' '!(pkg)' -w` + +#### Github action + +To install + +- `go install github.com/rhysd/actionlint/cmd/actionlint@latest` + +To run + +- `actionlint` + ### Cross-platform considerations - Ensure file/directory names do not exceed 32 characters in length to minimize filepath length issues on Windows. File/directory names should be shorter than this where possible. From b05dd34e9d7551100b4f15d52754f8e3464085f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:09:02 -0400 Subject: [PATCH 030/109] Bump github.com/rs/zerolog from 1.29.0 to 1.30.0 (#186) Bumps [github.com/rs/zerolog](https://github.com/rs/zerolog) from 1.29.0 to 1.30.0. - [Release notes](https://github.com/rs/zerolog/releases) - [Commits](https://github.com/rs/zerolog/compare/v1.29.0...v1.30.0) --- updated-dependencies: - dependency-name: github.com/rs/zerolog dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Pokora Co-authored-by: anishnaik --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 83b9d462..5cac4f40 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/fxamacker/cbor v1.5.1 github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 - github.com/rs/zerolog v1.29.0 + github.com/rs/zerolog v1.30.0 github.com/shopspring/decimal v1.3.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 7721ec5e..439dd9db 100644 --- a/go.sum +++ b/go.sum @@ -42,7 +42,7 @@ github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcju github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -249,9 +249,9 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= -github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= +github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= From 8ef2609e2bac12f3a77d7572a4a26c0b34186667 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:33:48 -0400 Subject: [PATCH 031/109] Bump github.com/google/uuid from 1.3.0 to 1.3.1 (#215) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5cac4f40..3c93f980 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/ethereum/go-ethereum v1.12.0 github.com/fxamacker/cbor v1.5.1 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.30.0 github.com/shopspring/decimal v1.3.1 diff --git a/go.sum b/go.sum index 439dd9db..52bd7664 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= From f3e364ef116253c7de4bb6c23fb1623e85df396a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:08:10 -0400 Subject: [PATCH 032/109] Bump golang.org/x/crypto from 0.12.0 to 0.13.0 (#226) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.12.0 to 0.13.0. - [Commits](https://github.com/golang/crypto/compare/v0.12.0...v0.13.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 3c93f980..6486d2b8 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.12.0 + golang.org/x/crypto v0.13.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/net v0.14.0 - golang.org/x/sys v0.11.0 + golang.org/x/sys v0.12.0 ) require ( @@ -64,7 +64,7 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 52bd7664..7dd94a5a 100644 --- a/go.sum +++ b/go.sum @@ -321,8 +321,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -396,8 +396,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -405,8 +405,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From c7a7ff33f75b8f1097103414ca39956ed9400327 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:08:41 -0500 Subject: [PATCH 033/109] Bump github.com/rs/zerolog from 1.30.0 to 1.31.0 (#236) Bumps [github.com/rs/zerolog](https://github.com/rs/zerolog) from 1.30.0 to 1.31.0. - [Release notes](https://github.com/rs/zerolog/releases) - [Commits](https://github.com/rs/zerolog/compare/v1.30.0...v1.31.0) --- updated-dependencies: - dependency-name: github.com/rs/zerolog dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6486d2b8..9246f409 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/fxamacker/cbor v1.5.1 github.com/google/uuid v1.3.1 github.com/pkg/errors v0.9.1 - github.com/rs/zerolog v1.30.0 + github.com/rs/zerolog v1.31.0 github.com/shopspring/decimal v1.3.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -47,7 +47,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect diff --git a/go.sum b/go.sum index 7dd94a5a..53eff249 100644 --- a/go.sum +++ b/go.sum @@ -183,7 +183,6 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -191,8 +190,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -250,8 +250,8 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= -github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -396,6 +396,7 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From 8654f73afc0179be637d5c673b01eb82dbf8a7b6 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 8 Nov 2023 11:45:18 -0500 Subject: [PATCH 034/109] add setuptools to pip install line (#252) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3722c69c..6af82f51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,7 @@ jobs: - name: Install Python dependencies run: | - pip3 install --no-cache-dir solc-select crytic-compile + pip3 install --no-cache-dir setuptools solc-select crytic-compile - name: Install solc run: | From 825a1c503eda461390413323771f10096579ea5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:24:31 -0500 Subject: [PATCH 035/109] Bump actions/checkout from 3 to 4 (#228) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6af82f51..c5c97e7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Speed up Go (Windows) if: runner.os == 'Windows' @@ -76,7 +76,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: @@ -116,7 +116,7 @@ jobs: timeout-minutes: 20 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Speed up Go, Python, Node (Windows) if: runner.os == 'Windows' From 2096d53d431f77aebbad82789f4aa3daec452645 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:47:35 -0500 Subject: [PATCH 036/109] Bump golang.org/x/crypto from 0.13.0 to 0.14.0 (#240) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.13.0 to 0.14.0. - [Commits](https://github.com/golang/crypto/compare/v0.13.0...v0.14.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 4 ++-- go.sum | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9246f409..358eff17 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.13.0 + golang.org/x/crypto v0.14.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/net v0.14.0 - golang.org/x/sys v0.12.0 + golang.org/x/sys v0.13.0 ) require ( diff --git a/go.sum b/go.sum index 53eff249..5e083873 100644 --- a/go.sum +++ b/go.sum @@ -321,8 +321,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -396,9 +396,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From 0c4d0a8f78e33a037e1fc7304ca9fc4036e2836c Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 8 Nov 2023 14:07:24 -0500 Subject: [PATCH 037/109] Fix `testAllContracts` behavior (#253) --- chain/test_chain.go | 10 ++++++---- chain/test_chain_deployments_tracer.go | 21 ++++++++++++--------- chain/test_chain_events.go | 4 ++++ chain/types/deployed_contract_bytecode.go | 4 ++++ fuzzing/config/config_defaults.go | 2 +- fuzzing/fuzzer_test.go | 1 + fuzzing/fuzzer_worker.go | 5 +++++ logging/logger_test.go | 2 +- 8 files changed, 34 insertions(+), 15 deletions(-) diff --git a/chain/test_chain.go b/chain/test_chain.go index a4466cc7..9e9ff4e2 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -856,8 +856,9 @@ func (t *TestChain) emitContractChangeEvents(reverting bool, messageResults ...* // this execution result is being committed to chain. if deploymentChange.Creation { err = t.Events.ContractDeploymentAddedEventEmitter.Publish(ContractDeploymentsAddedEvent{ - Chain: t, - Contract: deploymentChange.Contract, + Chain: t, + Contract: deploymentChange.Contract, + DynamicDeployment: deploymentChange.DynamicCreation, }) } else if deploymentChange.Destroyed { err = t.Events.ContractDeploymentRemovedEventEmitter.Publish(ContractDeploymentsRemovedEvent{ @@ -887,8 +888,9 @@ func (t *TestChain) emitContractChangeEvents(reverting bool, messageResults ...* }) } else if deploymentChange.Destroyed { err = t.Events.ContractDeploymentAddedEventEmitter.Publish(ContractDeploymentsAddedEvent{ - Chain: t, - Contract: deploymentChange.Contract, + Chain: t, + Contract: deploymentChange.Contract, + DynamicDeployment: deploymentChange.DynamicCreation, }) } if err != nil { diff --git a/chain/test_chain_deployments_tracer.go b/chain/test_chain_deployments_tracer.go index c2672aaa..d8bec11f 100644 --- a/chain/test_chain_deployments_tracer.go +++ b/chain/test_chain_deployments_tracer.go @@ -74,9 +74,10 @@ func (t *testChainDeploymentsTracer) CaptureStart(env *vm.EVM, from common.Addre InitBytecode: input, RuntimeBytecode: nil, }, - Creation: true, - SelfDestructed: false, - Destroyed: false, + Creation: true, + DynamicCreation: false, + SelfDestructed: false, + Destroyed: false, }) } } @@ -118,9 +119,10 @@ func (t *testChainDeploymentsTracer) CaptureEnter(typ vm.OpCode, from common.Add InitBytecode: input, RuntimeBytecode: nil, }, - Creation: true, - SelfDestructed: false, - Destroyed: false, + Creation: true, + DynamicCreation: true, + SelfDestructed: false, + Destroyed: false, }) } } @@ -158,9 +160,10 @@ func (t *testChainDeploymentsTracer) CaptureState(pc uint64, op vm.OpCode, gas, InitBytecode: nil, RuntimeBytecode: t.evm.StateDB.GetCode(scope.Contract.Address()), }, - Creation: false, - SelfDestructed: true, - Destroyed: t.selfDestructDestroysCode, + Creation: false, + DynamicCreation: false, + SelfDestructed: true, + Destroyed: t.selfDestructDestroysCode, }) } } diff --git a/chain/test_chain_events.go b/chain/test_chain_events.go index adc92fd4..49c4cf48 100644 --- a/chain/test_chain_events.go +++ b/chain/test_chain_events.go @@ -93,6 +93,10 @@ type ContractDeploymentsAddedEvent struct { // Contract defines information for the contract which was deployed to the Chain. Contract *types.DeployedContractBytecode + + // DynamicDeployment describes whether this contract deployment was dynamic (e.g. `c = new MyContract()`) or was + // because of a traditional transaction + DynamicDeployment bool } // ContractDeploymentsRemovedEvent describes an event where a contract has become unavailable on the TestChain, either diff --git a/chain/types/deployed_contract_bytecode.go b/chain/types/deployed_contract_bytecode.go index e32c76d1..9f49bef8 100644 --- a/chain/types/deployed_contract_bytecode.go +++ b/chain/types/deployed_contract_bytecode.go @@ -16,6 +16,10 @@ type DeployedContractBytecodeChange struct { // Destroyed are true. Creation bool + // DynamicCreation indicates whether the change made was a _dynamic_ contract creation. This cannot be true if + // Creation is false. + DynamicCreation bool + // SelfDestructed indicates whether the change made was due to a self-destruct instruction being executed. This // cannot be true if Creation is true. // Note: This may not be indicative of contract removal (as is the case with Destroyed), as proposed changes to diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index e82f1a1a..74cd76f4 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -53,7 +53,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { TransactionGasLimit: 12_500_000, Testing: TestingConfig{ StopOnFailedTest: true, - StopOnFailedContractMatching: true, + StopOnFailedContractMatching: false, StopOnNoTests: true, TestAllContracts: false, TraceAll: false, diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 55c845fd..7fdc4f47 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -398,6 +398,7 @@ func TestDeploymentsSelfDestruct(t *testing.T) { config.Fuzzing.DeploymentOrder = []string{"InnerDeploymentFactory"} config.Fuzzing.TestLimit = 500 // this test should expose a failure quickly. config.Fuzzing.Testing.StopOnNoTests = false + config.Fuzzing.Testing.TestAllContracts = true }, method: func(f *fuzzerTestContext) { // Subscribe to any mined block events globally. When receiving them, check contract changes for a diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index d8f01bed..27848a23 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -152,6 +152,11 @@ func (fw *FuzzerWorker) getNewCorpusCallSequenceWeight() *big.Int { // onChainContractDeploymentAddedEvent is the event callback used when the chain detects a new contract deployment. // It attempts bytecode matching and updates the list of deployed contracts the worker should use for fuzz testing. func (fw *FuzzerWorker) onChainContractDeploymentAddedEvent(event chain.ContractDeploymentsAddedEvent) error { + // Do not track the deployed contract if the contract deployment was a dynamic one and testAllContracts is false + if !fw.fuzzer.config.Fuzzing.Testing.TestAllContracts && event.DynamicDeployment { + return nil + } + // Add the contract address to our value set so our generator can use it in calls. fw.valueSet.AddAddress(event.Contract.Address) diff --git a/logging/logger_test.go b/logging/logger_test.go index 540e78e3..53e0d6a4 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -70,6 +70,6 @@ func TestDisabledColors(t *testing.T) { // Ensure that msg doesn't include colors afterwards prefix := fmt.Sprintf("%s %s", colors.LEFT_ARROW, "foo") - _, ok := strings.CutPrefix(buf.String(), prefix) + _, _, ok := strings.Cut(buf.String(), prefix) assert.True(t, ok) } From ce962f9efc1bdc7713c2eaee9d257d0bfa7594b3 Mon Sep 17 00:00:00 2001 From: Damilola Edwards Date: Wed, 8 Nov 2023 21:03:12 +0100 Subject: [PATCH 038/109] Open non-zero coverage reports in corpus by default (#243) Co-authored-by: anishnaik --- fuzzing/coverage/report_template.gohtml | 35 +++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/fuzzing/coverage/report_template.gohtml b/fuzzing/coverage/report_template.gohtml index cab7c07e..d7993e81 100644 --- a/fuzzing/coverage/report_template.gohtml +++ b/fuzzing/coverage/report_template.gohtml @@ -68,6 +68,9 @@ .collapsible-active:after { content: "\2212"; } + .collapsible-active + .collapsible-container { + max-height: none; + } .collapsible-container { margin-bottom: 5px; max-height: 0; @@ -150,12 +153,21 @@ {{$linesCoveredPercentInt := percentageInt $linesCovered $linesActive}} {{/* Output a collapsible header/container for each source*/}} - + {{if not $linesCoveredPercentInt}} + + {{else}} + + {{end}}

@@ -214,17 +226,12 @@ for (i = 0; i < collapsibleHeaders.length; i++) { collapsibleHeaders[i].addEventListener("click", function() { this.classList.toggle("collapsible-active"); - const collapsibleContainer = this.nextElementSibling; - if (collapsibleContainer.style.maxHeight){ - collapsibleContainer.style.maxHeight = null; - } else { - collapsibleContainer.style.maxHeight = collapsibleContainer.scrollHeight + "px"; - } + }); } - // If there's only one item, expand it by default. - if (collapsibleHeaders.length === 1) { + // If there's only one item and that item has 0% coverage, expand it by default. + if (collapsibleHeaders.length === 1 && !collapsibleHeaders.className.contains("collapsible-active")) { collapsibleHeaders[0].click(); } From e471c52460ae9a37f5e168a96cbb0fbbe4db33a0 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 9 Nov 2023 15:51:59 -0500 Subject: [PATCH 039/109] Fix `warp` cheatcode (#255) * fix warp cheatcode * revert if warp input value is greater than type(uint64).max * Make max uint64 a global value --- chain/standard_cheat_code_contract.go | 20 +++++++++++++++---- .../contracts/cheat_codes/vm/warp.sol | 10 +++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/chain/standard_cheat_code_contract.go b/chain/standard_cheat_code_contract.go index 141bf204..88b8f6d2 100644 --- a/chain/standard_cheat_code_contract.go +++ b/chain/standard_cheat_code_contract.go @@ -17,6 +17,9 @@ import ( // StandardCheatcodeContractAddress is the address for the standard cheatcode contract var StandardCheatcodeContractAddress = common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D") +// MaxUint64 holds the max value an uint64 can take +var _, MaxUint64 = utils.GetIntegerConstraints(false, 64) + // getStandardCheatCodeContract obtains a CheatCodeContract which implements common cheat codes. // Returns the precompiled contract, or an error if one occurs. func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, error) { @@ -67,13 +70,22 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, // Warp: Sets VM timestamp contract.addMethod( - "warp", abi.Arguments{{Type: typeUint64}}, abi.Arguments{}, + "warp", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes until the transaction exits. - original := tracer.evm.Context.Time - tracer.evm.Context.Time = inputs[0].(uint64) + originalTime := tracer.evm.Context.Time + + // Retrieve new timestamp and make sure it is LEQ max value of an uint64 + newTime := inputs[0].(*big.Int) + if newTime.Cmp(MaxUint64) > 0 { + return nil, cheatCodeRevertData([]byte("warp: timestamp exceeds max value of type(uint64).max")) + } + + // Set the time + tracer.evm.Context.Time = newTime.Uint64() tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { - tracer.evm.Context.Time = original + // Reset the time + tracer.evm.Context.Time = originalTime }) return nil, nil }, diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/warp.sol b/fuzzing/testdata/contracts/cheat_codes/vm/warp.sol index c70dcf13..536a49ee 100644 --- a/fuzzing/testdata/contracts/cheat_codes/vm/warp.sol +++ b/fuzzing/testdata/contracts/cheat_codes/vm/warp.sol @@ -1,6 +1,6 @@ // This test ensures that the block timestamp can be set with cheat codes interface CheatCodes { - function warp(uint64) external; + function warp(uint256) external; } contract TestContract { @@ -15,5 +15,13 @@ contract TestContract { assert(block.timestamp == 7); cheats.warp(9); assert(block.timestamp == 9); + + // Ensure that a value greater than type(uint64).max will cause warp to revert + // This is not the best way to test it but gets the job done + try cheats.warp(type(uint64).max + 1) { + assert(false); + } catch { + assert(true); + } } } From c33ce63ce790fa38e5f06acde51e01f02864bbfa Mon Sep 17 00:00:00 2001 From: anishnaik Date: Mon, 12 Feb 2024 21:08:05 -0500 Subject: [PATCH 040/109] Explicitly reset the state trie's cache to prevent mem leak (#290) --- chain/test_chain.go | 8 ++++++++ fuzzing/fuzzer_worker.go | 3 +++ go.mod | 2 +- go.sum | 6 ++++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/chain/test_chain.go b/chain/test_chain.go index 9e9ff4e2..a23ac5d9 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -199,6 +199,14 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh return chain, nil } +// Close will release any objects from the TestChain that must be _explicitly_ released. Currently, the one object that +// must be explicitly released is the stateDB trie's underlying cache. This cache, if not released, prevents the TestChain +// object from being freed by the garbage collector and causes a severe memory leak. +func (t *TestChain) Close() { + // Reset the state DB's cache + t.stateDatabase.TrieDB().ResetCache() +} + // Clone recreates the current TestChain state into a new instance. This simply reconstructs the block/chain state // but does not perform any other API-related changes such as adding additional tracers the original had. Additionally, // this does not clone pending blocks. The provided method, if non-nil, is used as callback to provide an intermediate diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 27848a23..a7ac3f4d 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -532,6 +532,9 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { return false, err } + // Defer the closing of the test chain object + defer fw.chain.Close() + // Emit an event indicating the worker has setup its chain. err = fw.Events.FuzzerWorkerChainSetup.Publish(FuzzerWorkerChainSetupEvent{ Worker: fw, diff --git a/go.mod b/go.mod index 358eff17..8e16ad95 100644 --- a/go.mod +++ b/go.mod @@ -70,4 +70,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20230811005223-cee04520a2f9 +replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca diff --git a/go.sum b/go.sum index 5e083873..4dad80c9 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crytic/medusa-geth v0.0.0-20230811005223-cee04520a2f9 h1:BUuL3h23IdVSmyq3I7LVUYRInKgXShMLKElYaFgn4RM= -github.com/crytic/medusa-geth v0.0.0-20230811005223-cee04520a2f9/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= +github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca h1:oGRWjrs9pStgFbnk1K5aLTQoY/aeO5zkzqADZH/maFw= +github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -396,6 +396,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From bccd25c3ccb27574c8704988ac1cca56b042a1f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:24:26 -0500 Subject: [PATCH 041/109] Bump github.com/spf13/cobra from 1.7.0 to 1.8.0 (#257) Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.7.0 to 1.8.0. - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.7.0...v1.8.0) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Pokora --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 8e16ad95..c4981b78 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.31.0 github.com/shopspring/decimal v1.3.1 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.14.0 diff --git a/go.sum b/go.sum index 4dad80c9..c7a0f633 100644 --- a/go.sum +++ b/go.sum @@ -44,7 +44,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca h1:oGRWjrs9pStgFbnk1K5aLTQoY/aeO5zkzqADZH/maFw= github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= @@ -267,8 +267,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= From 7a27e142ffe9ecd5c4d284a6102d7a8195970221 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:39:05 -0500 Subject: [PATCH 042/109] Bump github.com/google/uuid from 1.3.1 to 1.6.0 (#288) Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.3.1 to 1.6.0. - [Release notes](https://github.com/google/uuid/releases) - [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/uuid/compare/v1.3.1...v1.6.0) --- updated-dependencies: - dependency-name: github.com/google/uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c4981b78..d0124e2a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/ethereum/go-ethereum v1.12.0 github.com/fxamacker/cbor v1.5.1 - github.com/google/uuid v1.3.1 + github.com/google/uuid v1.6.0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.31.0 github.com/shopspring/decimal v1.3.1 diff --git a/go.sum b/go.sum index c7a0f633..596b00b0 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= From 62b648c16053d87e3244dfd14a55371059be701c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:51:07 -0500 Subject: [PATCH 043/109] Bump golang.org/x/net from 0.14.0 to 0.21.0 (#291) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.14.0 to 0.21.0. - [Commits](https://github.com/golang/net/compare/v0.14.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Pokora --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index d0124e2a..27b9f68c 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.14.0 + golang.org/x/crypto v0.19.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/net v0.14.0 - golang.org/x/sys v0.13.0 + golang.org/x/net v0.21.0 + golang.org/x/sys v0.17.0 ) require ( @@ -64,7 +64,7 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 596b00b0..4c98ad4e 100644 --- a/go.sum +++ b/go.sum @@ -321,8 +321,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -353,8 +353,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -398,8 +398,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -407,8 +407,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 9b279aa13f463b1564ad07a0a5996aa11c8449fb Mon Sep 17 00:00:00 2001 From: David Pokora Date: Mon, 12 Feb 2024 21:19:43 -0800 Subject: [PATCH 044/109] Fix the copy length based panic in the parseBytes32 cheatcode (#293) --- chain/standard_cheat_code_contract.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain/standard_cheat_code_contract.go b/chain/standard_cheat_code_contract.go index 88b8f6d2..79c2d3ad 100644 --- a/chain/standard_cheat_code_contract.go +++ b/chain/standard_cheat_code_contract.go @@ -452,7 +452,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, // Use a fixed array and copy the data over var bArray [32]byte - copy(bArray[:], bSlice[:32]) + copy(bArray[:], bSlice) return []any{bArray}, nil }, From 04df3f1f1aee989898799ab0b48d1f3e030a752a Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 20 Feb 2024 12:28:41 -0500 Subject: [PATCH 045/109] Enable all testing modes by default, update property mode testing, improve UX, allow for contracts to have starting balances, and fix coverage panic (#216) * - merged all three testing modes - created testing provider utils file to evaluate whether an abi.method is an optimization / property test - updated fuzzer tests - made default property test prefix "invariant_" * enable all testing modes, update fuzzer tests * add verification for config * improve config-related errors * update deploymentOrder to targetContracts * update edge case where coverage is 0/0 lines * add support for payable constructors * linting * fix bug * fix panic and improve return data printing in execution trace * encode bytes and byteX as hex strings * fix console * updates from PR review * change config language --- cmd/fuzz_flags.go | 49 ++---- cmd/init_flags.go | 10 +- fuzzing/config/config.go | 88 +++++++--- fuzzing/config/config_defaults.go | 30 ++-- fuzzing/config/gen_fuzzing_config.go | 148 +++++++++++++++++ fuzzing/coverage/coverage_maps.go | 6 +- fuzzing/coverage/report_generation.go | 4 +- fuzzing/executiontracer/execution_trace.go | 9 +- fuzzing/fuzzer.go | 45 +++-- fuzzing/fuzzer_test.go | 155 +++++++++++------- fuzzing/test_case_assertion_provider.go | 19 ++- fuzzing/test_case_optimization_provider.go | 31 ++-- fuzzing/test_case_property_provider.go | 28 +--- .../assertions/assert_and_property_test.sol | 2 +- .../contracts/chain/tx_out_of_gas.sol | 2 +- .../specific_call_sequence.sol | 2 +- .../deploy_payable_constructors.sol | 18 ++ .../deployments/deployment_order.sol | 4 +- .../deployments/deployment_with_args.sol | 8 +- .../deployments/inner_deployment.sol | 2 +- .../inner_deployment_on_construction.sol | 2 +- .../deployments/inner_inner_deployment.sol | 2 +- .../deployments/internal_library.sol | 2 +- .../contracts/deployments/testing_scope.sol | 4 +- .../value_generation/generate_all_types.sol | 2 +- .../value_generation/match_addr_contract.sol | 2 +- .../value_generation/match_addr_exact.sol | 2 +- .../value_generation/match_addr_sender.sol | 2 +- .../value_generation/match_ints_xy.sol | 2 +- .../value_generation/match_payable_xy.sol | 2 +- .../value_generation/match_string_exact.sol | 2 +- .../value_generation/match_structs_xy.sol | 2 +- .../value_generation/match_uints_xy.sol | 2 +- .../vm_tests/block_hash_store_check.sol | 2 +- .../vm_tests/block_number_increasing.sol | 2 +- .../vm_tests/block_timestamp_increasing.sol | 2 +- fuzzing/utils/fuzz_method_utils.go | 34 ++++ fuzzing/valuegeneration/abi_values.go | 12 +- go.mod | 4 + go.sum | 21 +++ logging/logger.go | 8 +- 41 files changed, 530 insertions(+), 243 deletions(-) create mode 100644 fuzzing/config/gen_fuzzing_config.go create mode 100644 fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol create mode 100644 fuzzing/utils/fuzz_method_utils.go diff --git a/cmd/fuzz_flags.go b/cmd/fuzz_flags.go index e1999804..9e1c9d37 100644 --- a/cmd/fuzz_flags.go +++ b/cmd/fuzz_flags.go @@ -21,8 +21,8 @@ func addFuzzFlags() error { // Config file fuzzCmd.Flags().String("config", "", "path to config file") - // Target - fuzzCmd.Flags().String("target", "", TargetFlagDescription) + // Compilation Target + fuzzCmd.Flags().String("compilation-target", "", TargetFlagDescription) // Number of workers fuzzCmd.Flags().Int("workers", 0, @@ -40,14 +40,13 @@ func addFuzzFlags() error { fuzzCmd.Flags().Int("seq-len", 0, fmt.Sprintf("maximum transactions to run in sequence (unless a config file is provided, default is %d)", defaultConfig.Fuzzing.CallSequenceLength)) - // Deployment order - fuzzCmd.Flags().StringSlice("deployment-order", []string{}, - fmt.Sprintf("order in which to deploy target contracts (unless a config file is provided, default is %v)", defaultConfig.Fuzzing.DeploymentOrder)) + // Target contracts + fuzzCmd.Flags().StringSlice("target-contracts", []string{}, + fmt.Sprintf("target contracts for fuzz testing (unless a config file is provided, default is %v)", defaultConfig.Fuzzing.TargetContracts)) // Corpus directory - // TODO: Update description when we add "coverage reports" feature fuzzCmd.Flags().String("corpus-dir", "", - fmt.Sprintf("directory path for corpus items (unless a config file is provided, default is %q)", defaultConfig.Fuzzing.CorpusDirectory)) + fmt.Sprintf("directory path for corpus items and coverage reports (unless a config file is provided, default is %q)", defaultConfig.Fuzzing.CorpusDirectory)) // Senders fuzzCmd.Flags().StringSlice("senders", []string{}, @@ -57,14 +56,6 @@ func addFuzzFlags() error { fuzzCmd.Flags().String("deployer", "", "account address used to deploy contracts") - // Assertion mode - fuzzCmd.Flags().Bool("assertion-mode", false, - fmt.Sprintf("enable assertion mode (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.AssertionTesting.Enabled)) - - // Optimization mode - fuzzCmd.Flags().Bool("optimization-mode", false, - fmt.Sprintf("enable optimization mode (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.OptimizationTesting.Enabled)) - // Trace all fuzzCmd.Flags().Bool("trace-all", false, fmt.Sprintf("print the execution trace for every element in a shrunken call sequence instead of only the last element (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.TraceAll)) @@ -79,10 +70,10 @@ func addFuzzFlags() error { func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config.ProjectConfig) error { var err error - // If --target was used - if cmd.Flags().Changed("target") { + // If --compilation-target was used + if cmd.Flags().Changed("compilation-target") { // Get the new target - newTarget, err := cmd.Flags().GetString("target") + newTarget, err := cmd.Flags().GetString("compilation-target") if err != nil { return err } @@ -125,9 +116,9 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. } } - // Update deployment order - if cmd.Flags().Changed("deployment-order") { - projectConfig.Fuzzing.DeploymentOrder, err = cmd.Flags().GetStringSlice("deployment-order") + // Update target contracts + if cmd.Flags().Changed("target-contracts") { + projectConfig.Fuzzing.TargetContracts, err = cmd.Flags().GetStringSlice("target-contracts") if err != nil { return err } @@ -157,22 +148,6 @@ func updateProjectConfigWithFuzzFlags(cmd *cobra.Command, projectConfig *config. } } - // Update assertion mode enablement - if cmd.Flags().Changed("assertion-mode") { - projectConfig.Fuzzing.Testing.AssertionTesting.Enabled, err = cmd.Flags().GetBool("assertion-mode") - if err != nil { - return err - } - } - - // Update optimization mode enablement - if cmd.Flags().Changed("optimization-mode") { - projectConfig.Fuzzing.Testing.OptimizationTesting.Enabled, err = cmd.Flags().GetBool("optimization-mode") - if err != nil { - return err - } - } - // Update trace all enablement if cmd.Flags().Changed("trace-all") { projectConfig.Fuzzing.Testing.TraceAll, err = cmd.Flags().GetBool("trace-all") diff --git a/cmd/init_flags.go b/cmd/init_flags.go index 40c3499a..dfe6d8d6 100644 --- a/cmd/init_flags.go +++ b/cmd/init_flags.go @@ -10,18 +10,18 @@ func addInitFlags() error { // Output path for configuration initCmd.Flags().String("out", "", "output path for the new project configuration file") - // Target file / directory - initCmd.Flags().String("target", "", TargetFlagDescription) + // Target file / directory for compilation + initCmd.Flags().String("compilation-target", "", TargetFlagDescription) return nil } // updateProjectConfigWithInitFlags will update the given projectConfig with any CLI arguments that were provided to the init command func updateProjectConfigWithInitFlags(cmd *cobra.Command, projectConfig *config.ProjectConfig) error { - // If --target was used - if cmd.Flags().Changed("target") { + // If --compilation-target was used + if cmd.Flags().Changed("compilation-target") { // Get the new target - newTarget, err := cmd.Flags().GetString("target") + newTarget, err := cmd.Flags().GetString("compilation-target") if err != nil { return err } diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index eac6c371..fd12bd53 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -3,15 +3,21 @@ package config import ( "encoding/json" "errors" - "os" - "github.com/crytic/medusa/chain/config" - "github.com/rs/zerolog" - "github.com/crytic/medusa/compilation" + "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/rs/zerolog" + "math/big" + "os" ) +// The following directives will be picked up by the `go generate` command to generate JSON marshaling code from +// templates defined below. They should be preserved for re-use in case we change our structures. +//go:generate go get github.com/fjl/gencodec +//go:generate go run github.com/fjl/gencodec -type FuzzingConfig -field-override fuzzingConfigMarshaling -out gen_fuzzing_config.go + type ProjectConfig struct { // Fuzzing describes the configuration used in fuzzing campaigns. Fuzzing FuzzingConfig `json:"fuzzing"` @@ -50,10 +56,15 @@ type FuzzingConfig struct { // CoverageEnabled describes whether to use coverage-guided fuzzing CoverageEnabled bool `json:"coverageEnabled"` - // DeploymentOrder determines the order in which the contracts should be deployed - DeploymentOrder []string `json:"deploymentOrder"` + // TargetContracts are the target contracts for fuzz testing + TargetContracts []string `json:"targetContracts"` + + // TargetContractsBalances holds the amount of wei that should be sent during deployment for one or more contracts in + // TargetContracts + TargetContractsBalances []*big.Int `json:"targetContractsBalances"` - // Constructor arguments for contracts deployment. It is available only in init mode + // ConstructorArgs holds the constructor arguments for TargetContracts deployments. It is available via the project + // configuration ConstructorArgs map[string]map[string]any `json:"constructorArgs"` // DeployerAddress describe the account address to be used to deploy contracts. @@ -85,6 +96,13 @@ type FuzzingConfig struct { TestChainConfig config.TestChainConfig `json:"chainConfig"` } +// fuzzingConfigMarshaling is a structure that overrides field types during JSON marshaling. It allows FuzzingConfig to +// have its custom marshaling methods auto-generated and will handle type conversions for serialization purposes. +// For example, this enables serialization of big.Int but specifying a different field type to control serialization. +type fuzzingConfigMarshaling struct { + TargetContractsBalances []*hexutil.Big +} + // TestingConfig describes the configuration options used for testing type TestingConfig struct { // StopOnFailedTest describes whether the fuzzing.Fuzzer should stop after detecting the first failed test. @@ -111,7 +129,7 @@ type TestingConfig struct { AssertionTesting AssertionTestingConfig `json:"assertionTesting"` // PropertyTesting describes the configuration used for property testing. - PropertyTesting PropertyTestConfig `json:"propertyTesting"` + PropertyTesting PropertyTestingConfig `json:"propertyTesting"` // OptimizationTesting describes the configuration used for optimization testing. OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"` @@ -125,13 +143,12 @@ type AssertionTestingConfig struct { // TestViewMethods dictates whether constant/pure/view methods should be tested. TestViewMethods bool `json:"testViewMethods"` - // AssertionModes describes the various panic codes that can be enabled and be treated as a "failing case" - AssertionModes AssertionModesConfig `json:"assertionModes"` + // PanicCodeConfig describes the various panic codes that can be enabled and be treated as a "failing case" + PanicCodeConfig PanicCodeConfig `json:"panicCodeConfig"` } -// AssertionModesConfig describes the configuration options for the various modes that can be enabled for assertion -// testing -type AssertionModesConfig struct { +// PanicCodeConfig describes the various panic codes that can be enabled and be treated as a failing assertion test +type PanicCodeConfig struct { // FailOnCompilerInsertedPanic describes whether a generic compiler inserted panic should be treated as a failing case FailOnCompilerInsertedPanic bool `json:"failOnCompilerInsertedPanic"` @@ -163,8 +180,8 @@ type AssertionModesConfig struct { FailOnCallUninitializedVariable bool `json:"failOnCallUninitializedVariable"` } -// PropertyTestConfig describes the configuration options used for property testing -type PropertyTestConfig struct { +// PropertyTestingConfig describes the configuration options used for property testing +type PropertyTestingConfig struct { // Enabled describes whether testing is enabled. Enabled bool `json:"enabled"` @@ -255,6 +272,12 @@ func (p *ProjectConfig) WriteToFile(path string) error { // Validate validates that the ProjectConfig meets certain requirements. // Returns an error if one occurs. func (p *ProjectConfig) Validate() error { + // Create logger instance if global logger is available + logger := logging.NewLogger(zerolog.Disabled) + if logging.GlobalLogger != nil { + logger = logging.GlobalLogger.NewSubLogger("module", "fuzzer config") + } + // Verify the worker count is a positive number. if p.Fuzzing.Workers <= 0 { return errors.New("project configuration must specify a positive number for the worker count") @@ -262,7 +285,7 @@ func (p *ProjectConfig) Validate() error { // Verify that the sequence length is a positive number if p.Fuzzing.CallSequenceLength <= 0 { - return errors.New("project configuration must specify a positive number for the transaction sequence length") + return errors.New("project configuration must specify a positive number for the transaction sequence lengt") } // Verify the worker reset limit is a positive number @@ -270,12 +293,30 @@ func (p *ProjectConfig) Validate() error { return errors.New("project configuration must specify a positive number for the worker reset limit") } + // Verify timeout + if p.Fuzzing.Timeout < 0 { + return errors.New("project configuration must specify a positive number for the timeout") + } + // Verify gas limits are appropriate if p.Fuzzing.BlockGasLimit < p.Fuzzing.TransactionGasLimit { return errors.New("project configuration must specify a block gas limit which is not less than the transaction gas limit") } if p.Fuzzing.BlockGasLimit == 0 || p.Fuzzing.TransactionGasLimit == 0 { - return errors.New("project configuration must specify a block and transaction gas limit which is non-zero") + return errors.New("project configuration must specify a block and transaction gas limit which are non-zero") + } + + // Log warning if max block delay is zero + if p.Fuzzing.MaxBlockNumberDelay == 0 { + logger.Warn("The maximum block number delay is set to zero. Please be aware that transactions will " + + "always be fit in the same block until the block gas limit is reached and that the block number will always " + + "increment by one.") + } + + // Log warning if max timestamp delay is zero + if p.Fuzzing.MaxBlockTimestampDelay == 0 { + logger.Warn("The maximum timestamp delay is set to zero. Please be aware that block time jumps will " + + "always be exactly one.") } // Verify that senders are well-formed addresses @@ -288,17 +329,10 @@ func (p *ProjectConfig) Validate() error { return errors.New("project configuration must specify only a well-formed deployer address") } - // Verify property testing fields. - if p.Fuzzing.Testing.PropertyTesting.Enabled { - // Test prefixes must be supplied if property testing is enabled. - if len(p.Fuzzing.Testing.PropertyTesting.TestPrefixes) == 0 { - return errors.New("project configuration must specify test name prefixes if property testing is enabled") - } - } - // Ensure that the log level is a valid one - if _, err := zerolog.ParseLevel(p.Logging.Level.String()); err != nil { - return err + level, err := zerolog.ParseLevel(p.Logging.Level.String()) + if err != nil || level == zerolog.FatalLevel { + return errors.New("project config must specify a valid log level (trace, debug, info, warn, error, or panic)") } return nil diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 74cd76f4..dd5e1c17 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -4,6 +4,7 @@ import ( testChainConfig "github.com/crytic/medusa/chain/config" "github.com/crytic/medusa/compilation" "github.com/rs/zerolog" + "math/big" ) // GetDefaultProjectConfig obtains a default configuration for a project. It populates a default compilation config @@ -32,15 +33,16 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { // Create a project configuration projectConfig := &ProjectConfig{ Fuzzing: FuzzingConfig{ - Workers: 10, - WorkerResetLimit: 50, - Timeout: 0, - TestLimit: 0, - CallSequenceLength: 100, - DeploymentOrder: []string{}, - ConstructorArgs: map[string]map[string]any{}, - CorpusDirectory: "", - CoverageEnabled: true, + Workers: 10, + WorkerResetLimit: 50, + Timeout: 0, + TestLimit: 0, + CallSequenceLength: 100, + TargetContracts: []string{}, + TargetContractsBalances: []*big.Int{}, + ConstructorArgs: map[string]map[string]any{}, + CorpusDirectory: "", + CoverageEnabled: true, SenderAddresses: []string{ "0x10000", "0x20000", @@ -58,20 +60,20 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { TestAllContracts: false, TraceAll: false, AssertionTesting: AssertionTestingConfig{ - Enabled: false, + Enabled: true, TestViewMethods: false, - AssertionModes: AssertionModesConfig{ + PanicCodeConfig: PanicCodeConfig{ FailOnAssertion: true, }, }, - PropertyTesting: PropertyTestConfig{ + PropertyTesting: PropertyTestingConfig{ Enabled: true, TestPrefixes: []string{ - "fuzz_", + "property_", }, }, OptimizationTesting: OptimizationTestingConfig{ - Enabled: false, + Enabled: true, TestPrefixes: []string{ "optimize_", }, diff --git a/fuzzing/config/gen_fuzzing_config.go b/fuzzing/config/gen_fuzzing_config.go new file mode 100644 index 00000000..5f1de304 --- /dev/null +++ b/fuzzing/config/gen_fuzzing_config.go @@ -0,0 +1,148 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package config + +import ( + "encoding/json" + "math/big" + + "github.com/crytic/medusa/chain/config" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var _ = (*fuzzingConfigMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (f FuzzingConfig) MarshalJSON() ([]byte, error) { + type FuzzingConfig struct { + Workers int `json:"workers"` + WorkerResetLimit int `json:"workerResetLimit"` + Timeout int `json:"timeout"` + TestLimit uint64 `json:"testLimit"` + CallSequenceLength int `json:"callSequenceLength"` + CorpusDirectory string `json:"corpusDirectory"` + CoverageEnabled bool `json:"coverageEnabled"` + TargetContracts []string `json:"targetContracts"` + TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` + ConstructorArgs map[string]map[string]any `json:"constructorArgs"` + DeployerAddress string `json:"deployerAddress"` + SenderAddresses []string `json:"senderAddresses"` + MaxBlockNumberDelay uint64 `json:"blockNumberDelayMax"` + MaxBlockTimestampDelay uint64 `json:"blockTimestampDelayMax"` + BlockGasLimit uint64 `json:"blockGasLimit"` + TransactionGasLimit uint64 `json:"transactionGasLimit"` + Testing TestingConfig `json:"testing"` + TestChainConfig config.TestChainConfig `json:"chainConfig"` + } + var enc FuzzingConfig + enc.Workers = f.Workers + enc.WorkerResetLimit = f.WorkerResetLimit + enc.Timeout = f.Timeout + enc.TestLimit = f.TestLimit + enc.CallSequenceLength = f.CallSequenceLength + enc.CorpusDirectory = f.CorpusDirectory + enc.CoverageEnabled = f.CoverageEnabled + enc.TargetContracts = f.TargetContracts + if f.TargetContractsBalances != nil { + enc.TargetContractsBalances = make([]*hexutil.Big, len(f.TargetContractsBalances)) + for k, v := range f.TargetContractsBalances { + enc.TargetContractsBalances[k] = (*hexutil.Big)(v) + } + } + enc.ConstructorArgs = f.ConstructorArgs + enc.DeployerAddress = f.DeployerAddress + enc.SenderAddresses = f.SenderAddresses + enc.MaxBlockNumberDelay = f.MaxBlockNumberDelay + enc.MaxBlockTimestampDelay = f.MaxBlockTimestampDelay + enc.BlockGasLimit = f.BlockGasLimit + enc.TransactionGasLimit = f.TransactionGasLimit + enc.Testing = f.Testing + enc.TestChainConfig = f.TestChainConfig + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { + type FuzzingConfig struct { + Workers *int `json:"workers"` + WorkerResetLimit *int `json:"workerResetLimit"` + Timeout *int `json:"timeout"` + TestLimit *uint64 `json:"testLimit"` + CallSequenceLength *int `json:"callSequenceLength"` + CorpusDirectory *string `json:"corpusDirectory"` + CoverageEnabled *bool `json:"coverageEnabled"` + TargetContracts []string `json:"targetContracts"` + TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` + ConstructorArgs map[string]map[string]any `json:"constructorArgs"` + DeployerAddress *string `json:"deployerAddress"` + SenderAddresses []string `json:"senderAddresses"` + MaxBlockNumberDelay *uint64 `json:"blockNumberDelayMax"` + MaxBlockTimestampDelay *uint64 `json:"blockTimestampDelayMax"` + BlockGasLimit *uint64 `json:"blockGasLimit"` + TransactionGasLimit *uint64 `json:"transactionGasLimit"` + Testing *TestingConfig `json:"testing"` + TestChainConfig *config.TestChainConfig `json:"chainConfig"` + } + var dec FuzzingConfig + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Workers != nil { + f.Workers = *dec.Workers + } + if dec.WorkerResetLimit != nil { + f.WorkerResetLimit = *dec.WorkerResetLimit + } + if dec.Timeout != nil { + f.Timeout = *dec.Timeout + } + if dec.TestLimit != nil { + f.TestLimit = *dec.TestLimit + } + if dec.CallSequenceLength != nil { + f.CallSequenceLength = *dec.CallSequenceLength + } + if dec.CorpusDirectory != nil { + f.CorpusDirectory = *dec.CorpusDirectory + } + if dec.CoverageEnabled != nil { + f.CoverageEnabled = *dec.CoverageEnabled + } + if dec.TargetContracts != nil { + f.TargetContracts = dec.TargetContracts + } + if dec.TargetContractsBalances != nil { + f.TargetContractsBalances = make([]*big.Int, len(dec.TargetContractsBalances)) + for k, v := range dec.TargetContractsBalances { + f.TargetContractsBalances[k] = (*big.Int)(v) + } + } + if dec.ConstructorArgs != nil { + f.ConstructorArgs = dec.ConstructorArgs + } + if dec.DeployerAddress != nil { + f.DeployerAddress = *dec.DeployerAddress + } + if dec.SenderAddresses != nil { + f.SenderAddresses = dec.SenderAddresses + } + if dec.MaxBlockNumberDelay != nil { + f.MaxBlockNumberDelay = *dec.MaxBlockNumberDelay + } + if dec.MaxBlockTimestampDelay != nil { + f.MaxBlockTimestampDelay = *dec.MaxBlockTimestampDelay + } + if dec.BlockGasLimit != nil { + f.BlockGasLimit = *dec.BlockGasLimit + } + if dec.TransactionGasLimit != nil { + f.TransactionGasLimit = *dec.TransactionGasLimit + } + if dec.Testing != nil { + f.Testing = *dec.Testing + } + if dec.TestChainConfig != nil { + f.TestChainConfig = *dec.TestChainConfig + } + return nil +} diff --git a/fuzzing/coverage/coverage_maps.go b/fuzzing/coverage/coverage_maps.go index 71ed9852..e9a1343c 100644 --- a/fuzzing/coverage/coverage_maps.go +++ b/fuzzing/coverage/coverage_maps.go @@ -2,7 +2,6 @@ package coverage import ( "bytes" - "fmt" compilationTypes "github.com/crytic/medusa/compilation/types" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" @@ -373,5 +372,8 @@ func (cm *CoverageMapBytecodeData) setCoveredAt(codeSize int, pc uint64) (bool, } return false, nil } - return false, fmt.Errorf("tried to set coverage map out of bounds (pc: %d, code size %d)", pc, len(cm.executedFlags)) + + // Since it is possible that the program counter is larger than the code size (e.g., malformed bytecode), we will + // simply return false with no error + return false, nil } diff --git a/fuzzing/coverage/report_generation.go b/fuzzing/coverage/report_generation.go index 184038f7..c9bc2da1 100644 --- a/fuzzing/coverage/report_generation.go +++ b/fuzzing/coverage/report_generation.go @@ -62,9 +62,9 @@ func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) err // Determine our precision string formatStr := "%." + strconv.Itoa(decimals) + "f" - // If no lines are active and none are covered, show 100% coverage + // If no lines are active and none are covered, show 0% coverage if x == 0 && y == 0 { - return fmt.Sprintf(formatStr, float64(100)) + return fmt.Sprintf(formatStr, float64(0)) } return fmt.Sprintf(formatStr, (float64(x)/float64(y))*100) }, diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index 287ce52c..695f595b 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -195,14 +195,19 @@ func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *CallFrame) []a // If we could not correctly obtain the unpacked arguments in a nice display string (due to not having a resolved // contract or method definition, or failure to unpack), we display as raw data in the worst case. - if outputArgumentsDisplayText == nil { + // TODO: Fix if return data is empty len byte array + if outputArgumentsDisplayText == nil && len(callFrame.ReturnData) > 0 { temp := fmt.Sprintf("return_data=%v", hex.EncodeToString(callFrame.ReturnData)) outputArgumentsDisplayText = &temp } // Wrap our return message and output it at the end. if callFrame.ReturnError == nil { - elements = append(elements, colors.GreenBold, fmt.Sprintf("[return (%v)]", *outputArgumentsDisplayText), colors.Reset, "\n") + if outputArgumentsDisplayText != nil { + elements = append(elements, colors.GreenBold, fmt.Sprintf("[return (%v)]", *outputArgumentsDisplayText), colors.Reset, "\n") + } else { + elements = append(elements, colors.GreenBold, "[return]", colors.Reset, "\n") + } return elements } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 4686eba8..3b2da8fe 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -3,6 +3,10 @@ package fuzzing import ( "context" "fmt" + "github.com/crytic/medusa/fuzzing/coverage" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" "math/big" "math/rand" "os" @@ -14,11 +18,6 @@ import ( "sync" "time" - "github.com/crytic/medusa/fuzzing/coverage" - "github.com/crytic/medusa/logging" - "github.com/crytic/medusa/logging/colors" - "github.com/rs/zerolog" - "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/core/types" @@ -93,7 +92,9 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { if config.Logging.NoColor { colors.DisableColor() } + // Create the global logger and add stdout as an unstructured output stream + // Note that we are not using the project config's log level because we have not validated it yet logging.GlobalLogger = logging.NewLogger(config.Logging.Level) logging.GlobalLogger.AddWriter(os.Stdout, logging.UNSTRUCTURED, !config.Logging.NoColor) @@ -110,16 +111,19 @@ func NewFuzzer(config config.ProjectConfig) (*Fuzzer, error) { logging.GlobalLogger.AddWriter(file, logging.UNSTRUCTURED, false) } - // Get the fuzzer's custom sub-logger - logger := logging.GlobalLogger.NewSubLogger("module", "fuzzer") - // Validate our provided config err := config.Validate() if err != nil { - logger.Error("Invalid configuration", err) + logging.GlobalLogger.Error("Invalid configuration", err) return nil, err } + // Update the log level of the global logger now + logging.GlobalLogger.SetLevel(config.Logging.Level) + + // Get the fuzzer's custom sub-logger + logger := logging.GlobalLogger.NewSubLogger("module", "fuzzer") + // Parse the senders addresses from our account config. senders, err := utils.HexStringsToAddresses(config.Fuzzing.SenderAddresses) if err != nil { @@ -330,19 +334,20 @@ func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { // definitions, as well as those added by Fuzzer.AddCompilationTargets. The contract deployment order is defined by // the Fuzzer.config. func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) error { - // Verify contract deployment order is not empty. If it's empty, but we only have one contract definition, - // we can infer the deployment order. Otherwise, we report an error. - if len(fuzzer.config.Fuzzing.DeploymentOrder) == 0 { + // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, + // we can infer the target contracts. Otherwise, we report an error. + if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { if len(fuzzer.contractDefinitions) == 1 { - fuzzer.config.Fuzzing.DeploymentOrder = []string{fuzzer.contractDefinitions[0].Name()} + fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { - return fmt.Errorf("you must specify a contract deployment order within your project configuration") + return fmt.Errorf("missing target contracts (update fuzzing.targetContracts in the project config " + + "or use the --target-contracts CLI flag)") } } // Loop for all contracts to deploy deployedContractAddr := make(map[string]common.Address) - for _, contractName := range fuzzer.config.Fuzzing.DeploymentOrder { + for i, contractName := range fuzzer.config.Fuzzing.TargetContracts { // Look for a contract in our compiled contract definitions that matches this one found := false for _, contract := range fuzzer.contractDefinitions { @@ -368,9 +373,15 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro return fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err) } + // If our project config has a non-zero balance for this target contract, retrieve it + contractBalance := big.NewInt(0) + if len(fuzzer.config.Fuzzing.TargetContractsBalances) > i { + contractBalance = new(big.Int).Set(fuzzer.config.Fuzzing.TargetContractsBalances[i]) + } + // Create a message to represent our contract deployment (we let deployments consume the whole block // gas limit rather than use tx gas limit) - msg := calls.NewCallMessage(fuzzer.deployer, nil, 0, big.NewInt(0), fuzzer.config.Fuzzing.BlockGasLimit, nil, nil, nil, msgData) + msg := calls.NewCallMessage(fuzzer.deployer, nil, 0, contractBalance, fuzzer.config.Fuzzing.BlockGasLimit, nil, nil, nil, msgData) msg.FillFromTestChainProperties(testChain) // Create a new pending block we'll commit to chain @@ -410,7 +421,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro // If we did not find a contract corresponding to this item in the deployment order, we throw an error. if !found { - return fmt.Errorf("DeploymentOrder specified a contract name which was not found in the compilation: %v\n", contractName) + return fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName) } } return nil diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 7fdc4f47..9167b183 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -21,9 +21,9 @@ func TestFuzzerHooks(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/assertions/assert_immediate.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.PropertyTesting.Enabled = false - config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Attach to fuzzer hooks which simply set a success state. @@ -76,18 +76,18 @@ func TestAssertionMode(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnAssertion = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnAllocateTooMuchMemory = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnArithmeticUnderflow = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnCallUninitializedVariable = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnEnumTypeConversionOutOfBounds = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnDivideByZero = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnIncorrectStorageAccess = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnOutOfBoundsArrayAccess = true + config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnPopEmptyArray = true config.Fuzzing.Testing.PropertyTesting.Enabled = false - config.Fuzzing.Testing.AssertionTesting.Enabled = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnAssertion = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnAllocateTooMuchMemory = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnArithmeticUnderflow = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnCallUninitializedVariable = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnEnumTypeConversionOutOfBounds = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnDivideByZero = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnIncorrectStorageAccess = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnOutOfBoundsArrayAccess = true - config.Fuzzing.Testing.AssertionTesting.AssertionModes.FailOnPopEmptyArray = true + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -107,10 +107,10 @@ func TestAssertionsNotRequire(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/assertions/assert_not_require.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.TestLimit = 500 config.Fuzzing.Testing.PropertyTesting.Enabled = false - config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -129,11 +129,10 @@ func TestAssertionsAndProperties(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/assertions/assert_and_property_test.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.TestLimit = 500 config.Fuzzing.Testing.StopOnFailedTest = false - config.Fuzzing.Testing.PropertyTesting.Enabled = true - config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -155,11 +154,10 @@ func TestOptimizationMode(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.TestLimit = 10_000 // this test should expose a failure quickly. config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.AssertionTesting.Enabled = false - config.Fuzzing.Testing.OptimizationTesting.Enabled = true - config.Fuzzing.TestLimit = 10_000 // this test should expose a failure quickly. }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -168,11 +166,10 @@ func TestOptimizationMode(t *testing.T) { // 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) + for _, testCase := range testCases { + if optimizationTestCase, ok := testCase.(*OptimizationTestCase); ok { + assert.EqualValues(t, optimizationTestCase.Value().Cmp(big.NewInt(4241)), 0) + } } }, }) @@ -185,11 +182,13 @@ func TestChainBehaviour(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/chain/tx_out_of_gas.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Workers = 1 config.Fuzzing.TestLimit = uint64(config.Fuzzing.CallSequenceLength) // we just need a few oog txs to test config.Fuzzing.Timeout = 10 // to be safe, we set a 10s timeout config.Fuzzing.TransactionGasLimit = 500000 // we set this low, so contract execution runs out of gas earlier. + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -237,7 +236,7 @@ func TestCheatCodes(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} // some tests require full sequence + revert to test fully config.Fuzzing.Workers = 3 @@ -266,8 +265,8 @@ func TestConsoleLog(t *testing.T) { // These are the logs that should show up in the execution trace expectedLogs := []string{ "2", - "hello world", - "byte", + "68656c6c6f20776f726c64", // This is "hello world" in hex + "62797465", // This is "byte" in hex "i is 2", "% bool is true, addr is 0x0000000000000000000000000000000000000000, u is 100", } @@ -279,11 +278,10 @@ func TestConsoleLog(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.TestLimit = 10000 - // enable assertion testing only - config.Fuzzing.Testing.PropertyTesting.Enabled = true - config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -326,10 +324,12 @@ func TestDeploymentsInnerDeployments(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"InnerDeploymentFactory"} + config.Fuzzing.TargetContracts = []string{"InnerDeploymentFactory"} config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly. config.Fuzzing.Testing.StopOnFailedContractMatching = true config.Fuzzing.Testing.TestAllContracts = true // test dynamically deployed contracts + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -347,10 +347,12 @@ func TestDeploymentsInnerDeployments(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/deployments/inner_deployment_on_construction.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"InnerDeploymentFactory"} + config.Fuzzing.TargetContracts = []string{"InnerDeploymentFactory"} config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly. config.Fuzzing.Testing.StopOnFailedContractMatching = true config.Fuzzing.Testing.TestAllContracts = true // test dynamically deployed contracts + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -368,8 +370,33 @@ func TestDeploymentsInternalLibrary(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/deployments/internal_library.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestInternalLibrary"} + config.Fuzzing.TargetContracts = []string{"TestInternalLibrary"} config.Fuzzing.TestLimit = 100 // this test should expose a failure quickly. + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + // Check for any failed tests and verify coverage was captured + assertFailedTestsExpected(f, false) + assertCorpusCallSequencesCollected(f, true) + }, + }) +} + +// TestDeploymentsWithPayableConstructor runs a test to ensure that we can send ether to payable constructors +func TestDeploymentsWithPayableConstructors(t *testing.T) { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/deployments/deploy_payable_constructors.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"FirstContract", "SecondContract"} + config.Fuzzing.TargetContractsBalances = []*big.Int{big.NewInt(0), big.NewInt(1e18)} + config.Fuzzing.TestLimit = 1 // this should happen immediately + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -395,9 +422,11 @@ func TestDeploymentsSelfDestruct(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"InnerDeploymentFactory"} + config.Fuzzing.TargetContracts = []string{"InnerDeploymentFactory"} config.Fuzzing.TestLimit = 500 // this test should expose a failure quickly. config.Fuzzing.Testing.StopOnNoTests = false + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.Testing.TestAllContracts = true }, method: func(f *fuzzerTestContext) { @@ -442,9 +471,9 @@ func TestExecutionTraces(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.Testing.PropertyTesting.Enabled = false - config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -482,12 +511,11 @@ func TestTestingScope(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/deployments/testing_scope.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly. config.Fuzzing.Testing.TestAllContracts = testingAllContracts config.Fuzzing.Testing.StopOnFailedTest = false - config.Fuzzing.Testing.AssertionTesting.Enabled = true - config.Fuzzing.Testing.PropertyTesting.Enabled = true + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -517,7 +545,7 @@ func TestDeploymentsWithArgs(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/deployments/deployment_with_args.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"DeploymentWithArgs", "Dependent"} + config.Fuzzing.TargetContracts = []string{"DeploymentWithArgs", "Dependent"} config.Fuzzing.ConstructorArgs = map[string]map[string]any{ "DeploymentWithArgs": { "_x": "123456789", @@ -533,6 +561,8 @@ func TestDeploymentsWithArgs(t *testing.T) { } config.Fuzzing.Testing.StopOnFailedTest = false config.Fuzzing.TestLimit = 500 // this test should expose a failure quickly. + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -550,8 +580,10 @@ func TestValueGenerationGenerateAllTypes(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/value_generation/generate_all_types.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"GenerateAllTypes"} + config.Fuzzing.TargetContracts = []string{"GenerateAllTypes"} config.Fuzzing.TestLimit = 10_000 + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -583,7 +615,9 @@ func TestValueGenerationSolving(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: filePath, configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -639,8 +673,9 @@ func TestASTValueExtraction(t *testing.T) { filePath: "testdata/contracts/value_generation/ast_value_extraction.sol", configUpdates: func(config *config.ProjectConfig) { config.Fuzzing.TestLimit = 1 // stop immediately to simply see what values were mined. - config.Fuzzing.Testing.AssertionTesting.Enabled = true config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Fuzzing.TargetContracts = []string{"TestContract"} }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -674,9 +709,11 @@ func TestVMCorrectness(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/vm_tests/block_number_increasing.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.MaxBlockTimestampDelay = 1 // this contract require calls every block config.Fuzzing.MaxBlockNumberDelay = 1 // this contract require calls every block + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Start the fuzzer @@ -693,7 +730,7 @@ func TestVMCorrectness(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/vm_tests/block_number_increasing.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.MaxBlockTimestampDelay = 1 // this contract require calls every block config.Fuzzing.MaxBlockNumberDelay = 1 // this contract require calls every block }, @@ -712,7 +749,7 @@ func TestVMCorrectness(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/vm_tests/block_hash_store_check.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.TestLimit = 1_000 // this test should expose a failure quickly. config.Fuzzing.MaxBlockTimestampDelay = 1 // this contract require calls every block config.Fuzzing.MaxBlockNumberDelay = 1 // this contract require calls every block @@ -737,8 +774,10 @@ func TestCorpusReplayability(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/value_generation/match_uints_xy.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"TestContract"} + config.Fuzzing.TargetContracts = []string{"TestContract"} config.Fuzzing.CorpusDirectory = "corpus" + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Setup checks for event emissions @@ -776,14 +815,16 @@ func TestCorpusReplayability(t *testing.T) { }) } -// TestDeploymentOrderWithCoverage will ensure that changing the deployment order does not lead to the same coverage -// This is also proof that changing the order changes the addresses of the contracts leading to the coverage not being -// useful. +// TestDeploymentOrderWithCoverage will ensure that changing the order of deployment for the target contracts does not +// lead to the same coverage. This is also proof that changing the order changes the addresses of the contracts leading +// to the coverage not being useful. func TestDeploymentOrderWithCoverage(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ filePath: "testdata/contracts/deployments/deployment_order.sol", configUpdates: func(config *config.ProjectConfig) { - config.Fuzzing.DeploymentOrder = []string{"InheritedFirstContract", "InheritedSecondContract"} + config.Fuzzing.TargetContracts = []string{"InheritedFirstContract", "InheritedSecondContract"} + config.Fuzzing.Testing.AssertionTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, method: func(f *fuzzerTestContext) { // Setup checks for event emissions @@ -806,8 +847,8 @@ func TestDeploymentOrderWithCoverage(t *testing.T) { return nil }) - // Update the deployment order - f.fuzzer.config.Fuzzing.DeploymentOrder = []string{"InheritedSecondContract", "InheritedFirstContract"} + // Update the order of target contracts + f.fuzzer.config.Fuzzing.TargetContracts = []string{"InheritedSecondContract", "InheritedFirstContract"} // Note that the fuzzer won't spin up any workers or fuzz anything. We just want to test that the coverage // maps don't populate due to deployment order changes diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 300d97dc..af1a0b47 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -5,6 +5,7 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/config" "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/accounts/abi" "golang.org/x/exp/slices" "sync" @@ -44,6 +45,16 @@ func attachAssertionTestCaseProvider(fuzzer *Fuzzer) *AssertionTestCaseProvider // isTestableMethod checks whether the method is configured by the attached fuzzer to be a target of assertion testing. // Returns true if this target should be tested, false otherwise. func (t *AssertionTestCaseProvider) isTestableMethod(method abi.Method) bool { + // Do not test optimization tests + if utils.IsOptimizationTest(method, t.fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) { + return false + } + + // Do not test property tests + if utils.IsPropertyTest(method, t.fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) { + return false + } + // Only test constant methods (pure/view) if we are configured to. return !method.IsConstant() || t.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods } @@ -73,7 +84,7 @@ func (t *AssertionTestCaseProvider) checkAssertionFailures(callSequence calls.Ca panicCode := abiutils.GetSolidityPanicCode(lastExecutionResult.Err, lastExecutionResult.ReturnData, true) failure := false if panicCode != nil { - failure = encounteredAssertionFailure(panicCode.Uint64(), t.fuzzer.config.Fuzzing.Testing.AssertionTesting.AssertionModes) + failure = encounteredAssertionFailure(panicCode.Uint64(), t.fuzzer.config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig) } return &methodId, failure, nil @@ -87,8 +98,8 @@ func (t *AssertionTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) // Create a test case for every test method. for _, contract := range t.fuzzer.ContractDefinitions() { - // If we're not testing all contracts, verify the current contract is one we specified in our deployment order. - if !t.fuzzer.config.Fuzzing.Testing.TestAllContracts && !slices.Contains(t.fuzzer.config.Fuzzing.DeploymentOrder, contract.Name()) { + // If we're not testing all contracts, verify the current contract is one we specified in our target contracts + if !t.fuzzer.config.Fuzzing.Testing.TestAllContracts && !slices.Contains(t.fuzzer.config.Fuzzing.TargetContracts, contract.Name()) { continue } @@ -241,7 +252,7 @@ func (t *AssertionTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorke // code was enabled in the config. Note that the panic codes are defined in the abiutils package and that this function // panic if it is provided a panic code that is not defined in the abiutils package. // TODO: This is a terrible design and a future PR should be made to maintain assertion and panic logic correctly -func encounteredAssertionFailure(panicCode uint64, conf config.AssertionModesConfig) bool { +func encounteredAssertionFailure(panicCode uint64, conf config.PanicCodeConfig) bool { // Switch on panic code switch panicCode { case abiutils.PanicCodeCompilerInserted: diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index 424db7e5..93c7e536 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -5,10 +5,10 @@ import ( "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" + "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" + "golang.org/x/exp/slices" "math/big" - "strings" "sync" ) @@ -45,6 +45,11 @@ type optimizationTestCaseProviderWorkerState struct { // attachOptimizationTestCaseProvider attaches a new OptimizationTestCaseProvider to the Fuzzer and returns it. func attachOptimizationTestCaseProvider(fuzzer *Fuzzer) *OptimizationTestCaseProvider { + // If there are no testing prefixes, then there is no reason to attach a test case provider and subscribe to events + if len(fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) == 0 { + return nil + } + // Create a test case provider t := &OptimizationTestCaseProvider{ fuzzer: fuzzer, @@ -60,21 +65,6 @@ func attachOptimizationTestCaseProvider(fuzzer *Fuzzer) *OptimizationTestCasePro return t } -// isOptimizationTest check whether the method is an optimization test given potential naming prefixes it must conform to -// and its underlying input/output arguments. -func (t *OptimizationTestCaseProvider) isOptimizationTest(method abi.Method) bool { - // Loop through all enabled prefixes to find a match - for _, prefix := range t.fuzzer.Config().Fuzzing.Testing.OptimizationTesting.TestPrefixes { - if strings.HasPrefix(method.Name, prefix) { - // An optimization test must take no inputs and return an int256 - if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.IntTy && method.Outputs[0].Type.Size == 256 { - return true - } - } - } - return false -} - // runOptimizationTest executes a given optimization test method (w/ an optional execution trace) and returns the return value // from the optimization test method. This is called after every call the Fuzzer makes when testing call sequences for each test case. func (t *OptimizationTestCaseProvider) runOptimizationTest(worker *FuzzerWorker, optimizationTestMethod *contracts.DeployedContractMethod, trace bool) (*big.Int, *executiontracer.ExecutionTrace, error) { @@ -141,9 +131,14 @@ func (t *OptimizationTestCaseProvider) onFuzzerStarting(event FuzzerStartingEven // Create a test case for every optimization test method. for _, contract := range t.fuzzer.ContractDefinitions() { + // If we're not testing all contracts, verify the current contract is one we specified in our target contracts + if !t.fuzzer.config.Fuzzing.Testing.TestAllContracts && !slices.Contains(t.fuzzer.config.Fuzzing.TargetContracts, contract.Name()) { + continue + } + for _, method := range contract.CompiledContract().Abi.Methods { // Verify this method is an optimization test method - if !t.isOptimizationTest(method) { + if !utils.IsOptimizationTest(method, t.fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) { continue } // Create local variables to avoid pointer types in the loop being overridden. diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 10399cc5..9f5d8277 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -5,11 +5,10 @@ import ( "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" + "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" "math/big" - "strings" "sync" ) @@ -47,6 +46,11 @@ type propertyTestCaseProviderWorkerState struct { // attachPropertyTestCaseProvider attaches a new PropertyTestCaseProvider to the Fuzzer and returns it. func attachPropertyTestCaseProvider(fuzzer *Fuzzer) *PropertyTestCaseProvider { + // If there are no testing prefixes, then there is no reason to attach a test case provider and subscribe to events + if len(fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) == 0 { + return nil + } + // Create a test case provider t := &PropertyTestCaseProvider{ fuzzer: fuzzer, @@ -62,20 +66,6 @@ func attachPropertyTestCaseProvider(fuzzer *Fuzzer) *PropertyTestCaseProvider { return t } -// isPropertyTest check whether the method is a property test given potential naming prefixes it must conform to -// and its underlying input/output arguments. -func (t *PropertyTestCaseProvider) isPropertyTest(method abi.Method) bool { - // Loop through all enabled prefixes to find a match - for _, prefix := range t.fuzzer.Config().Fuzzing.Testing.PropertyTesting.TestPrefixes { - if strings.HasPrefix(method.Name, prefix) { - if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.BoolTy { - return true - } - } - } - return false -} - // checkPropertyTestFailed executes a given property test method to see if it returns a failed status. This is used to // facilitate testing of property test methods after every call the Fuzzer makes when testing call sequences. // A boolean indicating whether an execution trace should be captured and returned is provided to the method. @@ -143,14 +133,14 @@ func (t *PropertyTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) e // Create a test case for every property test method. for _, contract := range t.fuzzer.ContractDefinitions() { - // If we're not testing all contracts, verify the current contract is one we specified in our deployment order. - if !t.fuzzer.config.Fuzzing.Testing.TestAllContracts && !slices.Contains(t.fuzzer.config.Fuzzing.DeploymentOrder, contract.Name()) { + // If we're not testing all contracts, verify the current contract is one we specified in our target contracts. + if !t.fuzzer.config.Fuzzing.Testing.TestAllContracts && !slices.Contains(t.fuzzer.config.Fuzzing.TargetContracts, contract.Name()) { continue } for _, method := range contract.CompiledContract().Abi.Methods { // Verify this method is a property test method - if !t.isPropertyTest(method) { + if !utils.IsPropertyTest(method, t.fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) { continue } diff --git a/fuzzing/testdata/contracts/assertions/assert_and_property_test.sol b/fuzzing/testdata/contracts/assertions/assert_and_property_test.sol index 90051ba5..03100042 100644 --- a/fuzzing/testdata/contracts/assertions/assert_and_property_test.sol +++ b/fuzzing/testdata/contracts/assertions/assert_and_property_test.sol @@ -5,7 +5,7 @@ contract TestContract { assert(false); } - function fuzz_failing_property() public view returns (bool) { + function property_failing_property() public view returns (bool) { // ASSERTION: fail immediately. return false; } diff --git a/fuzzing/testdata/contracts/chain/tx_out_of_gas.sol b/fuzzing/testdata/contracts/chain/tx_out_of_gas.sol index 96fb55d0..6054a7d8 100644 --- a/fuzzing/testdata/contracts/chain/tx_out_of_gas.sol +++ b/fuzzing/testdata/contracts/chain/tx_out_of_gas.sol @@ -10,7 +10,7 @@ contract TestContract { } } - function fuzz_never_apply_state_when_oog() public view returns (bool) { + function property_never_apply_state_when_oog() public view returns (bool) { // ASSERTION: this state should never be applied, as our out of gas error should revert changes. return x == 0; } diff --git a/fuzzing/testdata/contracts/corpus_mutation/specific_call_sequence.sol b/fuzzing/testdata/contracts/corpus_mutation/specific_call_sequence.sol index e66c3771..b4356dac 100644 --- a/fuzzing/testdata/contracts/corpus_mutation/specific_call_sequence.sol +++ b/fuzzing/testdata/contracts/corpus_mutation/specific_call_sequence.sol @@ -29,7 +29,7 @@ contract TestContract { } } - function fuzz_solve_me() public view returns (bool) { + function property_solve_me() public view returns (bool) { // ASSERTION: The fuzzer should be able to fail this test case and solve all challenges. return index < 7; } diff --git a/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol b/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol new file mode 100644 index 00000000..223d85c4 --- /dev/null +++ b/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol @@ -0,0 +1,18 @@ +// This source file provides two contracts to test whether we are able to send ether to payable constructors. FirstContract +// should get no ether and while SecondContract should receive 1 ether. +contract FirstContract { + constructor() payable {} + + function property_contract_has_no_balance() public returns(bool) { + return address(this).balance == 0; + } +} + + +contract SecondContract { + constructor() payable {} + + function property_contract_has_balance() public returns(bool) { + return address(this).balance == 1 ether; + } +} diff --git a/fuzzing/testdata/contracts/deployments/deployment_order.sol b/fuzzing/testdata/contracts/deployments/deployment_order.sol index efdff8bc..f7c6f3ed 100644 --- a/fuzzing/testdata/contracts/deployments/deployment_order.sol +++ b/fuzzing/testdata/contracts/deployments/deployment_order.sol @@ -15,7 +15,7 @@ contract InheritedFirstContract is FirstContract { y = value + 9; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: x should never be 10 at the same time y is 80 return !(x == 10 && y == 80); } @@ -41,7 +41,7 @@ contract InheritedSecondContract is SecondContract { c = value + 7; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: a should never be 10 at the same time b is 80 at the same time c is 14 return !(a == 10 && b == 80 && c == 14); } diff --git a/fuzzing/testdata/contracts/deployments/deployment_with_args.sol b/fuzzing/testdata/contracts/deployments/deployment_with_args.sol index 11e62d12..05991266 100644 --- a/fuzzing/testdata/contracts/deployments/deployment_with_args.sol +++ b/fuzzing/testdata/contracts/deployments/deployment_with_args.sol @@ -15,15 +15,15 @@ contract DeploymentWithArgs { z = _z; } - function fuzz_checkX() public returns (bool) { + function property_checkX() public returns (bool) { return x != 123456789; } - function fuzz_checkY() public returns (bool) { + function property_checkY() public returns (bool) { return y != 0x5465; } - function fuzz_checkZ() public returns (bool) { + function property_checkZ() public returns (bool) { return z.a != 0x4d2; } @@ -40,7 +40,7 @@ contract Dependent { deployed = _deployed; } - function fuzz_checkDeployed() public returns (bool) { + function property_checkDeployed() public returns (bool) { return deployed == 0x0000000000000000000000000000000000000000; } diff --git a/fuzzing/testdata/contracts/deployments/inner_deployment.sol b/fuzzing/testdata/contracts/deployments/inner_deployment.sol index d853687e..dc26b5b7 100644 --- a/fuzzing/testdata/contracts/deployments/inner_deployment.sol +++ b/fuzzing/testdata/contracts/deployments/inner_deployment.sol @@ -1,7 +1,7 @@ // InnerDeploymentFactory deploys InnerDeployment when a method is called after deployment, and verifies the fuzzer can // match bytecode and fail the test appropriately. contract InnerDeployment { - function fuzz_inner_deployment() public view returns (bool) { + function property_inner_deployment() public view returns (bool) { // ASSERTION: Fail immediately. return false; } diff --git a/fuzzing/testdata/contracts/deployments/inner_deployment_on_construction.sol b/fuzzing/testdata/contracts/deployments/inner_deployment_on_construction.sol index 15aec27c..4e08a111 100644 --- a/fuzzing/testdata/contracts/deployments/inner_deployment_on_construction.sol +++ b/fuzzing/testdata/contracts/deployments/inner_deployment_on_construction.sol @@ -6,7 +6,7 @@ contract InnerDeployment { x = 7; } - function fuzz_inner_deployment() public view returns (bool) { + function property_inner_deployment() public view returns (bool) { // ASSERTION: Fail immediately. return false; } diff --git a/fuzzing/testdata/contracts/deployments/inner_inner_deployment.sol b/fuzzing/testdata/contracts/deployments/inner_inner_deployment.sol index 8a1b1140..7718fc82 100644 --- a/fuzzing/testdata/contracts/deployments/inner_inner_deployment.sol +++ b/fuzzing/testdata/contracts/deployments/inner_inner_deployment.sol @@ -2,7 +2,7 @@ // deployed, a method can be used to deploy an InnerInnerDeployment. We verify we can violate an invariant // in a two-layer deep dynamic deployment. contract InnerInnerDeployment { - function fuzz_inner_inner_deployment() public view returns (bool) { + function property_inner_inner_deployment() public view returns (bool) { // ASSERTION: Fail immediately. return false; } diff --git a/fuzzing/testdata/contracts/deployments/internal_library.sol b/fuzzing/testdata/contracts/deployments/internal_library.sol index ba03a08a..27201d43 100644 --- a/fuzzing/testdata/contracts/deployments/internal_library.sol +++ b/fuzzing/testdata/contracts/deployments/internal_library.sol @@ -28,7 +28,7 @@ contract TestInternalLibrary { return a + b; } - function fuzz_library_linking_broken() public view returns (bool) { + function property_library_linking_broken() public view returns (bool) { // ASSERTION: We should always be able to compute correctly. return !failedTest; } diff --git a/fuzzing/testdata/contracts/deployments/testing_scope.sol b/fuzzing/testdata/contracts/deployments/testing_scope.sol index af4f6605..e98aef38 100644 --- a/fuzzing/testdata/contracts/deployments/testing_scope.sol +++ b/fuzzing/testdata/contracts/deployments/testing_scope.sol @@ -6,7 +6,7 @@ contract TestContractChild { assert(false); } - function fuzz_failing_property_test_method_child() public view returns (bool) { + function property_failing_property_test_method_child() public view returns (bool) { return false; } } @@ -22,7 +22,7 @@ contract TestContract { assert(false); } - function fuzz_failing_property_test_method() public view returns (bool) { + function property_failing_property_test_method() public view returns (bool) { return false; } } diff --git a/fuzzing/testdata/contracts/value_generation/generate_all_types.sol b/fuzzing/testdata/contracts/value_generation/generate_all_types.sol index a4d0114c..e9a6fa75 100644 --- a/fuzzing/testdata/contracts/value_generation/generate_all_types.sol +++ b/fuzzing/testdata/contracts/value_generation/generate_all_types.sol @@ -24,7 +24,7 @@ contract GenerateAllTypes { s = ""; } - function fuzz_never_fail() public view returns (bool) { + function property_never_fail() public view returns (bool) { // ASSERTION: never fail, to keep testing value generation return true; } diff --git a/fuzzing/testdata/contracts/value_generation/match_addr_contract.sol b/fuzzing/testdata/contracts/value_generation/match_addr_contract.sol index 27654a16..9a5cdb7e 100644 --- a/fuzzing/testdata/contracts/value_generation/match_addr_contract.sol +++ b/fuzzing/testdata/contracts/value_generation/match_addr_contract.sol @@ -6,7 +6,7 @@ contract TestContract { a = value; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: a should not be the contract's address itself. return !(a == address(this)); } diff --git a/fuzzing/testdata/contracts/value_generation/match_addr_exact.sol b/fuzzing/testdata/contracts/value_generation/match_addr_exact.sol index 385e2bfb..b46d594e 100644 --- a/fuzzing/testdata/contracts/value_generation/match_addr_exact.sol +++ b/fuzzing/testdata/contracts/value_generation/match_addr_exact.sol @@ -12,7 +12,7 @@ contract TestContract { y = value; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: x and y should not equal the exact addresses below. return !(x == address(0x12345) && y == address(7)); } diff --git a/fuzzing/testdata/contracts/value_generation/match_addr_sender.sol b/fuzzing/testdata/contracts/value_generation/match_addr_sender.sol index 6be91398..b67d80e4 100644 --- a/fuzzing/testdata/contracts/value_generation/match_addr_sender.sol +++ b/fuzzing/testdata/contracts/value_generation/match_addr_sender.sol @@ -8,7 +8,7 @@ contract TestContract { sender = msg.sender; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: a should not be sender's address who set it. return a != sender; } diff --git a/fuzzing/testdata/contracts/value_generation/match_ints_xy.sol b/fuzzing/testdata/contracts/value_generation/match_ints_xy.sol index 2cb5ca2b..3aeff78e 100644 --- a/fuzzing/testdata/contracts/value_generation/match_ints_xy.sol +++ b/fuzzing/testdata/contracts/value_generation/match_ints_xy.sol @@ -12,7 +12,7 @@ contract TestContract { } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: x should never be -10 at the same time y is -62 return !(x == -10 && y == -62); } diff --git a/fuzzing/testdata/contracts/value_generation/match_payable_xy.sol b/fuzzing/testdata/contracts/value_generation/match_payable_xy.sol index 68221d87..b885c42e 100644 --- a/fuzzing/testdata/contracts/value_generation/match_payable_xy.sol +++ b/fuzzing/testdata/contracts/value_generation/match_payable_xy.sol @@ -11,7 +11,7 @@ contract TestContract { paidAmount2 = msg.value; } - function fuzz_never_pay_exact_amounts() public view returns (bool) { + function property_never_pay_exact_amounts() public view returns (bool) { // ASSERTION: paid amounts should never equal the exact numbers below. return !(paidAmount == 7777 && paidAmount2 == 8888); } diff --git a/fuzzing/testdata/contracts/value_generation/match_string_exact.sol b/fuzzing/testdata/contracts/value_generation/match_string_exact.sol index 6dda6770..80f15fae 100644 --- a/fuzzing/testdata/contracts/value_generation/match_string_exact.sol +++ b/fuzzing/testdata/contracts/value_generation/match_string_exact.sol @@ -6,7 +6,7 @@ contract TestContract { s = value; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: s should not be the MAGIC_STRING return keccak256(abi.encodePacked((s))) != keccak256(abi.encodePacked((MAGIC_STRING))); } diff --git a/fuzzing/testdata/contracts/value_generation/match_structs_xy.sol b/fuzzing/testdata/contracts/value_generation/match_structs_xy.sol index cc6d8495..817e079b 100644 --- a/fuzzing/testdata/contracts/value_generation/match_structs_xy.sol +++ b/fuzzing/testdata/contracts/value_generation/match_structs_xy.sol @@ -23,7 +23,7 @@ contract TestContract { s = ts; } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: x should never be 10 at the same time y is 80 return !(s.x == 10 && s.i.y == 80); } diff --git a/fuzzing/testdata/contracts/value_generation/match_uints_xy.sol b/fuzzing/testdata/contracts/value_generation/match_uints_xy.sol index a064f423..d465708a 100644 --- a/fuzzing/testdata/contracts/value_generation/match_uints_xy.sol +++ b/fuzzing/testdata/contracts/value_generation/match_uints_xy.sol @@ -12,7 +12,7 @@ contract TestContract { } - function fuzz_never_specific_values() public view returns (bool) { + function property_never_specific_values() public view returns (bool) { // ASSERTION: x should never be 10 at the same time y is 80 return !(x == 10 && y == 80); } diff --git a/fuzzing/testdata/contracts/vm_tests/block_hash_store_check.sol b/fuzzing/testdata/contracts/vm_tests/block_hash_store_check.sol index d9f3d8df..cbc757b8 100644 --- a/fuzzing/testdata/contracts/vm_tests/block_hash_store_check.sol +++ b/fuzzing/testdata/contracts/vm_tests/block_hash_store_check.sol @@ -49,7 +49,7 @@ contract TestContract { lastBlockNumber = block.number; } - function fuzz_violate_block_hash_continuity() public view returns (bool) { + function property_violate_block_hash_continuity() public view returns (bool) { // ASSERTION: we fail if our blockHash works as expected so our fuzzer will catch it. return !failedTest; } diff --git a/fuzzing/testdata/contracts/vm_tests/block_number_increasing.sol b/fuzzing/testdata/contracts/vm_tests/block_number_increasing.sol index cbcecab4..b8a6450c 100644 --- a/fuzzing/testdata/contracts/vm_tests/block_number_increasing.sol +++ b/fuzzing/testdata/contracts/vm_tests/block_number_increasing.sol @@ -10,7 +10,7 @@ contract TestContract { // This method does nothing but is left exposed so it can be called by the fuzzer to advance block.number } - function fuzz_increase_block_number_by_10() public view returns (bool) { + function property_increase_block_number_by_10() public view returns (bool) { // ASSERTION: block number should never increase more than 10 (we expect failure) return !(block.number - startingBlockNumber >= 10); } diff --git a/fuzzing/testdata/contracts/vm_tests/block_timestamp_increasing.sol b/fuzzing/testdata/contracts/vm_tests/block_timestamp_increasing.sol index f14bbaa7..7d6b0961 100644 --- a/fuzzing/testdata/contracts/vm_tests/block_timestamp_increasing.sol +++ b/fuzzing/testdata/contracts/vm_tests/block_timestamp_increasing.sol @@ -10,7 +10,7 @@ contract TestContract { // This method does nothing but is left exposed so it can be called by the fuzzer to advance blocks/timestamps. } - function fuzz_increase_block_timestamp() public view returns (bool) { + function property_increase_block_timestamp() public view returns (bool) { // ASSERTION: block timestamp should never increase more than 10 (we expect failure) return !(block.timestamp - startingBlockTimestamp >= 10); } diff --git a/fuzzing/utils/fuzz_method_utils.go b/fuzzing/utils/fuzz_method_utils.go new file mode 100644 index 00000000..1174246b --- /dev/null +++ b/fuzzing/utils/fuzz_method_utils.go @@ -0,0 +1,34 @@ +package utils + +import ( + "github.com/ethereum/go-ethereum/accounts/abi" + "strings" +) + +// IsOptimizationTest checks whether the method is an optimization test given potential naming prefixes it must conform to +// and its underlying input/output arguments. +func IsOptimizationTest(method abi.Method, prefixes []string) bool { + // Loop through all enabled prefixes to find a match + for _, prefix := range prefixes { + if strings.HasPrefix(method.Name, prefix) { + // An optimization test must take no inputs and return an int256 + if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.IntTy && method.Outputs[0].Type.Size == 256 { + return true + } + } + } + return false +} + +// IsPropertyTest checks whether the method is a property test given potential naming prefixes it must conform to +// and its underlying input/output arguments. +func IsPropertyTest(method abi.Method, prefixes []string) bool { + // Loop through all enabled prefixes to find a match + for _, prefix := range prefixes { + // The property test must simply have the right prefix and take no inputs + if strings.HasPrefix(method.Name, prefix) && len(method.Inputs) == 0 { + return true + } + } + return false +} diff --git a/fuzzing/valuegeneration/abi_values.go b/fuzzing/valuegeneration/abi_values.go index 56232e9d..616c8305 100644 --- a/fuzzing/valuegeneration/abi_values.go +++ b/fuzzing/valuegeneration/abi_values.go @@ -443,21 +443,17 @@ func encodeABIArgumentToString(inputType *abi.Type, value any) (string, error) { } return strconv.QuoteToASCII(str), nil case abi.BytesTy: - // Prepare a byte array. Return as a string enclosed with "". The returned string uses Go escape - // sequences (\t, \n, \xFF, \u0100) for non-ASCII characters and non-printable characters. b, ok := value.([]byte) if !ok { return "", fmt.Errorf("could not encode dynamic-sized bytes as the value provided is not of the correct type") } - return strconv.QuoteToASCII(string(b)), nil + // Convert the fixed byte array to a hex string + return hex.EncodeToString(b), nil case abi.FixedBytesTy: - // Prepare a fixed-size byte array. Return as a string enclosed with "". The returned string uses Go escape - // sequences (\t, \n, \xFF, \u0100) for non-ASCII characters and non-printable characters. - // TODO: Error checking to ensure `value` is of the correct type. b := reflectionutils.ArrayToSlice(reflect.ValueOf(value)).([]byte) - // Convert the byte array to a string and use the QuoteToASCII method to format the string with Go escape sequences. - return strconv.QuoteToASCII(string(b)), nil + // Convert the byte array to a hex string + return hex.EncodeToString(b), nil case abi.ArrayTy: // Prepare an array. Return as a string enclosed with [], where specific elements are comma-separated. reflectedArray := reflect.ValueOf(value) diff --git a/go.mod b/go.mod index 27b9f68c..45b23f24 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e // indirect + github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect @@ -64,6 +66,8 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/tools v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect diff --git a/go.sum b/go.sum index 4c98ad4e..bb098adc 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqR github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE= github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -38,6 +40,7 @@ github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoG github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811/go.mod h1:Nb5lgvnQ2+oGlE/EyZy4+2/CxRh9KfvCXnag1vtpxVM= github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -54,6 +57,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= @@ -67,11 +71,15 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY= +github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= +github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= @@ -81,6 +89,7 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -152,9 +161,12 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= @@ -207,7 +219,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= @@ -275,6 +289,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -334,6 +349,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -423,6 +440,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -454,6 +473,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -472,6 +492,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging/logger.go b/logging/logger.go index 48d5c539..7a6c09e4 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -5,7 +5,6 @@ import ( "github.com/crytic/medusa/logging/colors" "github.com/rs/zerolog" "io" - "os" "strings" ) @@ -57,11 +56,11 @@ type StructuredLogInfo map[string]any func NewLogger(level zerolog.Level) *Logger { return &Logger{ level: level, - structuredLogger: zerolog.New(os.Stdout).Level(zerolog.Disabled), + structuredLogger: zerolog.New(nil).Level(level), structuredWriters: make([]io.Writer, 0), - unstructuredLogger: zerolog.New(os.Stdout).Level(zerolog.Disabled), + unstructuredLogger: zerolog.New(nil).Level(level), unstructuredWriters: make([]io.Writer, 0), - unstructuredColorLogger: zerolog.New(os.Stdout).Level(zerolog.Disabled), + unstructuredColorLogger: zerolog.New(nil).Level(level), unstructuredColorWriters: make([]io.Writer, 0), } } @@ -206,6 +205,7 @@ func (l *Logger) SetLevel(level zerolog.Level) { l.structuredLogger = l.structuredLogger.Level(level) l.unstructuredColorLogger = l.unstructuredColorLogger.Level(level) l.unstructuredLogger = l.unstructuredLogger.Level(level) + } // Trace is a wrapper function that will log a trace event From f0084e5e645a0196758910f6d20d1091f224d2a0 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 20 Feb 2024 23:53:16 -0500 Subject: [PATCH 046/109] fix OOB panic (#303) --- fuzzing/coverage/coverage_maps.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fuzzing/coverage/coverage_maps.go b/fuzzing/coverage/coverage_maps.go index e9a1343c..1e8a48d6 100644 --- a/fuzzing/coverage/coverage_maps.go +++ b/fuzzing/coverage/coverage_maps.go @@ -344,10 +344,9 @@ func (cm *CoverageMapBytecodeData) update(coverageMap *CoverageMapBytecodeData) return true, nil } - // Update each byte which represents a position in the bytecode which was covered. We ignore any size - // differences as init bytecode can have arbitrary length arguments appended. + // 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++ { + for i := 0; i < len(cm.executedFlags) && i < len(coverageMap.executedFlags); i++ { if cm.executedFlags[i] == 0 && coverageMap.executedFlags[i] != 0 { cm.executedFlags[i] = 1 changed = true From 729b78ae435f1745c27ea7d296f8c04f4769565c Mon Sep 17 00:00:00 2001 From: David Pokora Date: Mon, 26 Feb 2024 12:11:50 -0800 Subject: [PATCH 047/109] Added a unique exit code for failed tests, added generic way to bubble up errors with exit codes from cmd, organized exit code definitions into one package (#301) Co-authored-by: anishnaik --- cmd/exitcodes/error_with_exit_code.go | 37 +++++++++++++++++++++++++++ cmd/exitcodes/exit_codes.go | 21 +++++++++++++++ cmd/fuzz.go | 6 +++++ main.go | 6 ++--- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 cmd/exitcodes/error_with_exit_code.go create mode 100644 cmd/exitcodes/exit_codes.go diff --git a/cmd/exitcodes/error_with_exit_code.go b/cmd/exitcodes/error_with_exit_code.go new file mode 100644 index 00000000..eb2367a7 --- /dev/null +++ b/cmd/exitcodes/error_with_exit_code.go @@ -0,0 +1,37 @@ +package exitcodes + +// ErrorWithExitCode is an `error` type that wraps an existing error and exit code, providing exit codes +// for a given error if they are bubbled up to the top-level. +type ErrorWithExitCode struct { + err error + exitCode int +} + +// NewErrorWithExitCode creates a new error (ErrorWithExitCode) with the provided internal error and exit code. +func NewErrorWithExitCode(err error, exitCode int) *ErrorWithExitCode { + return &ErrorWithExitCode{ + err: err, + exitCode: exitCode, + } +} + +// Error returns the error message string, implementing the `error` interface. +func (e *ErrorWithExitCode) Error() string { + return e.err.Error() +} + +// GetErrorExitCode checks the given exit code that the application should exit with, if this error is bubbled to +// the top-level. This will be 0 for a nil error, 1 for a generic error, or arbitrary if the error is of type +// ErrorWithExitCode. +// Returns the exit code associated with the error. +func GetErrorExitCode(err error) int { + // If we have no error, return 0, if we have a generic error, return 1, if we have a custom error code, unwrap + // and return it. + if err == nil { + return ExitCodeSuccess + } else if unwrappedErr, ok := err.(*ErrorWithExitCode); ok { + return unwrappedErr.exitCode + } else { + return ExitCodeGeneralError + } +} diff --git a/cmd/exitcodes/exit_codes.go b/cmd/exitcodes/exit_codes.go new file mode 100644 index 00000000..cb6c7c98 --- /dev/null +++ b/cmd/exitcodes/exit_codes.go @@ -0,0 +1,21 @@ +package exitcodes + +const ( + // ================================ + // Platform-universal exit codes + // ================================ + + // ExitCodeSuccess indicates no errors or failures had occurred. + ExitCodeSuccess = 0 + + // ExitCodeGeneralError indicates some type of general error occurred. + ExitCodeGeneralError = 1 + + // ================================ + // Application-specific exit codes + // ================================ + // Note: Despite not being standardized, exit codes 2-5 are often used for common use cases, so we avoid them. + + // ExitCodeTestFailed indicates a test case had failed. + ExitCodeTestFailed = 7 +) diff --git a/cmd/fuzz.go b/cmd/fuzz.go index 3fc22fd3..28453ae8 100644 --- a/cmd/fuzz.go +++ b/cmd/fuzz.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/crytic/medusa/cmd/exitcodes" "github.com/crytic/medusa/logging/colors" "os" "os/signal" @@ -160,5 +161,10 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { // Start the fuzzing process with our cancellable context. err = fuzzer.Start() + // If we have no error and failed test cases, we'll want to return a special exit code + if err == nil && len(fuzzer.TestCasesWithStatus(fuzzing.TestCaseStatusFailed)) > 0 { + return exitcodes.NewErrorWithExitCode(err, exitcodes.ExitCodeTestFailed) + } + return err } diff --git a/main.go b/main.go index 4902eaeb..ad2537e1 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/crytic/medusa/cmd" + "github.com/crytic/medusa/cmd/exitcodes" "os" ) @@ -9,7 +10,6 @@ func main() { // Run our root CLI command, which contains all underlying command logic and will handle parsing/invocation. err := cmd.Execute() - if err != nil { - os.Exit(1) - } + // Determine the exit code from any potential error and exit out. + os.Exit(exitcodes.GetErrorExitCode(err)) } From c0c3718b29ffb75dcfd79414553c16067392fbb1 Mon Sep 17 00:00:00 2001 From: David Pokora Date: Wed, 28 Feb 2024 13:58:42 -0800 Subject: [PATCH 048/109] Fix corpus call method resolution bug, improve startup logging (#308) * * Fixed a corpus call method resolution bug. * Deprecated "methodName" in corpus (lack of support for function overloading). To be removed later, in favor of new "methodSignature" key. * Improved fuzzer initialization logging * Print basic metrics for corpus health on startup * Reorder printing to avoid "Creating X workers" message after "fuzz: elapsed[...]" message. * Update corpus health log for readability --------- Co-authored-by: anishnaik --- fuzzing/calls/call_message_abi_values.go | 44 +++++++++++++++++++----- fuzzing/calls/call_sequence.go | 12 ++++++- fuzzing/corpus/corpus.go | 21 +++++++---- fuzzing/fuzzer.go | 21 +++++++++-- 4 files changed, 79 insertions(+), 19 deletions(-) diff --git a/fuzzing/calls/call_message_abi_values.go b/fuzzing/calls/call_message_abi_values.go index 1f3bbdbf..8aec6da3 100644 --- a/fuzzing/calls/call_message_abi_values.go +++ b/fuzzing/calls/call_message_abi_values.go @@ -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. @@ -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 @@ -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"` } @@ -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, } @@ -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. @@ -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) @@ -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 } diff --git a/fuzzing/calls/call_sequence.go b/fuzzing/calls/call_sequence.go index 53ae874e..1321da28 100644 --- a/fuzzing/calls/call_sequence.go +++ b/fuzzing/calls/call_sequence.go @@ -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. diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 9cc4cda6..a452ddea 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -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 } } @@ -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() @@ -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. @@ -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 } } } @@ -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. diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 3b2da8fe..f2856b1e 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -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 @@ -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) @@ -640,6 +640,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") err = f.Hooks.ChainSetupFunc(f, baseTestChain) if err != nil { f.logger.Error("Failed to initialize the test chain", err) @@ -647,12 +648,26 @@ func (f *Fuzzer) Start() error { } // 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() From c5d71288574cdf6072946b2f70ca9b65b7509e54 Mon Sep 17 00:00:00 2001 From: David Pokora Date: Wed, 28 Feb 2024 14:43:48 -0800 Subject: [PATCH 049/109] Add shrinking limits (#297) * Add shrinking limits, improve "call removal" logic, fixed a bug where corpus results could re-record if replayed/reshrunk differently * Removed comment indicating shrinkLimit must be greater than zero (untrue) * Re-order unexecuted sequences so test result corpus items are replayed first * change where shrinkIncrement is updated --------- Co-authored-by: Anish Naik --- fuzzing/config/config.go | 7 +- fuzzing/config/config_defaults.go | 1 + fuzzing/corpus/corpus.go | 12 +- fuzzing/fuzzer.go | 2 + fuzzing/fuzzer_metrics.go | 14 +++ fuzzing/fuzzer_worker.go | 128 ++++++++++++-------- fuzzing/fuzzer_worker_sequence_generator.go | 2 +- 7 files changed, 107 insertions(+), 59 deletions(-) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index fd12bd53..2fbb1e64 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -38,14 +38,17 @@ type FuzzingConfig struct { // so that memory from its underlying chain is freed. WorkerResetLimit int `json:"workerResetLimit"` - // Timeout describes a time in seconds for which the fuzzing operation should run. Providing negative or zero value - // will result in no timeout. + // Timeout describes a time threshold in seconds for which the fuzzing operation should run. Providing negative or + // zero value will result in no timeout. Timeout int `json:"timeout"` // TestLimit describes a threshold for the number of transactions to test, after which it will exit. This number // must be non-negative. A zero value indicates the test limit should not be enforced. TestLimit uint64 `json:"testLimit"` + // ShrinkLimit describes a threshold for the iterations (call sequence tests) which shrinking should perform. + ShrinkLimit uint64 `json:"shrinkLimit"` + // CallSequenceLength describes the maximum length a transaction sequence can be generated as. CallSequenceLength int `json:"callSequenceLength"` diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index dd5e1c17..1fed1120 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -37,6 +37,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { WorkerResetLimit: 50, Timeout: 0, TestLimit: 0, + ShrinkLimit: 5_000, CallSequenceLength: 100, TargetContracts: []string{}, TargetContractsBalances: []*big.Int{}, diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index a452ddea..819da24a 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -293,15 +293,21 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions // Next we replay every call sequence, checking its validity on this chain and measuring coverage. Valid sequences // are added to the corpus for mutations, re-execution, etc. - err = c.initializeSequences(c.mutableSequenceFiles, testChain, deployedContracts, true) + // + // The order of initializations here is important, as it determines the order of "unexecuted sequences" to replay + // when the fuzzer's worker starts up. We want to replay test results first, so that other corpus items + // do not trigger the same test failures instead. + err = c.initializeSequences(c.testResultSequenceFiles, testChain, deployedContracts, false) if err != nil { return 0, 0, err } - err = c.initializeSequences(c.immutableSequenceFiles, testChain, deployedContracts, false) + + err = c.initializeSequences(c.mutableSequenceFiles, testChain, deployedContracts, true) if err != nil { return 0, 0, err } - err = c.initializeSequences(c.testResultSequenceFiles, testChain, deployedContracts, false) + + err = c.initializeSequences(c.immutableSequenceFiles, testChain, deployedContracts, false) if err != nil { return 0, 0, err } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index f2856b1e..dde89e9c 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -749,6 +749,7 @@ func (f *Fuzzer) printMetricsLoop() { callsTested := f.metrics.CallsTested() sequencesTested := f.metrics.SequencesTested() workerStartupCount := f.metrics.WorkerStartupCount() + workersShrinking := f.metrics.WorkersShrinkingCount() // Calculate time elapsed since the last update secondsSinceLastUpdate := time.Since(lastPrintedTime).Seconds() @@ -767,6 +768,7 @@ func (f *Fuzzer) printMetricsLoop() { 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) 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) } diff --git a/fuzzing/fuzzer_metrics.go b/fuzzing/fuzzer_metrics.go index 9d64c91a..70fc3788 100644 --- a/fuzzing/fuzzer_metrics.go +++ b/fuzzing/fuzzer_metrics.go @@ -19,6 +19,9 @@ type fuzzerWorkerMetrics struct { // workerStartupCount describes the amount of times the worker was generated, or re-generated for this index. workerStartupCount *big.Int + + // shrinking indicates whether the fuzzer worker is currently shrinking. + shrinking bool } // newFuzzerMetrics obtains a new FuzzerMetrics struct for a given number of workers specified by workerCount. @@ -63,3 +66,14 @@ func (m *FuzzerMetrics) WorkerStartupCount() *big.Int { } return workerStartupCount } + +// WorkersShrinkingCount returns the amount of workers currently performing shrinking operations. +func (m *FuzzerMetrics) WorkersShrinkingCount() uint64 { + shrinkingCount := uint64(0) + for _, workerMetrics := range m.workerMetrics { + if workerMetrics.shrinking { + shrinkingCount++ + } + } + return shrinkingCount +} diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index a7ac3f4d..7ee4e93a 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -315,8 +315,8 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // If this was not a new call sequence, indicate not to save the shrunken result to the corpus again. if !isNewSequence { - for _, shrinkRequest := range shrinkCallSequenceRequests { - shrinkRequest.RecordResultInCorpus = false + for i := 0; i < len(shrinkCallSequenceRequests); i++ { + shrinkCallSequenceRequests[i].RecordResultInCorpus = false } } @@ -391,73 +391,95 @@ func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.Ca // shrinkCallSequence takes a provided call sequence and attempts to shrink it by looking for redundant // calls which can be removed, and values which can be minimized, while continuing to satisfy the provided shrink // verifier. +// +// This function should *always* be called if there are shrink requests, and should always report a result, +// even if it is the original sequence provided. +// // Returns a call sequence that was optimized to include as little calls as possible to trigger the // expected conditions, or an error if one occurred. func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shrinkRequest ShrinkCallSequenceRequest) (calls.CallSequence, error) { // Define a variable to track our most optimized sequence across all optimization iterations. optimizedSequence := callSequence - // First try to remove any calls we can. We go from start to end to avoid index shifting. - for i := 0; i < len(optimizedSequence); { - // If our fuzzer context is done, exit out immediately without results. - if utils.CheckContextDone(fw.fuzzer.ctx) { - return nil, nil - } - - // Recreate our current optimized sequence without the item at this index - possibleShrunkSequence, err := optimizedSequence.Clone() - if err != nil { - return nil, err - } - possibleShrunkSequence = append(possibleShrunkSequence[:i], possibleShrunkSequence[i+1:]...) - - // Test the shrunken sequence. - validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) - if err != nil { - return nil, err - } - - // If this current sequence satisfied our conditions, set it as our optimized sequence. - if validShrunkSequence { - optimizedSequence = possibleShrunkSequence - } else { - // We didn't remove an item at this index, so we'll iterate to the next one. - i++ - } + // Obtain our shrink limits and begin shrinking. + shrinkIteration := uint64(0) + shrinkLimit := fw.fuzzer.config.Fuzzing.ShrinkLimit + shrinkingEnded := func() bool { + return shrinkIteration >= shrinkLimit || utils.CheckContextDone(fw.fuzzer.ctx) } + if shrinkLimit > 0 { + // The first pass of shrinking is greedy towards trying to remove any unnecessary calls. + // For each call in the sequence, the following removal strategies are used: + // 1) Plain removal (lower block/time gap between surrounding blocks, maintain properties of max delay) + // 2) Add block/time delay to previous call (retain original block/time, possibly exceed max delays) + // At worst, this costs `2 * len(callSequence)` shrink iterations. + fw.workerMetrics().shrinking = true + for removalStrategy := 0; removalStrategy < 2 && !shrinkingEnded(); removalStrategy++ { + for i := len(optimizedSequence) - 1; i >= 0 && !shrinkingEnded(); i-- { + // Recreate our current optimized sequence without the item at this index + possibleShrunkSequence, err := optimizedSequence.Clone() + removedCall := possibleShrunkSequence[i] + if err != nil { + return nil, err + } + possibleShrunkSequence = append(possibleShrunkSequence[:i], possibleShrunkSequence[i+1:]...) + + // Exercise the next removal strategy for this call. + if removalStrategy == 0 { + // Case 1: Plain removal. + } else if removalStrategy == 1 { + // Case 2: Add block/time delay to previous call. + if i > 0 { + possibleShrunkSequence[i-1].BlockNumberDelay += removedCall.BlockNumberDelay + possibleShrunkSequence[i-1].BlockTimestampDelay += removedCall.BlockTimestampDelay + } + } - // Next try to shrink our values of every transaction a given number of rounds. - for i := 0; i < len(optimizedSequence); i++ { - for optimizationRound := 0; optimizationRound < 200; optimizationRound++ { - // If our fuzzer context is done, exit out immediately without results. - if utils.CheckContextDone(fw.fuzzer.ctx) { - return nil, nil + // Test the shrunken sequence. + validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) + shrinkIteration++ + if err != nil { + return nil, err + } + + // If the current sequence satisfied our conditions, set it as our optimized sequence. + if validShrunkSequence { + optimizedSequence = possibleShrunkSequence + } } + } - // Clone the optimized sequence. - possibleShrunkSequence, _ := optimizedSequence.Clone() + // The second pass of shrinking attempts to shrink values for each call in our call sequence. + // This is performed exhaustively in a round-robin fashion for each call, until the shrink limit is hit. + for !shrinkingEnded() { + for i := len(optimizedSequence) - 1; i >= 0 && !shrinkingEnded(); i-- { + // Clone the optimized sequence. + possibleShrunkSequence, _ := optimizedSequence.Clone() + + // Loop for each argument in the currently indexed call to mutate it. + abiValuesMsgData := possibleShrunkSequence[i].Call.DataAbiValues + for j := 0; j < len(abiValuesMsgData.InputValues); j++ { + mutatedInput, err := valuegeneration.MutateAbiValue(fw.sequenceGenerator.config.ValueGenerator, fw.shrinkingValueMutator, &abiValuesMsgData.Method.Inputs[j].Type, abiValuesMsgData.InputValues[j]) + if err != nil { + return nil, fmt.Errorf("error when shrinking call sequence input argument: %v", err) + } + abiValuesMsgData.InputValues[j] = mutatedInput + } - // Loop for each argument in the currently indexed call to mutate it. - abiValuesMsgData := possibleShrunkSequence[i].Call.DataAbiValues - for j := 0; j < len(abiValuesMsgData.InputValues); j++ { - mutatedInput, err := valuegeneration.MutateAbiValue(fw.sequenceGenerator.config.ValueGenerator, fw.shrinkingValueMutator, &abiValuesMsgData.Method.Inputs[j].Type, abiValuesMsgData.InputValues[j]) + // Test the shrunken sequence. + validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) + shrinkIteration++ if err != nil { - return nil, fmt.Errorf("error when shrinking call sequence input argument: %v", err) + return nil, err } - abiValuesMsgData.InputValues[j] = mutatedInput - } - // Test the shrunken sequence. - validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) - if err != nil { - return nil, err - } - - // If this current sequence satisfied our conditions, set it as our optimized sequence. - if validShrunkSequence { - optimizedSequence = possibleShrunkSequence + // If this current sequence satisfied our conditions, set it as our optimized sequence. + if validShrunkSequence { + optimizedSequence = possibleShrunkSequence + } } } + fw.workerMetrics().shrinking = false } // If the shrink request wanted the sequence recorded in the corpus, do so now. diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index 2b9358a4..8cfe21e2 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -192,7 +192,7 @@ func (g *CallSequenceGenerator) InitializeNextSequence() (bool, error) { g.fetchIndex = 0 g.prefetchModifyCallFunc = nil - // Check if there are any previously une-xecuted corpus call sequences. If there are, the fuzzer should execute + // Check if there are any previously un-executed corpus call sequences. If there are, the fuzzer should execute // those first. unexecutedSequence := g.worker.fuzzer.corpus.UnexecutedCallSequence() if unexecutedSequence != nil { From 72e9b8586ad93b37ff9063ccf3f5b471f934c264 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 28 Feb 2024 17:58:12 -0500 Subject: [PATCH 050/109] update version (#310) --- cmd/root.go | 2 +- go.mod | 4 ---- go.sum | 21 --------------------- 3 files changed, 1 insertion(+), 26 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index ae03d15d..6dd04a3d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.2" +const version = "0.1.3" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ diff --git a/go.mod b/go.mod index 45b23f24..27b9f68c 100644 --- a/go.mod +++ b/go.mod @@ -32,8 +32,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect - github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e // indirect - github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect @@ -66,8 +64,6 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/mod v0.9.0 // indirect - golang.org/x/tools v0.7.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect diff --git a/go.sum b/go.sum index bb098adc..4c98ad4e 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,6 @@ github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqR github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE= github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -40,7 +38,6 @@ github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoG github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811/go.mod h1:Nb5lgvnQ2+oGlE/EyZy4+2/CxRh9KfvCXnag1vtpxVM= github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -57,7 +54,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= @@ -71,15 +67,11 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e h1:bBLctRc7kr01YGvaDfgLbTwjFNW5jdp5y5rj8XXBHfY= -github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= -github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc= -github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= @@ -89,7 +81,6 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -161,12 +152,9 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= @@ -219,9 +207,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= @@ -289,7 +275,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -349,8 +334,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -440,8 +423,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -473,7 +454,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -492,7 +472,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 092b433c9d4d2d65d8bae3a2c2d5b4d6e22c7574 Mon Sep 17 00:00:00 2001 From: Damilola Edwards Date: Mon, 25 Mar 2024 18:52:41 +0100 Subject: [PATCH 051/109] Feat: snapshot and revertTo cheatcodes (#276) --- chain/standard_cheat_code_contract.go | 21 +++++++ fuzzing/fuzzer_test.go | 1 + .../cheat_codes/vm/snapshot_and_revert_to.sol | 58 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 fuzzing/testdata/contracts/cheat_codes/vm/snapshot_and_revert_to.sol diff --git a/chain/standard_cheat_code_contract.go b/chain/standard_cheat_code_contract.go index 79c2d3ad..144c7e77 100644 --- a/chain/standard_cheat_code_contract.go +++ b/chain/standard_cheat_code_contract.go @@ -283,6 +283,27 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, }, ) + // snapshot: Takes a snapshot of the current state of the evm and returns the id associated with the snapshot + contract.addMethod( + "snapshot", abi.Arguments{}, abi.Arguments{{Type: typeUint256}}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + snapshotID := tracer.evm.StateDB.Snapshot() + + return []any{snapshotID}, nil + }, + ) + + // revertTo(uint256): Revert the state of the evm to a previous snapshot. Takes the snapshot id to revert to. + contract.addMethod( + "revertTo", abi.Arguments{{Type: typeUint256}}, abi.Arguments{{Type: typeBool}}, + func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { + snapshotID := inputs[0].(*big.Int) + tracer.evm.StateDB.RevertToSnapshot(int(snapshotID.Int64())) + + return []any{true}, nil + }, + ) + // FFI: Run arbitrary command on base OS contract.addMethod( "ffi", abi.Arguments{{Type: typeStringSlice}}, abi.Arguments{{Type: typeBytes}}, diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 9167b183..b5ad96cc 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -208,6 +208,7 @@ func TestCheatCodes(t *testing.T) { "testdata/contracts/cheat_codes/utils/to_string.sol", "testdata/contracts/cheat_codes/utils/sign.sol", "testdata/contracts/cheat_codes/utils/parse.sol", + "testdata/contracts/cheat_codes/vm/snapshot_and_revert_to.sol", "testdata/contracts/cheat_codes/vm/coinbase.sol", "testdata/contracts/cheat_codes/vm/chain_id.sol", "testdata/contracts/cheat_codes/vm/deal.sol", diff --git a/fuzzing/testdata/contracts/cheat_codes/vm/snapshot_and_revert_to.sol b/fuzzing/testdata/contracts/cheat_codes/vm/snapshot_and_revert_to.sol new file mode 100644 index 00000000..577ff194 --- /dev/null +++ b/fuzzing/testdata/contracts/cheat_codes/vm/snapshot_and_revert_to.sol @@ -0,0 +1,58 @@ +// This test ensures that we can take a snapshot of the current state of the testchain and revert to the state at that snapshot using the snapshot and revertTo cheatcodes +pragma solidity ^0.8.0; + +interface CheatCodes { + function warp(uint256) external; + + function deal(address, uint256) external; + + function snapshot() external returns (uint256); + + function revertTo(uint256) external returns (bool); +} + +struct Storage { + uint slot0; + uint slot1; +} + +contract TestContract { + Storage store; + uint256 timestamp; + + function test() public { + // Obtain our cheat code contract reference. + CheatCodes cheats = CheatCodes( + 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D + ); + + store.slot0 = 10; + store.slot1 = 20; + timestamp = block.timestamp; + cheats.deal(address(this), 5 ether); + + // Save state + uint256 snapshot = cheats.snapshot(); + + // Change state + store.slot0 = 300; + store.slot1 = 400; + cheats.deal(address(this), 500 ether); + cheats.warp(12345); + + // Assert that state has been changed + assert(store.slot0 == 300); + assert(store.slot1 == 400); + assert(address(this).balance == 500 ether); + assert(block.timestamp == 12345); + + // Revert to snapshot + cheats.revertTo(snapshot); + + // Ensure state has been reset + assert(store.slot0 == 10); + assert(store.slot1 == 20); + assert(address(this).balance == 5 ether); + assert(block.timestamp == timestamp); + } +} From da11676911d5519f5dcbfa42551527e5d5bd6649 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 25 Mar 2024 13:03:04 -0500 Subject: [PATCH 052/109] fix: attach execution trace to reverting properties (#335) Co-authored-by: anishnaik --- fuzzing/test_case_property_provider.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 9f5d8277..6d85db17 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -2,14 +2,15 @@ package fuzzing import ( "fmt" + "math/big" + "sync" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" - "math/big" - "sync" ) // PropertyTestCaseProvider is a provider for on-chain property tests. @@ -100,7 +101,7 @@ func (t *PropertyTestCaseProvider) checkPropertyTestFailed(worker *FuzzerWorker, // If our property test method call failed, we flag a failed test. if executionResult.Failed() { - return true, nil, nil + return true, executionTrace, nil } // Decode our ABI outputs From 382f7f2ce195dc5ac2b9117a329f62c2b87720b2 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 25 Mar 2024 13:14:56 -0500 Subject: [PATCH 053/109] fix: use signature in traces to handle overloaded function names (#336) * fix: use signature in traces to handle overloaded function names * fix test --------- Co-authored-by: anishnaik --- fuzzing/calls/call_sequence.go | 5 +++-- fuzzing/executiontracer/execution_trace.go | 7 ++++--- fuzzing/fuzzer_test.go | 9 +++++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/fuzzing/calls/call_sequence.go b/fuzzing/calls/call_sequence.go index 1321da28..f4e06c4f 100644 --- a/fuzzing/calls/call_sequence.go +++ b/fuzzing/calls/call_sequence.go @@ -3,6 +3,8 @@ package calls import ( "encoding/binary" "fmt" + "strconv" + "github.com/crytic/medusa/chain" chainTypes "github.com/crytic/medusa/chain/types" fuzzingTypes "github.com/crytic/medusa/fuzzing/contracts" @@ -13,7 +15,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - "strconv" ) // CallSequence describes a sequence of calls sent to a chain. @@ -230,7 +231,7 @@ func (cse *CallSequenceElement) String() string { method, err := cse.Method() methodName := "" if err == nil && method != nil { - methodName = method.Name + methodName = method.Sig } // Next decode our arguments (we jump four bytes to skip the function selector) diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index 695f595b..90a4df05 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -3,6 +3,9 @@ package executiontracer import ( "encoding/hex" "fmt" + "regexp" + "strings" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/contracts" @@ -12,8 +15,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" coreTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "regexp" - "strings" ) // ExecutionTrace contains information recorded by an ExecutionTracer. It contains information about each call @@ -77,7 +78,7 @@ func (t *ExecutionTrace) generateCallFrameEnterElements(callFrame *CallFrame) ([ } else { method, err = callFrame.CodeContractAbi.MethodById(callFrame.InputData) if err == nil { - methodName = method.Name + methodName = method.Sig } } } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index b5ad96cc..d0a47392 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -2,15 +2,16 @@ package fuzzing import ( "encoding/hex" + "math/big" + "math/rand" + "testing" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/events" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" - "math/big" - "math/rand" - "testing" "github.com/crytic/medusa/fuzzing/config" "github.com/stretchr/testify/assert" @@ -461,7 +462,7 @@ func TestDeploymentsSelfDestruct(t *testing.T) { func TestExecutionTraces(t *testing.T) { expectedMessagesPerTest := map[string][]string{ "testdata/contracts/execution_tracing/call_and_deployment_args.sol": {"Hello from deployment args!", "Hello from call args!"}, - "testdata/contracts/execution_tracing/cheatcodes.sol": {"StdCheats.toString(true)"}, + "testdata/contracts/execution_tracing/cheatcodes.sol": {"StdCheats.toString(bool)(true)"}, "testdata/contracts/execution_tracing/event_emission.sol": {"TestEvent", "TestIndexedEvent", "TestMixedEvent", "Hello from event args!", "Hello from library event args!"}, "testdata/contracts/execution_tracing/proxy_call.sol": {"TestContract -> InnerDeploymentContract.setXY", "Hello from proxy call args!"}, "testdata/contracts/execution_tracing/revert_custom_error.sol": {"CustomError", "Hello from a custom error!"}, From e7b5e153a2ee474fa5989c24c965827100886f2b Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 27 Mar 2024 11:26:56 -0500 Subject: [PATCH 054/109] feat: add execution trace for failed target contract deployments (#337) * feat: add execution trace for failed target contract deployments * fix bugs in logging and improve trace output to console --------- Co-authored-by: anishnaik --- fuzzing/executiontracer/execution_tracer.go | 9 ++- fuzzing/fuzzer.go | 67 +++++++++++++++------ fuzzing/fuzzer_hooks.go | 4 +- fuzzing/fuzzer_test.go | 3 +- logging/log_buffer.go | 8 ++- logging/logger.go | 16 ----- 6 files changed, 67 insertions(+), 40 deletions(-) diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index ed96dbe1..17ec57fe 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -1,6 +1,8 @@ package executiontracer import ( + "math/big" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/contracts" "github.com/ethereum/go-ethereum/common" @@ -8,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" "golang.org/x/exp/slices" - "math/big" ) // CallWithExecutionTrace obtains an execution trace for a given call, on the provided chain, using the state @@ -118,6 +119,12 @@ func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *CallFra callFrame.ToContractName = toContract.Name() callFrame.ToContractAbi = &toContract.CompiledContract().Abi t.resolveCallFrameConstructorArgs(callFrame, toContract) + + // If this is a contract creation, set the code address to the address of the contract we just deployed. + if callFrame.IsContractCreation() { + callFrame.CodeContractName = toContract.Name() + callFrame.CodeContractAbi = &toContract.CompiledContract().Abi + } } } } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index dde89e9c..5697d1cf 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -2,11 +2,9 @@ package fuzzing import ( "context" + "errors" "fmt" - "github.com/crytic/medusa/fuzzing/coverage" - "github.com/crytic/medusa/logging" - "github.com/crytic/medusa/logging/colors" - "github.com/rs/zerolog" + "github.com/crytic/medusa/fuzzing/executiontracer" "math/big" "math/rand" "os" @@ -18,6 +16,11 @@ import ( "sync" "time" + "github.com/crytic/medusa/fuzzing/coverage" + "github.com/crytic/medusa/logging" + "github.com/crytic/medusa/logging/colors" + "github.com/rs/zerolog" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/core/types" @@ -333,7 +336,7 @@ func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { // all compiled contract definitions. This includes any successful compilations as a result of the Fuzzer.config // definitions, as well as those added by Fuzzer.AddCompilationTargets. The contract deployment order is defined by // the Fuzzer.config. -func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) error { +func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) { // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, // we can infer the target contracts. Otherwise, we report an error. if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { @@ -341,7 +344,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { return fmt.Errorf("missing target contracts (update fuzzing.targetContracts in the project config " + - "or use the --target-contracts CLI flag)") + "or use the --target-contracts CLI flag)"), nil } } @@ -357,20 +360,20 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro if len(contract.CompiledContract().Abi.Constructor.Inputs) > 0 { jsonArgs, ok := fuzzer.config.Fuzzing.ConstructorArgs[contractName] if !ok { - return fmt.Errorf("constructor arguments for contract %s not provided", contractName) + return fmt.Errorf("constructor arguments for contract %s not provided", contractName), nil } decoded, err := valuegeneration.DecodeJSONArgumentsFromMap(contract.CompiledContract().Abi.Constructor.Inputs, jsonArgs, deployedContractAddr) if err != nil { - return err + return err, nil } args = decoded } - // Constructor our deployment message/tx data field + // Construct our deployment message/tx data field msgData, err := contract.CompiledContract().GetDeploymentMessageData(args) if err != nil { - return fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err) + return fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err), nil } // If our project config has a non-zero balance for this target contract, retrieve it @@ -387,25 +390,45 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro // Create a new pending block we'll commit to chain block, err := testChain.PendingBlockCreate() if err != nil { - return err + return err, nil } // Add our transaction to the block // Add our transaction to the block err = testChain.PendingBlockAddTx(msg.ToCoreMessage()) if err != nil { - return err + return err, nil } // Commit the pending block to the chain, so it becomes the new head. err = testChain.PendingBlockCommit() if err != nil { - return err + return err, nil } - // Ensure our transaction succeeded + // Ensure our transaction succeeded and, if it did not, attach an execution trace to it and re-run it. + // The execution trace will be returned so that it can be provided to the user for debugging if block.MessageResults[0].Receipt.Status != types.ReceiptStatusSuccessful { - return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err) + // Create a call sequence element to represent the failed contract deployment tx + cse := calls.NewCallSequenceElement(nil, msg, 0, 0) + cse.ChainReference = &calls.CallSequenceElementChainReference{ + Block: block, + TransactionIndex: len(block.Messages) - 1, + } + + // Replay the execution trace for the failed contract deployment tx + err = cse.AttachExecutionTrace(testChain, fuzzer.contractDefinitions) + + // Throw an error if execution tracing threw an error or the trace is nil + if err != nil { + return fmt.Errorf("failed to attach execution trace to failed contract deployment tx: %v", err), nil + } + if cse.ExecutionTrace == nil { + return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err), nil + } + + // Return the execution error and the execution trace + return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err), cse.ExecutionTrace } // Record our deployed contract so the next config-specified constructor args can reference this @@ -421,10 +444,10 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) erro // If we did not find a contract corresponding to this item in the deployment order, we throw an error. if !found { - return fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName) + return fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName), nil } } - return nil + return nil, nil } // defaultCallSequenceGeneratorConfigFunc is a NewCallSequenceGeneratorConfigFunc which creates a @@ -641,9 +664,13 @@ 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) + err, trace := f.Hooks.ChainSetupFunc(f, baseTestChain) if err != nil { - f.logger.Error("Failed to initialize the test chain", err) + if trace != nil { + f.logger.Error("Failed to initialize the test chain", err, errors.New(trace.Log().ColorString())) + } else { + f.logger.Error("Failed to initialize the test chain", err) + } return err } @@ -825,7 +852,7 @@ func (f *Fuzzer) printExitingResults() { // Print the results of each individual test case. f.logger.Info("Fuzzer stopped, test results follow below ...") for _, testCase := range f.testCases { - f.logger.Info(testCase.LogMessage().Elements()...) + f.logger.Info(testCase.LogMessage().ColorString()) // Tally our pass/fail count. if testCase.Status() == TestCaseStatusPassed { diff --git a/fuzzing/fuzzer_hooks.go b/fuzzing/fuzzer_hooks.go index ea2d8486..cf458192 100644 --- a/fuzzing/fuzzer_hooks.go +++ b/fuzzing/fuzzer_hooks.go @@ -1,6 +1,7 @@ package fuzzing import ( + "github.com/crytic/medusa/fuzzing/executiontracer" "math/rand" "github.com/crytic/medusa/chain" @@ -41,7 +42,8 @@ type NewShrinkingValueMutatorFunc func(fuzzer *Fuzzer, valueSet *valuegeneration type NewCallSequenceGeneratorConfigFunc func(fuzzer *Fuzzer, valueSet *valuegeneration.ValueSet, randomProvider *rand.Rand) (*CallSequenceGeneratorConfig, error) // TestChainSetupFunc describes a function which sets up a test chain's initial state prior to fuzzing. -type TestChainSetupFunc func(fuzzer *Fuzzer, testChain *chain.TestChain) error +// An execution trace can also be returned in case of a deployment error for an improved debugging experience +type TestChainSetupFunc func(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) // CallSequenceTestFunc defines a method called after a fuzzing.FuzzerWorker sends another call in a types.CallSequence // during a fuzzing campaign. It returns a ShrinkCallSequenceRequest set, which represents a set of requests for diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index d0a47392..860ebd93 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -2,6 +2,7 @@ package fuzzing import ( "encoding/hex" + "github.com/crytic/medusa/fuzzing/executiontracer" "math/big" "math/rand" "testing" @@ -35,7 +36,7 @@ func TestFuzzerHooks(t *testing.T) { return existingSeqGenConfigFunc(fuzzer, valueSet, randomProvider) } existingChainSetupFunc := f.fuzzer.Hooks.ChainSetupFunc - f.fuzzer.Hooks.ChainSetupFunc = func(fuzzer *Fuzzer, testChain *chain.TestChain) error { + f.fuzzer.Hooks.ChainSetupFunc = func(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) { chainSetupOk = true return existingChainSetupFunc(fuzzer, testChain) } diff --git a/logging/log_buffer.go b/logging/log_buffer.go index 70abf4a4..761d7ab5 100644 --- a/logging/log_buffer.go +++ b/logging/log_buffer.go @@ -28,6 +28,12 @@ func (l *LogBuffer) Elements() []any { // String provides the non-colorized string representation of the LogBuffer func (l LogBuffer) String() string { - _, msg, _, _ := buildMsgs(l.elements) + _, msg, _, _ := buildMsgs(l.elements...) + return msg +} + +// ColorString provides the colorized string representation of the LogBuffer +func (l LogBuffer) ColorString() string { + msg, _, _, _ := buildMsgs(l.elements...) return msg } diff --git a/logging/logger.go b/logging/logger.go index 7a6c09e4..e52f4414 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -343,22 +343,6 @@ func chainStructuredLogInfoErrorsAndMsgs(structuredLog *zerolog.Event, unstructu // First, we need to create a formatted error string for unstructured output var errStr string for _, err := range errs { - // To make the formatting a little nicer, we will add a tab after each new line in the error so that - // errors can be better differentiated on unstructured channels - lines := make([]string, 0) - for i, line := range strings.Split(err.Error(), "\n") { - // Add a tab to the line only after the first new line in the error message - if i != 0 { - line = "\t" + line - } - lines = append(lines, line) - } - - // Update the error string to be based on the tabbed lines array - if len(lines) > 0 { - err = fmt.Errorf("%v", strings.Join(lines, "\n")) - } - // Append a bullet point and the formatted error to the error string errStr += "\n" + colors.BULLET_POINT + " " + err.Error() } From 2604d819acbdff815d478c633423da05d6bfbdcf Mon Sep 17 00:00:00 2001 From: David Pokora Date: Wed, 27 Mar 2024 14:11:22 -0700 Subject: [PATCH 055/109] Output non-fuzzing failure related error messages (#312) * Output error message if there was one * Slight cleanup/refactor * Updated exit code and error return so error messages are only printed if there is an error (despite exiting with a special exit code) * proposed changed to prevent double logging/printing of fuzzer errors * change name of exit code * fix bug --------- Co-authored-by: anishnaik --- cmd/exitcodes/error_with_exit_code.go | 18 +++++++++++------- cmd/exitcodes/exit_codes.go | 4 ++++ cmd/fuzz.go | 17 ++++++++++------- main.go | 16 ++++++++++++++-- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/cmd/exitcodes/error_with_exit_code.go b/cmd/exitcodes/error_with_exit_code.go index eb2367a7..96102f7f 100644 --- a/cmd/exitcodes/error_with_exit_code.go +++ b/cmd/exitcodes/error_with_exit_code.go @@ -17,21 +17,25 @@ func NewErrorWithExitCode(err error, exitCode int) *ErrorWithExitCode { // Error returns the error message string, implementing the `error` interface. func (e *ErrorWithExitCode) Error() string { + if e.err == nil { + return "" + } return e.err.Error() } -// GetErrorExitCode checks the given exit code that the application should exit with, if this error is bubbled to -// the top-level. This will be 0 for a nil error, 1 for a generic error, or arbitrary if the error is of type +// GetInnerErrorAndExitCode checks the given exit code that the application should exit with, if this error is bubbled +// to the top-level. This will be 0 for a nil error, 1 for a generic error, or arbitrary if the error is of type // ErrorWithExitCode. -// Returns the exit code associated with the error. -func GetErrorExitCode(err error) int { +// Returns the error (or inner error if it is an ErrorWithExitCode error type), along with the exit code associated +// with the error. +func GetInnerErrorAndExitCode(err error) (error, int) { // If we have no error, return 0, if we have a generic error, return 1, if we have a custom error code, unwrap // and return it. if err == nil { - return ExitCodeSuccess + return nil, ExitCodeSuccess } else if unwrappedErr, ok := err.(*ErrorWithExitCode); ok { - return unwrappedErr.exitCode + return unwrappedErr.err, unwrappedErr.exitCode } else { - return ExitCodeGeneralError + return err, ExitCodeGeneralError } } diff --git a/cmd/exitcodes/exit_codes.go b/cmd/exitcodes/exit_codes.go index cb6c7c98..5bbed621 100644 --- a/cmd/exitcodes/exit_codes.go +++ b/cmd/exitcodes/exit_codes.go @@ -16,6 +16,10 @@ const ( // ================================ // Note: Despite not being standardized, exit codes 2-5 are often used for common use cases, so we avoid them. + // ExitCodeHandledError indicates that there was an error that was logged already and does not need to be handled + // by main. + ExitCodeHandledError = 6 + // ExitCodeTestFailed indicates a test case had failed. ExitCodeTestFailed = 7 ) diff --git a/cmd/fuzz.go b/cmd/fuzz.go index 28453ae8..188f5d95 100644 --- a/cmd/fuzz.go +++ b/cmd/fuzz.go @@ -145,9 +145,9 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { } // Create our fuzzing - fuzzer, err := fuzzing.NewFuzzer(*projectConfig) - if err != nil { - return err + fuzzer, fuzzErr := fuzzing.NewFuzzer(*projectConfig) + if fuzzErr != nil { + return exitcodes.NewErrorWithExitCode(fuzzErr, exitcodes.ExitCodeHandledError) } // Stop our fuzzing on keyboard interrupts @@ -159,12 +159,15 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { }() // Start the fuzzing process with our cancellable context. - err = fuzzer.Start() + fuzzErr = fuzzer.Start() + if fuzzErr != nil { + return exitcodes.NewErrorWithExitCode(fuzzErr, exitcodes.ExitCodeHandledError) + } // If we have no error and failed test cases, we'll want to return a special exit code - if err == nil && len(fuzzer.TestCasesWithStatus(fuzzing.TestCaseStatusFailed)) > 0 { - return exitcodes.NewErrorWithExitCode(err, exitcodes.ExitCodeTestFailed) + if fuzzErr == nil && len(fuzzer.TestCasesWithStatus(fuzzing.TestCaseStatusFailed)) > 0 { + return exitcodes.NewErrorWithExitCode(fuzzErr, exitcodes.ExitCodeTestFailed) } - return err + return fuzzErr } diff --git a/main.go b/main.go index ad2537e1..b0bb8eaf 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "github.com/crytic/medusa/cmd" "github.com/crytic/medusa/cmd/exitcodes" "os" @@ -10,6 +11,17 @@ func main() { // Run our root CLI command, which contains all underlying command logic and will handle parsing/invocation. err := cmd.Execute() - // Determine the exit code from any potential error and exit out. - os.Exit(exitcodes.GetErrorExitCode(err)) + // Obtain the actual error and exit code from the error, if any. + var exitCode int + err, exitCode = exitcodes.GetInnerErrorAndExitCode(err) + + // If we have an error, print it. + if err != nil && exitCode != exitcodes.ExitCodeHandledError { + fmt.Println(err) + } + + // If we have a non-success exit code, exit with it. + if exitCode != exitcodes.ExitCodeSuccess { + os.Exit(exitCode) + } } From c1c065d46d0348415d4d09edbc7d8c96784ffee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:53:07 -0300 Subject: [PATCH 056/109] Update README to reflect current CLI and config options (#343) Closes #341 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c4597293..90ce585b 100644 --- a/README.md +++ b/README.md @@ -45,20 +45,20 @@ You can then fetch the latest binaries for your platform from our [GitHub Releas Although we recommend users run `medusa` in a configuration file driven format for more customizability, you can also run `medusa` through the CLI directly. We provide instructions for both below. -We recommend you familiarize yourself with writing [assertion](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/basic/assertion-checking.md) and [property](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/introduction/how-to-test-a-property.md) tests for Echidna. `medusa` supports Echidna-like property testing with config-defined function prefixes (default: `fuzz_`) and assertion testing using Solidity `assert(...)` statements. +We recommend you familiarize yourself with writing [assertion](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/basic/assertion-checking.md) and [property](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/introduction/how-to-test-a-property.md) tests for Echidna. `medusa` supports Echidna-like property testing and function optimization with config-defined function prefixes (default: `property_` and `optimize_`, respectively) and assertion testing using Solidity `assert(...)` statements. ### Command-line only You can use the following command to run `medusa` against a contract: ```console -medusa fuzz --target contract.sol --deployment-order ContractName +medusa fuzz --compilation-target contract.sol --target-contracts ContractName ``` Where: -- `--target` specifies the path `crytic-compile` should use to compile contracts -- `--deployment-order` specifies comma-separated names of contracts to be deployed for testing. +- `--compilation-target` specifies the path `crytic-compile` should use to compile contracts +- `--target-contracts` specifies comma-separated names of contracts to be deployed for testing. **Note:** Check out the [command-line interface](https://github.com/crytic/medusa/wiki/Command-Line-Interface) wiki page, or run `medusa --help` for more information. @@ -74,7 +74,7 @@ medusa init This will create a `medusa.json` in your current folder. There are two required fields that should be set correctly: - Set your `"target"` under `"compilation"` to point to the file/directory which `crytic-compile` should use to build your contracts. -- Put the names of any contracts you wish to deploy and run tests against in the `"deploymentOrder"` field. This must be non-empty. +- Put the names of any contracts you wish to deploy and run tests against in the `"targetContracts"` field. This must be non-empty. After you have a configuration in place, you can execute: From a0801b22104090fd2bda5c3e7e591af878f3791b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:50:06 -0400 Subject: [PATCH 057/109] Bump golang.org/x/crypto from 0.19.0 to 0.22.0 (#345) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.19.0 to 0.22.0. - [Commits](https://github.com/golang/crypto/compare/v0.19.0...v0.22.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 27b9f68c..b4eccdd5 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.19.0 + golang.org/x/crypto v0.22.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/net v0.21.0 - golang.org/x/sys v0.17.0 + golang.org/x/sys v0.19.0 ) require ( diff --git a/go.sum b/go.sum index 4c98ad4e..2ec373b5 100644 --- a/go.sum +++ b/go.sum @@ -321,8 +321,8 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= @@ -398,8 +398,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= From f18e3f930d2fd315210582c9a17c70573144e116 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:18:57 -0400 Subject: [PATCH 058/109] Bump google.golang.org/protobuf from 1.28.1 to 1.33.0 (#332) Bumps google.golang.org/protobuf from 1.28.1 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b4eccdd5..bfae4653 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2ec373b5..82679dec 100644 --- a/go.sum +++ b/go.sum @@ -452,8 +452,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 759b6297255f7a8b438f1a950b35739bbd7e9e6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:43:33 -0400 Subject: [PATCH 059/109] Bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#314) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bfae4653..16fa2f1b 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.22.0 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/net v0.21.0 diff --git a/go.sum b/go.sum index 82679dec..ff4eb54b 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= From da2bcb658bd04053789448726227c0a5d603373a Mon Sep 17 00:00:00 2001 From: Igor Konnov Date: Wed, 10 Apr 2024 18:23:29 +0200 Subject: [PATCH 060/109] fix two outdated comments (#347) --- fuzzing/fuzzer_worker_sequence_generator.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index 8cfe21e2..a931b04a 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -2,11 +2,12 @@ package fuzzing import ( "fmt" + "math/big" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/crytic/medusa/utils/randomutils" - "math/big" ) // CallSequenceGenerator generates call sequences iteratively per element, for use in fuzzing campaigns. It is attached @@ -336,10 +337,10 @@ func callSeqGenFuncCorpusHead(sequenceGenerator *CallSequenceGenerator, sequence // Obtain a call sequence from the corpus corpusSequence, err := sequenceGenerator.worker.fuzzer.corpus.RandomMutationTargetSequence() if err != nil { - return fmt.Errorf("could not obtain corpus call sequence for tail mutation: %v", err) + return fmt.Errorf("could not obtain corpus call sequence for head mutation: %v", err) } - // Determine a random position to slice the call sequence. + // Determine the length of the slice to be copied in the head. maxLength := utils.Min(len(sequence), len(corpusSequence)) copy(sequence, corpusSequence[:maxLength]) From 921a58f1f5155365ab6fd5c5a851d6a2595d05ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:43:22 -0300 Subject: [PATCH 061/109] ci: automated release builds (#342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add M1 mac arch build and test * Update .github/workflows/ci.yml Co-authored-by: Emilio López <2642849+elopez@users.noreply.github.com> * Update .github/workflows/ci.yml Co-authored-by: Emilio López <2642849+elopez@users.noreply.github.com> * ci: automate release creation when pushing a tag * ci: upgrade actions/setup-{node,go} * ci: fix Python dependency installation on macOS error: externally-managed-environment × This environment is externally managed ╰─> To install Python packages system-wide, try brew install xyz, where xyz is the package you are trying to install. If you wish to install a non-brew-packaged Python package, create a virtual environment using python3 -m venv path/to/venv. Then use path/to/venv/bin/python and path/to/venv/bin/pip. If you wish to install a non-brew packaged Python application, it may be easiest to use pipx install xyz, which will manage a virtual environment for you. Make sure you have pipx installed. note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages. hint: See PEP 668 for the detailed specification. * ci: use `alls-green` to decide success status `release` can be skipped, but GitHub will then skip `all-checks`. We need to check that `release` is successful when it runs, but ignore it when it is skipped. --------- Co-authored-by: Anish Naik --- .github/workflows/ci.yml | 90 +++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5c97e7d..ea20f899 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,8 @@ on: push: branches: - master + tags: + - "v*" pull_request: branches: - master @@ -22,7 +24,10 @@ jobs: needs: [lint, test] strategy: matrix: - environment: [ubuntu-latest, macos-latest, windows-latest] + environment: [ubuntu-latest, macos-12, macos-14, windows-latest] + permissions: + contents: read + id-token: write runs-on: ${{ matrix.environment }} timeout-minutes: 10 @@ -44,9 +49,11 @@ jobs: printf 'TEMP=%s\\tmpdir\n' "$DIR" | tee -a "$GITHUB_ENV" go env - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "^1.18.1" + # disable caching during release (tag) builds + cache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Build (Linux and macOS) if: runner.os == 'Linux' || runner.os == 'macOS' @@ -54,7 +61,7 @@ jobs: - name: Compress (Linux and macOS) if: runner.os == 'Linux' || runner.os == 'macOS' - run: tar -czvf medusa.tar.gz medusa + run: tar -czvf medusa-${{ runner.os }}-${{ runner.arch }}.tar.gz medusa - name: Build (Windows) if: runner.os == 'Windows' @@ -62,14 +69,56 @@ jobs: - name: Compress (Windows) if: runner.os == 'Windows' - run: tar -czvf medusa.tar.gz medusa.exe + run: tar -czvf medusa-${{ runner.os }}-${{ runner.arch }}.tar.gz medusa.exe - - name: Upload artifact on merge to master - if: github.ref == 'refs/heads/master' - uses: actions/upload-artifact@v3 + - name: Rename for release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + shell: bash + run: | + [ ! -f medusa-Linux-X64.tar.gz ] || mv medusa-Linux-X64.tar.gz medusa-linux-x64.tar.gz + [ ! -f medusa-macOS-X64.tar.gz ] || mv medusa-macOS-X64.tar.gz medusa-mac-x64.tar.gz + [ ! -f medusa-macOS-ARM64.tar.gz ] || mv medusa-macOS-ARM64.tar.gz medusa-mac-arm64.tar.gz + [ ! -f medusa-Windows-X64.tar.gz ] || mv medusa-Windows-X64.tar.gz medusa-win-x64.tar.gz + + - name: Sign artifact + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + uses: sigstore/gh-action-sigstore-python@v2.1.1 + with: + inputs: ./medusa-*.tar.gz + + - name: Upload artifact + if: github.ref == 'refs/heads/master' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + uses: actions/upload-artifact@v4 with: - name: medusa-${{ runner.os }} - path: medusa.tar.gz + name: medusa-${{ runner.os }}-${{ runner.arch }} + path: | + ./medusa-*.tar.gz + ./medusa-*.tar.gz.sigstore + + release: + needs: [build] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Download binaries + uses: actions/download-artifact@v4 + with: + pattern: medusa-* + merge-multiple: true + + - name: Create GitHub release and upload binaries + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v2.0.4 + with: + draft: true + name: "${{ github.ref_name }}" + files: | + ./medusa-*.tar.gz + ./medusa-*.tar.gz.sigstore lint: runs-on: ubuntu-latest @@ -78,7 +127,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "^1.18.1" @@ -110,7 +159,7 @@ jobs: test: strategy: matrix: - environment: [ubuntu-latest, macos-latest, windows-latest] + environment: [ubuntu-latest, macos-12, macos-14, windows-latest] runs-on: ${{ matrix.environment }} timeout-minutes: 20 @@ -118,6 +167,10 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Speed up Go, Python, Node (Windows) if: runner.os == 'Windows' run: | @@ -142,11 +195,11 @@ jobs: npm config set cache "$DIR\\npm-cache" --global echo "::endgroup::" - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: "^1.18.1" - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18.15 @@ -155,7 +208,7 @@ jobs: - name: Install Python dependencies run: | - pip3 install --no-cache-dir setuptools solc-select crytic-compile + pip3 install --no-cache-dir solc-select crytic-compile - name: Install solc run: | @@ -165,9 +218,14 @@ jobs: run: go test ./... all-checks: - needs: [lint, test, build] + if: always() + needs: [lint, test, build, release] runs-on: ubuntu-latest steps: - - run: true + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + allowed-skips: release + jobs: ${{ toJSON(needs) }} From 6750032502ed64952435dc408be3d8a1a107eb5c Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 2 May 2024 21:29:20 -0400 Subject: [PATCH 062/109] `mdbook` for medusa (#348) * Migrate to mdbook (#223) Co-authored-by: Zach McManus <59886732+zachmdsi@users.noreply.github.com> Co-authored-by: Damilola Edwards --- .gitignore | 5 +- README.md | 94 +------- docs/book.toml | 17 ++ docs/src/README.md | 18 ++ docs/src/SUMMARY.md | 69 ++++++ docs/src/advanced.md | 16 ++ docs/src/api/api_overview.md | 185 ++++++++++++++++ docs/src/cheatcodes/addr.md | 24 ++ docs/src/cheatcodes/chain_id.md | 22 ++ docs/src/cheatcodes/cheatcodes_overview.md | 118 ++++++++++ docs/src/cheatcodes/coinbase.md | 22 ++ docs/src/cheatcodes/deal.md | 23 ++ docs/src/cheatcodes/difficulty.md | 25 +++ docs/src/cheatcodes/etch.md | 29 +++ docs/src/cheatcodes/fee.md | 22 ++ docs/src/cheatcodes/ffi.md | 58 +++++ docs/src/cheatcodes/get_nonce.md | 22 ++ docs/src/cheatcodes/load.md | 27 +++ docs/src/cheatcodes/parse_address.md | 30 +++ docs/src/cheatcodes/parse_bool.md | 30 +++ docs/src/cheatcodes/parse_bytes.md | 30 +++ docs/src/cheatcodes/parse_bytes32.md | 30 +++ docs/src/cheatcodes/parse_int.md | 30 +++ docs/src/cheatcodes/parse_uint.md | 30 +++ docs/src/cheatcodes/prank.md | 38 ++++ docs/src/cheatcodes/prank_here.md | 49 +++++ docs/src/cheatcodes/roll.md | 24 ++ docs/src/cheatcodes/set_nonce.md | 24 ++ docs/src/cheatcodes/sign.md | 28 +++ docs/src/cheatcodes/snapshot.md | 68 ++++++ docs/src/cheatcodes/store.md | 27 +++ docs/src/cheatcodes/to_string.md | 84 +++++++ docs/src/cheatcodes/warp.md | 24 ++ docs/src/cli/completion.md | 21 ++ docs/src/cli/fuzz.md | 131 +++++++++++ docs/src/cli/init.md | 36 +++ docs/src/cli/overview.md | 10 + docs/src/console_logging.md | 55 +++++ docs/src/coverage_reports.md | 3 + docs/src/faq.md | 16 ++ docs/src/getting_started/first_steps.md | 36 +++ docs/src/getting_started/installation.md | 64 ++++++ .../src/project_configuration/chain_config.md | 25 +++ .../compilation_config.md | 59 +++++ .../project_configuration/fuzzing_config.md | 205 ++++++++++++++++++ .../project_configuration/logging_config.md | 25 +++ docs/src/project_configuration/overview.md | 49 +++++ .../project_configuration/testing_config.md | 178 +++++++++++++++ docs/src/static/contract_deployment.png | Bin 0 -> 7790 bytes docs/src/static/coverage.png | Bin 0 -> 36174 bytes docs/src/static/custom.css | 6 + .../static/function_level_testing_medusa.json | 72 ++++++ docs/src/static/medusa.json | 72 ++++++ docs/src/static/medusa_logo.png | Bin 0 -> 110398 bytes docs/src/testing/coverage_reports.md | 1 + docs/src/testing/fuzzing_lifecycle.md | 136 ++++++++++++ docs/src/testing/invariants.md | 69 ++++++ docs/src/testing/overview.md | 26 +++ docs/src/testing/tips.md | 32 +++ .../writing-function-level-invariants.md | 145 +++++++++++++ .../writing-system-level-invariants.md | 3 + docs/src/testing/writing-tests.md | 189 ++++++++++++++++ docs/theme/favicon.png | Bin 0 -> 121411 bytes docs/theme/favicon.svg | 129 +++++++++++ docs/theme/highlight.js | 6 + fuzzing/executiontracer/execution_trace.go | 5 +- fuzzing/valuegeneration/abi_values.go | 2 +- 67 files changed, 3057 insertions(+), 91 deletions(-) create mode 100644 docs/book.toml create mode 100644 docs/src/README.md create mode 100644 docs/src/SUMMARY.md create mode 100644 docs/src/advanced.md create mode 100644 docs/src/api/api_overview.md create mode 100644 docs/src/cheatcodes/addr.md create mode 100644 docs/src/cheatcodes/chain_id.md create mode 100644 docs/src/cheatcodes/cheatcodes_overview.md create mode 100644 docs/src/cheatcodes/coinbase.md create mode 100644 docs/src/cheatcodes/deal.md create mode 100644 docs/src/cheatcodes/difficulty.md create mode 100644 docs/src/cheatcodes/etch.md create mode 100644 docs/src/cheatcodes/fee.md create mode 100644 docs/src/cheatcodes/ffi.md create mode 100644 docs/src/cheatcodes/get_nonce.md create mode 100644 docs/src/cheatcodes/load.md create mode 100644 docs/src/cheatcodes/parse_address.md create mode 100644 docs/src/cheatcodes/parse_bool.md create mode 100644 docs/src/cheatcodes/parse_bytes.md create mode 100644 docs/src/cheatcodes/parse_bytes32.md create mode 100644 docs/src/cheatcodes/parse_int.md create mode 100644 docs/src/cheatcodes/parse_uint.md create mode 100644 docs/src/cheatcodes/prank.md create mode 100644 docs/src/cheatcodes/prank_here.md create mode 100644 docs/src/cheatcodes/roll.md create mode 100644 docs/src/cheatcodes/set_nonce.md create mode 100644 docs/src/cheatcodes/sign.md create mode 100644 docs/src/cheatcodes/snapshot.md create mode 100644 docs/src/cheatcodes/store.md create mode 100644 docs/src/cheatcodes/to_string.md create mode 100644 docs/src/cheatcodes/warp.md create mode 100644 docs/src/cli/completion.md create mode 100644 docs/src/cli/fuzz.md create mode 100644 docs/src/cli/init.md create mode 100644 docs/src/cli/overview.md create mode 100644 docs/src/console_logging.md create mode 100644 docs/src/coverage_reports.md create mode 100644 docs/src/faq.md create mode 100644 docs/src/getting_started/first_steps.md create mode 100644 docs/src/getting_started/installation.md create mode 100644 docs/src/project_configuration/chain_config.md create mode 100644 docs/src/project_configuration/compilation_config.md create mode 100644 docs/src/project_configuration/fuzzing_config.md create mode 100644 docs/src/project_configuration/logging_config.md create mode 100644 docs/src/project_configuration/overview.md create mode 100644 docs/src/project_configuration/testing_config.md create mode 100644 docs/src/static/contract_deployment.png create mode 100644 docs/src/static/coverage.png create mode 100644 docs/src/static/custom.css create mode 100644 docs/src/static/function_level_testing_medusa.json create mode 100644 docs/src/static/medusa.json create mode 100755 docs/src/static/medusa_logo.png create mode 100644 docs/src/testing/coverage_reports.md create mode 100644 docs/src/testing/fuzzing_lifecycle.md create mode 100644 docs/src/testing/invariants.md create mode 100644 docs/src/testing/overview.md create mode 100644 docs/src/testing/tips.md create mode 100644 docs/src/testing/writing-function-level-invariants.md create mode 100644 docs/src/testing/writing-system-level-invariants.md create mode 100644 docs/src/testing/writing-tests.md create mode 100755 docs/theme/favicon.png create mode 100755 docs/theme/favicon.svg create mode 100644 docs/theme/highlight.js diff --git a/.gitignore b/.gitignore index 051bf3b0..b1251402 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,7 @@ *node_modules/ # Medusa binary -medusa \ No newline at end of file +medusa + +# Medusa docs +docs/book diff --git a/README.md b/README.md index 90ce585b..18f154b5 100644 --- a/README.md +++ b/README.md @@ -17,98 +17,18 @@ It provides parallelized fuzz testing of smart contracts through CLI, or its Go - ✔️**Extensible low-level testing API** through events and hooks provided throughout the fuzzer, workers, and test chains. - ❌ **Extensible high-level testing API** allowing for the addition of per-contract or global post call/event property tests with minimal effort. -## Installation +## Documentation -### Precompiled binaries +To learn more about how to install and use `medusa`, please refer to our [documentation](./docs/src/SUMMARY.md). -To use `medusa`, ensure you have: +For a better viewing experience, we recommend you install [mdbook](https://rust-lang.github.io/mdBook/guide/installation.html) +and then running the following steps from medusa's source directory: -- [crytic-compile](https://github.com/crytic/crytic-compile) (`pip3 install crytic-compile`) -- a suitable compilation framework (e.g. `solc`, `hardhat`) installed on your machine. We recommend [solc-select](https://github.com/crytic/solc-select) to quickly switch between Solidity compiler versions. - -You can then fetch the latest binaries for your platform from our [GitHub Releases](https://github.com/crytic/medusa/releases) page. - -### Building from source - -#### Requirements - -- You must have at least go 1.18 installed. -- [Windows only] The `go-ethereum` dependency may require [TDM-GCC](https://jmeubank.github.io/tdm-gcc/) to build. - -#### Steps - -- Clone the repository, then execute `go build` in the repository root. -- Go will automatically fetch all dependencies and build a binary for you in the same folder when completed. - -## Usage - -Although we recommend users run `medusa` in a configuration file driven format for more customizability, you can also run `medusa` through the CLI directly. -We provide instructions for both below. - -We recommend you familiarize yourself with writing [assertion](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/basic/assertion-checking.md) and [property](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/introduction/how-to-test-a-property.md) tests for Echidna. `medusa` supports Echidna-like property testing and function optimization with config-defined function prefixes (default: `property_` and `optimize_`, respectively) and assertion testing using Solidity `assert(...)` statements. - -### Command-line only - -You can use the following command to run `medusa` against a contract: - -```console -medusa fuzz --compilation-target contract.sol --target-contracts ContractName +```bash +cd docs +mdbook serve ``` -Where: - -- `--compilation-target` specifies the path `crytic-compile` should use to compile contracts -- `--target-contracts` specifies comma-separated names of contracts to be deployed for testing. - -**Note:** Check out the [command-line interface](https://github.com/crytic/medusa/wiki/Command-Line-Interface) wiki page, or run `medusa --help` for more information. - -### Configuration file driven - -The preferred method to use medusa is to enter your project directory (hardhat directory, or directory with your contracts), -then execute the following command: - -```console -medusa init -``` - -This will create a `medusa.json` in your current folder. There are two required fields that should be set correctly: - -- Set your `"target"` under `"compilation"` to point to the file/directory which `crytic-compile` should use to build your contracts. -- Put the names of any contracts you wish to deploy and run tests against in the `"targetContracts"` field. This must be non-empty. - -After you have a configuration in place, you can execute: - -```console -medusa fuzz -``` - -This will use the `medusa.json` configuration in the current directory and begin the fuzzing campaign. - -**Note:** Check out the [project configuration](https://github.com/crytic/medusa/wiki/Project-Configuration) wiki page, or run `medusa --help` for more information. - -## Running Unit Tests - -First, install [crytic-compile](https://github.com/crytic/crytic-compile), [solc-select](https://github.com/crytic/solc-select), and ensure you have `solc` (version >=0.8.7), and `hardhat` available on your system. - -- From the root of the repository, invoke `go test -v ./...` on through command-line to run tests from all packages at or below the root. - - Or enter each package directory to run `go test -v .` to test the immediate package. - - Note: the `-v` parameter provides verbose output. -- Otherwise, use an IDE like [GoLand](https://www.jetbrains.com/go/) to visualize the tests and logically separate output. - -## FAQs - -**Why create `medusa` if Echidna is already working just fine?** - -With `medusa`, we are exploring a different EVM implementation and language for our smart contract fuzzer. We believe that -experimenting with a new fuzzer provides us with the following benefits: - -- Since `medusa` is written in Go, we believe that this will **lower the barrier of entry for external contributions**. - We have taken great care in thoroughly commenting our code so that it is easy for new contributors to get up-to-speed and start contributing! -- The use of Go allows us to build an API to hook into the various parts of the fuzzer to build custom testing methodologies. See the [API Overview (WIP)]() section in the Wiki for more details. -- Our forked version of go-ethereum, [`medusa-geth`](https://github.com/crytic/medusa-geth), exhibits behavior that is closer to that of the EVM in production environments. -- We can take the lessons we learned while developing Echidna to create a fuzzer that is just as feature-rich but with additional capabilities to - create powerful and unique testing methodologies. - ## Contributing For information about how to contribute to this project, check out the [CONTRIBUTING](./CONTRIBUTING.md) guidelines. diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 00000000..c09f7b92 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,17 @@ +[book] +title = "medusa" +authors = ["Trail of Bits"] +language = "en" +multilingual = false +src = "src" +description = "This repository, brought to you by Trail of Bits, contains the documentation files for the medusa fuzzer." + +[output.html] +git-repository-url = "https://github.com/crytic/medusa" +edit-url-template = "https://github.com/crytic/medusa/edit/master/docs/{path}" +additional-css = ["src/static/custom.css"] +default-theme = "light" + +[output.html.fold] +enable = true +level = 1 \ No newline at end of file diff --git a/docs/src/README.md b/docs/src/README.md new file mode 100644 index 00000000..e9ceb477 --- /dev/null +++ b/docs/src/README.md @@ -0,0 +1,18 @@ +![medusa_logo](./static/medusa_logo.png) + +`medusa` is a cross-platform go-ethereum-based smart contract fuzzer inspired by Echidna. It provides parallelized fuzz +testing of smart contracts through CLI, or its Go API that allows custom user-extended testing methodology. + +## Table of Contents + +- [Getting Started](./getting_started/installation.md): Learn how to install `medusa` and how to set it up for your first project. +- [Project Configuration](./project_configuration/overview.md): Learn how to set up `medusa` for your project as well as + the vast number of configuration options that can be set up based on your project needs. +- [Command Line Interface](./cli/overview.md): Learn how to use `medusa`'s CLI. +- [Writing Tests](./testing/overview.md): Learn how to write tests with `medusa` +- [API (WIP)](./api/api_overview.md): Learn about `medusa`'s Go API that can be used to perform advanced testing + methodologies and extend `medusa`'s capabilities. +- Appendices + - [Cheatcodes](./cheatcodes/cheatcodes_overview.md): Learn about the various cheatcodes that are supported by `medusa`. + - [Console Logging](./console_logging.md): Learn about how to use `console.log` with `medusa`. + - [FAQ](./faq.md) diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 00000000..383a71ef --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,69 @@ +# Summary + +[Introduction](./README.md) + +# Getting Started + +- [Installation](./getting_started/installation.md) +- [First Steps](./getting_started/first_steps.md) + +# Project Configuration + +- [Configuration Overview](project_configuration/overview.md) +- [Fuzzing Configuration](project_configuration/fuzzing_config.md) +- [Testing Configuration](project_configuration/testing_config.md) +- [Chain Configuration](project_configuration/chain_config.md) +- [Compilation Configuration](project_configuration/compilation_config.md) +- [Logging Configuration](project_configuration/logging_config.md) + +# Command Line Interface (CLI) + +- [CLI Overview](./cli/overview.md) +- [init](./cli/init.md) +- [fuzz](./cli/fuzz.md) +- [completion](./cli/completion.md) + +# Writing Tests + +- [Testing Overview](./testing/overview.md) +- [The Fuzzing Lifecycle](./testing/fuzzing_lifecycle.md) +- [Types of Invariants](./testing/invariants.md) +- [Writing Function-Level Invariants](./testing/writing-function-level-invariants.md) +- [Writing System-Level Invariants (WIP)](./testing/writing-system-level-invariants.md) +- [Coverage Reports (WIP)](./testing/coverage_reports.md) + +# API + +- [API Overview (WIP)](api/api_overview.md) + +# Appendices + +- [Cheatcodes](cheatcodes/cheatcodes_overview.md) + - [warp](./cheatcodes/warp.md) + - [roll](./cheatcodes/roll.md) + - [fee](./cheatcodes/fee.md) + - [difficulty](./cheatcodes/difficulty.md) + - [chainId](./cheatcodes/chain_id.md) + - [store](./cheatcodes/store.md) + - [load](./cheatcodes/load.md) + - [etch](./cheatcodes/etch.md) + - [deal](./cheatcodes/deal.md) + - [snapshot](./cheatcodes/snapshot.md) + - [getNonce](./cheatcodes/get_nonce.md) + - [setNonce](./cheatcodes/set_nonce.md) + - [coinbase](./cheatcodes/coinbase.md) + - [prank](./cheatcodes/prank.md) + - [prankHere](./cheatcodes/prank_here.md) + - [ffi](./cheatcodes/ffi.md) + - [addr](./cheatcodes/addr.md) + - [sign](./cheatcodes/sign.md) + - [toString](./cheatcodes/to_string.md) + - [parseBytes](./cheatcodes/parse_bytes.md) + - [parseBytes32](./cheatcodes/parse_bytes32.md) + - [parseInt](./cheatcodes/parse_int.md) + - [parseUint](./cheatcodes/parse_uint.md) + - [parseBool](./cheatcodes/parse_bool.md) + - [parseAddress](./cheatcodes/parse_address.md) +- [Console Logging](./console_logging.md) + +[FAQ](./faq.md) diff --git a/docs/src/advanced.md b/docs/src/advanced.md new file mode 100644 index 00000000..a4a1f87f --- /dev/null +++ b/docs/src/advanced.md @@ -0,0 +1,16 @@ +> **Definition**: Stateful fuzzing is the process of maintaining EVM state across multiple fuzzed transactions. + +Stateful fuzzing is an incredibly powerful feature because it allows medusa to test your system **end-to-end**. Let's +take, for example, a staking system where you have the ability to `deposit`, `stake`, `unstake`, and `withdraw`. Because +medusa can execute an array of transactions, medusa can call [`deposit`, `stake`, `unstake`, `withdraw`] inorder and test the +whole system in one fell swoop. It is very important to note that medusa was not _forced_ to call those functions in +sequence. Medusa, over time, will identify that calling deposit allows it to stake tokens and having a staked balance +allows it to unstake, and so on. + +In contrast, having a call sequence length of 1 is called **stateless fuzzing**. + +> **Definition**: Stateless fuzzing is the process of executing a single transaction before resetting the EVM state. + +Stateless fuzzing is useful for arithmetic libraries or isolated functions where state does not need to be maintained +across transactions. Stateless fuzzing, although faster, is not useful for larger systems that have many code paths with +nuanced and complex invariants. diff --git a/docs/src/api/api_overview.md b/docs/src/api/api_overview.md new file mode 100644 index 00000000..ab3e6dee --- /dev/null +++ b/docs/src/api/api_overview.md @@ -0,0 +1,185 @@ +# API Overview (WIP) + +`medusa` offers a lower level API to hook into various parts of the fuzzer, its workers, and underlying chains. Although assertion and property testing are two built-in testing providers, they are implementing using events and hooks offered throughout the `Fuzzer`, `FuzzerWorker`(s), and underlying `TestChain`. These same hooks can be used by external developers wishing to implement their own customing testing methodology. In the sections below, we explore some of the relevant components throughout `medusa`, their events/hooks, an example of creating custom testing methodology with it. + +## Component overview + +A rudimentary description of the objects/providers and their roles are explained below. + +### Data types + +- `ProjectConfig`: This defines the configuration for the Fuzzer, including the targets to compile, deploy, and how to fuzz or test them. + +- `ValueSet`: This is an object that acts as a dictionary of values, used in mutation operations. It is populated at compilation time with some rudimentary static analysis. + +- `Contract`: Can be thought of as a "contract definition", it is a data type which stores the name of the contract, and a reference to the underlying `CompiledContract`, a definition derived from compilation, containing the bytecode, source maps, ABI, etc. + +- `CallSequence`: This represents a list of `CallSequenceElement`s, which define a transaction to send, the suggested block number and timestamp delay to use, and stores a reference to the block/transaction/results when it is executed (for later querying in tests). They are used to generate and execute transaction sequences in the fuzzer. + +- `CoverageMaps` define a list of `CoverageMap` objects, which record all instruction offsets executed for a given contract address and code hash. + +- `TestCase` defines the interface for a test that the `Fuzzer` will track. It simply defines a name, ID, status (not started, running, passed, failed) and message for the `Fuzzer`. + +### Providers + +- `ValueGenerator`: This is an object that provides methods to generate values of different kinds for transactions. Examples include the `RandomValueGenerator` and superceding `MutationalValueGenerator`. They are provided a `ValueSet` by their worker, which they may use in generation operations. + +- `TestChain`: This is a fake chain that operates on fake block structures created for the purpose of testing. Rather than operating on `types.Transaction` (which requires signing), it operates on `core.Message`s, which are derived from transactions and simply allow you to set the `sender` field. It is responsible for: + + - Maintaining state of the chain (blocks, transactions in them, results/receipts) + - Providing methods to create blocks, add transactions to them, commit them to chain, revert to previous block numbers. + - Allowing spoofing of block number and timestamp (commiting block number 1, then 50, jumping 49 blocks ahead), while simulating the existence of intermediate blocks. + - Provides methods to add tracers such as `evm.Logger` (standard go-ethereum tracers) or extend them with an additional interface (`TestChainTracer`) to also store any captured traced information in the execution results. This allows you to trace EVM execution for certain conditions, store results, and query them at a later time for testing. + +- `Fuzzer`: This is the main provider for the fuzzing process. It takes a `ProjectConfig` and is responsible for: + + - Housing data shared between the `FuzzerWorker`s such as contract definitions, a `ValueSet` derived from compilation to use in value generation, the reference to `Corpus`, the `CoverageMaps` representing all coverage achieved, as well as maintaining `TestCase`s registered to it and printing their results. + - Compiling the targets defined by the project config and setting up state. + - Provides methods to start/stop the fuzzing process, add additional compilation targets, access the initial value set prior to fuzzing start, access corpus, config, register new test cases and report them finished. + - Starts the fuzzing process by creating a "base" `TestChain`, deploys compiled contracts, replays all corpus sequences to measure existing coverage from previous fuzzing campaign, then spawns as many `FuzzerWorker`s as configured on their own goroutines ("threads") and passes them the "base" `TestChain` (which they clone) to begin the fuzzing operation. + - Respawns `FuzzerWorker`s when they hit a config-defined reset limit for the amount of transaction sequences they should process before destroying themselves and freeing memory. + - Maintains the context for when fuzzing should stop, which all workers track. + +- `FuzzerWorker`: This describes an object spawned by the `Fuzzer` with a given "base" `TestChain` with target contracts already deployed, ready to be fuzzed. It clones this chain, then is called upon to begin creating fuzz transactions. It is responsible for: + - Maintaining a reference to the parent `Fuzzer` for any shared information between it and other workers (`Corpus`, total `CoverageMaps`, contract definitions to match deployment's bytecode, etc) + - Maintaining its own `TestChain` to run fuzzed transaction sequences. + - Maintaining its own `ValueSet` which derives from the `Fuzzer`'s `ValueSet` (populated by compilation or user-provided values through API), as each `FuzzerWorker` may populate its `ValueSet` with different runtime values depending on their own chain state. + - Spawning a `ValueGenerator` which uses the `ValueSet`, to generate values used to construct fuzzed transaction sequences. + - Most importantly, it continuously: + - Generates `CallSequence`s (a series of transactions), plays them on its `TestChain`, records the results of in each `CallSequenceElement`, and calls abstract/hookable "test functions" to indicate they should perform post-tx tests (for which they can return requests for a shrunk test sequence). + - Updates the total `CoverageMaps` and `Corpus` with the current `CallSequence` if the most recent call increased coverage. + - Processes any shrink requests from the previous step (shrink requests can define arbitrary criteria for shrinking). + - Eventually, hits the config-defined reset limit for how many sequences it should process, and destroys itself to free all memory, expecting the `Fuzzer` to respawn another in its place. + +## Creating a project configuration + +`medusa` is config-driven. To begin a fuzzing campaign on an API level, you must first define a project configuration so the fuzzer knows what contracts to compile, deploy, and how it should operate. + +When using `medusa` over command-line, it operates a project config similarly (see [docs](https://github.com/trailofbits/medusa/wiki/Project-Configuration) or [example](https://github.com/trailofbits/medusa/wiki/Example-Project-Configuration-File)). Similarly, interfacing with a `Fuzzer` requires a `ProjectConfig` object. After importing `medusa` into your Go project, you can create one like this: + +```go +// Initialize a default project config with using crytic-compile as a compilation platform, and set the target it should compile. +projectConfig := config.GetDefaultProjectConfig("crytic-compile") +err := projectConfig.Compilation.SetTarget("contract.sol") +if err != nil { + return err +} + +// You can edit any of the values as you please. +projectConfig.Fuzzing.Workers = 20 +projectConfig.Fuzzing.DeploymentOrder = []string{"TestContract1", "TestContract2"} +``` + +You may also instantiate the whole config in-line with all the fields you'd like, setting the underlying platform config yourself. + +> **NOTE**: The `CompilationConfig` and `PlatformConfig` WILL BE deprecated and replaced with something more intuitive in the future, as the `compilation` package has not been updated since the project's inception, prior to the release of generics in go 1.18. + +## Creating and starting the fuzzer + +After you have created a `ProjectConfig`, you can create a new `Fuzzer` with it, and tell it to start: + +```go + // Create our fuzzer + fuzzer, err := fuzzing.NewFuzzer(*projectConfig) + if err != nil { + return err + } + + // Start the fuzzer + err = fuzzer.Start() + if err != nil { + return err + } + + // Fetch test cases results + testCases := fuzzer.TestCases() +[...] +``` + +> **Note**: `Fuzzer.Start()` is a blocking operation. If you wish to stop, you must define a TestLimit or Timeout in your config. Otherwise start it on another goroutine and call `Fuzzer.Stop()` to stop it. + +## Events/Hooks + +### Events + +Now it may be the case that you wish to hook the `Fuzzer`, `FuzzerWorker`, or `TestChain` to provide your own functionality. You can add your own testing methodology, and even power it with your own low-level EVM execution tracers to store and query results about each call. + +There are a few events/hooks that may be useful of the bat: + +The `Fuzzer` maintains event emitters for the following events under `Fuzzer.Events.*`: + +- `FuzzerStartingEvent`: Indicates a `Fuzzer` is starting and provides a reference to it. + +- `FuzzerStoppingEvent`: Indicates a `Fuzzer` has just stopped all workers and is about to print results and exit. + +- `FuzzerWorkerCreatedEvent`: Indicates a `FuzzerWorker` was created by a `Fuzzer`. It provides a reference to the `FuzzerWorker` spawned. The parent `Fuzzer` can be accessed through `FuzzerWorker.Fuzzer()`. +- `FuzzerWorkerDestroyedEvent`: Indicates a `FuzzerWorker` was destroyed. This can happen either due to hitting the config-defined worker reset limit or the fuzzing operation stopping. It provides a reference to the destroyed worker (for reference, though this should not be stored, to allow memory to free). + +The `FuzzerWorker` maintains event emiters for the following events under `FuzzerWorker.Events.*`: + +- `FuzzerWorkerChainCreatedEvent`: This indicates the `FuzzerWorker` is about to begin working and has created its chain (but not yet copied data from the "base" `TestChain` the `Fuzzer` provided). This offers an opportunity to attach tracers for calls made during chain setup. It provides a reference to the `FuzzerWorker` and its underlying `TestChain`. + +- `FuzzerWorkerChainSetupEvent`: This indicates the `FuzzerWorker` is about to begin working and has both created its chain, and copied data from the "base" `TestChain`, so the initial deployment of contracts is complete and fuzzing is ready to begin. It provides a reference to the `FuzzerWorker` and its underlying `TestChain`. + +- `CallSequenceTesting`: This indicates a new `CallSequence` is about to be generated and tested by the `FuzzerWorker`. It provides a reference to the `FuzzerWorker`. + +- `CallSequenceTested`: This indicates a `CallSequence` was just tested by the `FuzzerWorker`. It provides a reference to the `FuzzerWorker`. + +- `FuzzerWorkerContractAddedEvent`: This indicates a contract was added on the `FuzzerWorker`'s underlying `TestChain`. This event is emitted when the contract byte code is resolved to a `Contract` definition known by the `Fuzzer`. It may be emitted due to a contract deployment, or the reverting of a block which caused a SELFDESTRUCT. It provides a reference to the `FuzzerWorker`, the deployed contract address, and the `Contract` definition that it was matched to. + +- `FuzzerWorkerContractDeletedEvent`: This indicates a contract was removed on the `FuzzerWorker`'s underlying `TestChain`. It may be emitted due to a contract deployment which was reverted, or a SELFDESTRUCT operation. It provides a reference to the `FuzzerWorker`, the deployed contract address, and the `Contract` definition that it was matched to. + +The `TestChain` maintains event emitters for the following events under `TestChain.Events.*`: + +- `PendingBlockCreatedEvent`: This indicates a new block is being created but has not yet been committed to the chain. The block is empty at this point but will likely be populated. It provides a reference to the `Block` and `TestChain`. + +- `PendingBlockAddedTxEvent`: This indicates a pending block which has not yet been commited to chain has added a transaction to it, as it is being constructed. It provides a reference to the `Block`, `TestChain`, and index of the transaction in the `Block`. + +- `PendingBlockCommittedEvent`: This indicates a pending block was committed to chain as the new head. It provides a reference to the `Block` and `TestChain`. + +- `PendingBlockDiscardedEvent`: This indicates a pending block was not committed to chain and was instead discarded. + +- `BlocksRemovedEvent`: This indicates blocks were removed from the chain. This happens when a chain revert to a previous block number is invoked. It provides a reference to the `Block` and `TestChain`. + +- `ContractDeploymentsAddedEvent`: This indicates a new contract deployment was detected on chain. It provides a reference to the `TestChain`, as well as information captured about the bytecode. This may be triggered on contract deployment, or the reverting of a SELFDESTRUCT operation. + +- `ContractDeploymentsRemovedEvent`: This indicates a previously deployed contract deployment was removed from chain. It provides a reference to the `TestChain`, as well as information captured about the bytecode. This may be triggered on revert of a contract deployment, or a SELFDESTRUCT operation. + +### Hooks + +The `Fuzzer` maintains hooks for some of its functionality under `Fuzzer.Hooks.*`: + +- `NewValueGeneratorFunc`: This method is used to create a `ValueGenerator` for each `FuzzerWorker`. By default, this uses a `MutationalValueGenerator` constructed with the provided `ValueSet`. It can be replaced to provide a custom `ValueGenerator`. + +- `TestChainSetupFunc`: This method is used to set up a chain's initial state before fuzzing. By default, this method deploys all contracts compiled and marked for deployment in the `ProjectConfig` provided to the `Fuzzer`. It only deploys contracts if they have no constructor arguments. This can be replaced with your own method to do custom deployments. + + - **Note**: We do not recommend replacing this for now, as the `Contract` definitions may not be known to the `Fuzzer`. Additionally, `SenderAddresses` and `DeployerAddress` are the only addresses funded at genesis. This will be updated at a later time. + +- `CallSequenceTestFuncs`: This is a list of functions which are called after each `FuzzerWorker` executed another call in its current `CallSequence`. It takes the `FuzzerWorker` and `CallSequence` as input, and is expected to return a list of `ShinkRequest`s if some interesting result was found and we wish for the `FuzzerWorker` to shrink the sequence. You can add a function here as part of custom post-call testing methodology to check if some property was violated, then request a shrunken sequence for it with arbitrary criteria to verify the shrunk sequence satisfies your requirements (e.g. violating the same property again). + +### Extending testing methodology + +Although we will build out guidance on how you can solve different challenges or employ different tests with this lower level API, we intend to wrap some of this into a higher level API that allows testing complex post-call/event conditions with just a few lines of code externally. The lower level API will serve for more granular control across the system, and fine tuned optimizations. + +To ensure testing methodology was agnostic and extensible in `medusa`, we note that both assertion and property testing is implemented through the abovementioned events and hooks. When a higher level API is introduced, we intend to migrate these test case providers to that API. + +For now, the built-in `AssertionTestCaseProvider` (found [here](https://github.com/trailofbits/medusa/blob/8036697794481b7bf9fa78c922ec7fa6a8a3005c/fuzzing/test_case_assertion_provider.go)) and its test cases (found [here](https://github.com/trailofbits/medusa/blob/8036697794481b7bf9fa78c922ec7fa6a8a3005c/fuzzing/test_case_assertion.go)) are an example of code that _could_ exist externally outside of `medusa`, but plug into it to offer extended testing methodology. Although it makes use of some private variables, they can be replaced with public getter functions that are available. As such, if assertion testing didn't exist in `medusa` natively, you could've implemented it yourself externally! + +In the end, using it would look something like this: + +```go + // Create our fuzzer + fuzzer, err := fuzzing.NewFuzzer(*projectConfig) + if err != nil { + return err + } + + // Attach our custom test case provider + attachAssertionTestCaseProvider(fuzzer) + + // Start the fuzzer + err = fuzzer.Start() + if err != nil { + return err + } +``` diff --git a/docs/src/cheatcodes/addr.md b/docs/src/cheatcodes/addr.md new file mode 100644 index 00000000..5fe04c04 --- /dev/null +++ b/docs/src/cheatcodes/addr.md @@ -0,0 +1,24 @@ +# `addr` + +## Description + +The `addr` cheatcode will compute the address for a given private key. + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Test with random private key +uint256 pkOne = 0x6df21769a2082e03f7e21f6395561279e9a7feb846b2bf740798c794ad196e00; +address addrOne = 0xdf8Ef652AdE0FA4790843a726164df8cf8649339; +address result = cheats.addr(pkOne); +assert(result == addrOne); +``` + +## Function Signature + +```solidity +function addr(uint256 privateKey) external returns (address); +``` diff --git a/docs/src/cheatcodes/chain_id.md b/docs/src/cheatcodes/chain_id.md new file mode 100644 index 00000000..ae8fedeb --- /dev/null +++ b/docs/src/cheatcodes/chain_id.md @@ -0,0 +1,22 @@ +# `chainId` + +## Description + +The `chainId` cheatcode will set the `block.chainid` + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.chainId(777123); +assert(block.chainid == 777123); +``` + +## Function Signature + +```solidity +function chainId(uint256) external; +``` diff --git a/docs/src/cheatcodes/cheatcodes_overview.md b/docs/src/cheatcodes/cheatcodes_overview.md new file mode 100644 index 00000000..ec2dcf46 --- /dev/null +++ b/docs/src/cheatcodes/cheatcodes_overview.md @@ -0,0 +1,118 @@ +# Cheatcodes Overview + +Cheatcodes allow users to manipulate EVM state, blockchain behavior, provide easy ways to manipulate data, and much more. +The cheatcode contract is deployed at `0x7109709ECfa91a80626fF3989D68f67F5b1DD12D`. + +## Cheatcode Interface + +The following interface must be added to your Solidity project if you wish to use cheatcodes. Note that if you use Foundry +as your compilation platform that the cheatcode interface is already provided [here](https://book.getfoundry.sh/reference/forge-std/#forge-stds-test). +However, it is important to note that medusa does not support all the cheatcodes provided out-of-box +by Foundry (see below for supported cheatcodes). + +```solidity +interface StdCheats { + // Set block.timestamp + function warp(uint256) external; + + // Set block.number + function roll(uint256) external; + + // Set block.basefee + function fee(uint256) external; + + // Set block.difficulty and block.prevrandao + function difficulty(uint256) external; + + // Set block.chainid + function chainId(uint256) external; + + // Sets the block.coinbase + function coinbase(address) external; + + // Loads a storage slot from an address + function load(address account, bytes32 slot) external returns (bytes32); + + // Stores a value to an address' storage slot + function store(address account, bytes32 slot, bytes32 value) external; + + // Sets the *next* call's msg.sender to be the input address + function prank(address) external; + + // Set msg.sender to the input address until the current call exits + function prankHere(address) external; + + // Sets an address' balance + function deal(address who, uint256 newBalance) external; + + // Sets an address' code + function etch(address who, bytes calldata code) external; + + // Signs data + function sign(uint256 privateKey, bytes32 digest) + external + returns (uint8 v, bytes32 r, bytes32 s); + + // Computes address for a given private key + function addr(uint256 privateKey) external returns (address); + + // Gets the nonce of an account + function getNonce(address account) external returns (uint64); + + // Sets the nonce of an account + // The new nonce must be higher than the current nonce of the account + function setNonce(address account, uint64 nonce) external; + + // Performs a foreign function call via terminal + function ffi(string[] calldata) external returns (bytes memory); + + // Take a snapshot of the current state of the EVM + function snapshot() external returns (uint256); + + // Revert state back to a snapshot + function revertTo(uint256) external returns (bool); + + // Convert Solidity types to strings + function toString(address) external returns(string memory); + function toString(bytes calldata) external returns(string memory); + function toString(bytes32) external returns(string memory); + function toString(bool) external returns(string memory); + function toString(uint256) external returns(string memory); + function toString(int256) external returns(string memory); + + // Convert strings into Solidity types + function parseBytes(string memory) external returns(bytes memory); + function parseBytes32(string memory) external returns(bytes32); + function parseAddress(string memory) external returns(address); + function parseUint(string memory)external returns(uint256); + function parseInt(string memory) external returns(int256); + function parseBool(string memory) external returns(bool); +} +``` + +# Using cheatcodes + +Below is an example snippet of how you would import the cheatcode interface into your project and use it. + +```solidity +// Assuming cheatcode interface is in the same directory +import "./IStdCheats.sol"; + +// MyContract will utilize the cheatcode interface +contract MyContract { + // Set up reference to cheatcode contract + IStdCheats cheats = IStdCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // This is a test function that will set the msg.sender's nonce to the provided input argument + function testFunc(uint256 _x) public { + // Ensure that the input argument is greater than msg.sender's current nonce + require(_x > cheats.getNonce(msg.sender)); + + // Set sender's nonce + cheats.setNonce(msg.sender, x); + + // Assert that the nonce has been correctly updated + assert(cheats.getNonce(msg.sender) == x); + } +} +``` diff --git a/docs/src/cheatcodes/coinbase.md b/docs/src/cheatcodes/coinbase.md new file mode 100644 index 00000000..a0ab068e --- /dev/null +++ b/docs/src/cheatcodes/coinbase.md @@ -0,0 +1,22 @@ +# `coinbase` + +## Description + +The `coinbase` cheatcode will set the `block.coinbase` + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.coinbase(address(7)); +assert(block.coinbase == address(7)); +``` + +## Function Signature + +```solidity +function coinbase(address) external; +``` diff --git a/docs/src/cheatcodes/deal.md b/docs/src/cheatcodes/deal.md new file mode 100644 index 00000000..4518f61f --- /dev/null +++ b/docs/src/cheatcodes/deal.md @@ -0,0 +1,23 @@ +# `deal` + +## Description + +The `deal` cheatcode will set the ETH balance of address `who` to `newBalance` + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +address acc = address(777); +cheats.deal(acc, x); +assert(acc.balance == x); +``` + +## Function Signature + +```solidity +function deal(address who, uint256 newBalance) external; +``` diff --git a/docs/src/cheatcodes/difficulty.md b/docs/src/cheatcodes/difficulty.md new file mode 100644 index 00000000..fa901849 --- /dev/null +++ b/docs/src/cheatcodes/difficulty.md @@ -0,0 +1,25 @@ +# `difficulty` + +## Description + +The `difficulty` cheatcode will set the `block.difficulty` and the `block.prevrandao` value. At the moment, both values +are changed since the cheatcode does not check what EVM version is running. + +Note that this behavior will change in the future. + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.difficulty(x); +assert(block.difficulty == x); +``` + +## Function Signature + +```solidity +function difficulty(uint256) external; +``` diff --git a/docs/src/cheatcodes/etch.md b/docs/src/cheatcodes/etch.md new file mode 100644 index 00000000..4ba045bf --- /dev/null +++ b/docs/src/cheatcodes/etch.md @@ -0,0 +1,29 @@ +# `etch` + +## Description + +The `etch` cheatcode will set the `who` address's bytecode to `code`. + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Obtain our original code hash for an account. +address acc = address(777); +bytes32 originalCodeHash; +assembly { originalCodeHash := extcodehash(acc) } + +// Change value and verify. +cheats.etch(acc, address(someContract).code); +bytes32 updatedCodeHash; +assembly { updatedCodeHash := extcodehash(acc) } +assert(originalCodeHash != updatedCodeHash); +``` + +## Function Signature + +```solidity +function etch(address who, bytes calldata code) external; +``` diff --git a/docs/src/cheatcodes/fee.md b/docs/src/cheatcodes/fee.md new file mode 100644 index 00000000..a4c6f115 --- /dev/null +++ b/docs/src/cheatcodes/fee.md @@ -0,0 +1,22 @@ +# `fee` + +## Description + +The `fee` cheatcode will set the `block.basefee`. + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.fee(7); +assert(block.basefee == 7); +``` + +## Function Signature + +```solidity +function fee(uint256) external; +``` diff --git a/docs/src/cheatcodes/ffi.md b/docs/src/cheatcodes/ffi.md new file mode 100644 index 00000000..562528ad --- /dev/null +++ b/docs/src/cheatcodes/ffi.md @@ -0,0 +1,58 @@ +# `ffi` + +## Description + +The `ffi` cheatcode is used to call an arbitrary command on your host OS. Note that `ffi` must be enabled via the project +configuration file by setting `fuzzing.chainConfig.cheatCodes.enableFFI` to `true`. + +Note that enabling `ffi` allows anyone to execute arbitrary commands on devices that run the fuzz tests which may +become a security risk. + +Please review [Foundry's documentation on the `ffi` cheatcode](https://book.getfoundry.sh/cheatcodes/ffi#tips) for general tips. + +## Example with ABI-encoded hex + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Create command +string[] memory inputs = new string[](3); +inputs[0] = "echo"; +inputs[1] = "-n"; +// Encoded "hello" +inputs[2] = "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000568656C6C6F000000000000000000000000000000000000000000000000000000"; + +// Call cheats.ffi +bytes memory res = cheats.ffi(inputs); + +// ABI decode +string memory output = abi.decode(res, (string)); +assert(keccak256(abi.encodePacked(output)) == keccak256(abi.encodePacked("hello"))); +``` + +## Example with UTF8 encoding + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Create command +string[] memory inputs = new string[](3); +inputs[0] = "echo"; +inputs[1] = "-n"; +inputs[2] = "hello"; + +// Call cheats.ffi +bytes memory res = cheats.ffi(inputs); + +// Convert to UTF-8 string +string memory output = string(res); +assert(keccak256(abi.encodePacked(output)) == keccak256(abi.encodePacked("hello"))); +``` + +## Function Signature + +```solidity +function ffi(string[] calldata) external returns (bytes memory); +``` diff --git a/docs/src/cheatcodes/get_nonce.md b/docs/src/cheatcodes/get_nonce.md new file mode 100644 index 00000000..b94f3773 --- /dev/null +++ b/docs/src/cheatcodes/get_nonce.md @@ -0,0 +1,22 @@ +# `getNonce` + +## Description + +The `getNonce` cheatcode will get the current nonce of `account`. + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Get nonce and verify that the sender has sent at least one transaction +address acc = address(msg.sender); +assert(cheats.getNonce(acc) > 0); +``` + +## Function Signature + +```solidity +function getNonce(address account) external returns (uint64); +``` diff --git a/docs/src/cheatcodes/load.md b/docs/src/cheatcodes/load.md new file mode 100644 index 00000000..488891b4 --- /dev/null +++ b/docs/src/cheatcodes/load.md @@ -0,0 +1,27 @@ +# `load` + +## Description + +The `load` cheatcode will load storage slot `slot` for `account` + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Load and verify x + bytes32 value = cheats.load(address(this), bytes32(uint(0))); + assert(value == bytes32(uint(123))); + } +} +``` + +## Function Signature + +```solidity +function load(address account, bytes32 slot) external returns (bytes32); +``` diff --git a/docs/src/cheatcodes/parse_address.md b/docs/src/cheatcodes/parse_address.md new file mode 100644 index 00000000..335a7eb0 --- /dev/null +++ b/docs/src/cheatcodes/parse_address.md @@ -0,0 +1,30 @@ +# `parseAddress` + +## Description + +The `parseAddress` cheatcode will parse the input string into an address + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + address expectedAddress = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + string memory test = "0x7109709ECfa91a80626fF3989D68f67F5b1DD12D"; + + // Call cheats.parseAddress + address result = cheats.parseAddress(test); + assert(expectedAddress == result); + } +} +``` + +## Function Signature + +```solidity +function parseAddress(string calldata) external returns (address); +``` diff --git a/docs/src/cheatcodes/parse_bool.md b/docs/src/cheatcodes/parse_bool.md new file mode 100644 index 00000000..dbbc7241 --- /dev/null +++ b/docs/src/cheatcodes/parse_bool.md @@ -0,0 +1,30 @@ +# `parseBool` + +## Description + +The `parseBool` cheatcode will parse the input string into a boolean + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + bool expectedBool = true; + string memory test = "true"; + + // Call cheats.parseBool + bool result = cheats.parseBool(test); + assert(expectedBool == result); + } +} +``` + +## Function Signature + +```solidity +function parseBool(string calldata) external returns (bool); +``` diff --git a/docs/src/cheatcodes/parse_bytes.md b/docs/src/cheatcodes/parse_bytes.md new file mode 100644 index 00000000..4612116e --- /dev/null +++ b/docs/src/cheatcodes/parse_bytes.md @@ -0,0 +1,30 @@ +# `parseBytes` + +## Description + +The `parseBytes` cheatcode will parse the input string into bytes + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + bytes memory expectedBytes = "medusa"; + string memory test = "medusa"; + + // Call cheats.parseBytes + bytes memory result = cheats.parseBytes(test); + assert(keccak256(expectedBytes) == keccak256(result)); + } +} +``` + +## Function Signature + +```solidity +function parseBytes(string calldata) external returns (bytes memory); +``` diff --git a/docs/src/cheatcodes/parse_bytes32.md b/docs/src/cheatcodes/parse_bytes32.md new file mode 100644 index 00000000..6fb0ab2a --- /dev/null +++ b/docs/src/cheatcodes/parse_bytes32.md @@ -0,0 +1,30 @@ +# `parseBytes32` + +## Description + +The `parseBytes32` cheatcode will parse the input string into bytes32 + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + int256 expectedInt = -12345; + string memory test = "-12345"; + + // Call cheats.parseInt + int256 result = cheats.parseInt(test); + assert(expectedInt == result); + } +} +``` + +## Function Signature + +```solidity +function parseBytes32(string calldata) external returns (bytes32); +``` diff --git a/docs/src/cheatcodes/parse_int.md b/docs/src/cheatcodes/parse_int.md new file mode 100644 index 00000000..6a820a3c --- /dev/null +++ b/docs/src/cheatcodes/parse_int.md @@ -0,0 +1,30 @@ +# `parseInt` + +## Description + +The `parseInt` cheatcode will parse the input string into a int256 + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + address expectedAddress = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + string memory test = "0x7109709ECfa91a80626fF3989D68f67F5b1DD12D"; + + // Call cheats.parseAddress + address result = cheats.parseAddress(test); + assert(expectedAddress == result); + } +} +``` + +## Function Signature + +```solidity +function parseInt(string calldata) external returns (int256); +``` diff --git a/docs/src/cheatcodes/parse_uint.md b/docs/src/cheatcodes/parse_uint.md new file mode 100644 index 00000000..8c9f2e46 --- /dev/null +++ b/docs/src/cheatcodes/parse_uint.md @@ -0,0 +1,30 @@ +# `parseUint` + +## Description + +The `parseUint` cheatcode will parse the input string into a uint256 + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + uint256 expectedUint = 12345; + string memory test = "12345"; + + // Call cheats.parseUint + uint256 result = cheats.parseUint(test); + assert(expectedUint == result); + } +} +``` + +## Function Signature + +```solidity +function parseUint(string calldata) external returns (uint256); +``` diff --git a/docs/src/cheatcodes/prank.md b/docs/src/cheatcodes/prank.md new file mode 100644 index 00000000..f309ba3f --- /dev/null +++ b/docs/src/cheatcodes/prank.md @@ -0,0 +1,38 @@ +# `prank` + +## Description + +The `prank` cheatcode will set the `msg.sender` for _only the next call_ to the specified input address. Note that, +contrary to [`prank` in Foundry](https://book.getfoundry.sh/cheatcodes/prank#description), calling the cheatcode contract will count as a +valid "next call" + +## Example + +```solidity +contract TestContract { + address owner = address(123); + function transferOwnership(address _newOwner) public { + require(msg.sender == owner); + + // Change ownership + owner = _newOwner; + } + + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Prank, change ownership, and verify + address newOwner = address(456); + cheats.prank(owner); + transferOwnership(newOwner); + assert(owner == newOwner); + } + } +``` + +## Function Signature + +```solidity +function prank(address) external; +``` diff --git a/docs/src/cheatcodes/prank_here.md b/docs/src/cheatcodes/prank_here.md new file mode 100644 index 00000000..5723cfb8 --- /dev/null +++ b/docs/src/cheatcodes/prank_here.md @@ -0,0 +1,49 @@ +# `prankHere` + +## Description + +The `prankHere` cheatcode will set the `msg.sender` to the specified input address until the current call exits. Compared +to `prank`, `prankHere` can persist for multiple calls. + +## Example + +```solidity +contract TestContract { + address owner = address(123); + uint256 x = 0; + uint256 y = 0; + + function updateX() public { + require(msg.sender == owner); + + // Update x + x = 1; + } + + function updateY() public { + require(msg.sender == owner); + + // Update y + y = 1; + } + + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Prank, update variables, and verify + cheats.prank(owner); + updateX(); + updateY(); + assert((x == 1) && (y == 1)); + + // Once this function returns, the `msg.sender` is reset + } +} +``` + +## Function Signature + +```solidity +function prankHere(address) external; +``` diff --git a/docs/src/cheatcodes/roll.md b/docs/src/cheatcodes/roll.md new file mode 100644 index 00000000..57b901e0 --- /dev/null +++ b/docs/src/cheatcodes/roll.md @@ -0,0 +1,24 @@ +# `roll` + +## Description + +The `roll` cheatcode sets the `block.number` + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.roll(7); +assert(block.number == 7); +cheats.roll(9); +assert(block.number == 9); +``` + +## Function Signature + +```solidity +function roll(uint256) external; +``` diff --git a/docs/src/cheatcodes/set_nonce.md b/docs/src/cheatcodes/set_nonce.md new file mode 100644 index 00000000..8d949c8e --- /dev/null +++ b/docs/src/cheatcodes/set_nonce.md @@ -0,0 +1,24 @@ +# setNonce + +## Description + +The `setNonce` cheatcode will set the nonce of `account` to `nonce`. Note that the `nonce` must be strictly greater than +the current nonce + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Set nonce and verify (assume nonce before `setNonce` was less than 7) +address acc = address(msg.sender); +cheats.setNonce(acc, 7); +assert(cheats.getNonce(acc) == 7); +``` + +## Function Signature + +```solidity +function setNonce(address account, uint64 nonce) external; +``` diff --git a/docs/src/cheatcodes/sign.md b/docs/src/cheatcodes/sign.md new file mode 100644 index 00000000..dea23c2c --- /dev/null +++ b/docs/src/cheatcodes/sign.md @@ -0,0 +1,28 @@ +# `sign` + +## Description + +The `sign` cheatcode will take in a private key `privateKey` and a hash digest `digest` to generate a `(v, r, s)` +signature + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +bytes32 digest = keccak256("Data To Sign"); + +// Call cheats.sign +(uint8 v, bytes32 r, bytes32 s) = cheats.sign(0x6df21769a2082e03f7e21f6395561279e9a7feb846b2bf740798c794ad196e00, digest); +address signer = ecrecover(digest, v, r, s); +assert(signer == 0xdf8Ef652AdE0FA4790843a726164df8cf8649339); +``` + +## Function Signature + +```solidity +function sign(uint256 privateKey, bytes32 digest) +external +returns (uint8 v, bytes32 r, bytes32 s); +``` diff --git a/docs/src/cheatcodes/snapshot.md b/docs/src/cheatcodes/snapshot.md new file mode 100644 index 00000000..27298f6b --- /dev/null +++ b/docs/src/cheatcodes/snapshot.md @@ -0,0 +1,68 @@ +# `snapshot` and `revertTo` + +## Description + +The `snapshot` cheatcode will take a snapshot of the current state of the blockchain and return an identifier for the +snapshot. + +On the flipside, the `revertTo` cheatcode will revert the EVM state back based on the provided identifier. + +## Example + +```solidity +interface CheatCodes { + function warp(uint256) external; + + function deal(address, uint256) external; + + function snapshot() external returns (uint256); + + function revertTo(uint256) external returns (bool); +} + +struct Storage { + uint slot0; + uint slot1; +} + +contract TestContract { + Storage store; + uint256 timestamp; + + function test() public { + // Obtain our cheat code contract reference. + CheatCodes cheats = CheatCodes( + 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D + ); + + store.slot0 = 10; + store.slot1 = 20; + timestamp = block.timestamp; + cheats.deal(address(this), 5 ether); + + // Save state + uint256 snapshot = cheats.snapshot(); + + // Change state + store.slot0 = 300; + store.slot1 = 400; + cheats.deal(address(this), 500 ether); + cheats.warp(12345); + + // Assert that state has been changed + assert(store.slot0 == 300); + assert(store.slot1 == 400); + assert(address(this).balance == 500 ether); + assert(block.timestamp == 12345); + + // Revert to snapshot + cheats.revertTo(snapshot); + + // Ensure state has been reset + assert(store.slot0 == 10); + assert(store.slot1 == 20); + assert(address(this).balance == 5 ether); + assert(block.timestamp == timestamp); + } +} +``` diff --git a/docs/src/cheatcodes/store.md b/docs/src/cheatcodes/store.md new file mode 100644 index 00000000..d3fb580d --- /dev/null +++ b/docs/src/cheatcodes/store.md @@ -0,0 +1,27 @@ +# `store` + +## Description + +The `store` cheatcode will store `value` in storage slot `slot` for `account` + +## Example + +```solidity +contract TestContract { + uint x = 123; + function test() public { + // Obtain our cheat code contract reference. + IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + // Store into x, verify it. + cheats.store(address(this), bytes32(uint(0)), bytes32(uint(456))); + assert(y == 456); + } +} +``` + +## Function Signature + +```solidity +function store(address account, bytes32 slot, bytes32 value) external; +``` diff --git a/docs/src/cheatcodes/to_string.md b/docs/src/cheatcodes/to_string.md new file mode 100644 index 00000000..75e4182f --- /dev/null +++ b/docs/src/cheatcodes/to_string.md @@ -0,0 +1,84 @@ +# `toString` + +## Description + +The `toString` cheatcodes aid in converting primitive Solidity types into strings. Similar to +[Foundry's behavior](https://book.getfoundry.sh/cheatcodes/to-string?highlight=toStr#description), bytes are converted +to a hex-encoded string with `0x` prefixed. + +## Example + +```solidity +contract TestContract { + IStdCheats cheats; + + constructor() { + cheats = IStdCheats(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + } + + function testAddress() public { + address test = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + string memory expectedString = "0x7109709ECfa91a80626fF3989D68f67F5b1DD12D"; + + // Call cheats.toString + string memory result = cheats.toString(test); + assert(keccak256(abi.encodePacked(result)) == keccak256(abi.encodePacked(expectedString))); + } + + function testBool() public { + bool test = true; + string memory expectedString = "true"; + + // Call cheats.toString + string memory result = cheats.toString(test); + assert(keccak256(abi.encodePacked(result)) == keccak256(abi.encodePacked(expectedString))); + } + + function testUint256() public { + uint256 test = 12345; + string memory expectedString = "12345"; + + // Call cheats.toString + string memory result = cheats.toString(test); + assert(keccak256(abi.encodePacked(result)) == keccak256(abi.encodePacked(expectedString))); + } + + function testInt256() public { + int256 test = -12345; + string memory expectedString = "-12345"; + + // Call cheats.toString + string memory result = cheats.toString(test); + assert(keccak256(abi.encodePacked(result)) == keccak256(abi.encodePacked(expectedString))); + } + + function testBytes32() public { + bytes32 test = "medusa"; + string memory expectedString = "0x6d65647573610000000000000000000000000000000000000000000000000000"; + + // Call cheats.toString + string memory result = cheats.toString(test); + assert(keccak256(abi.encodePacked(result)) == keccak256(abi.encodePacked(expectedString))); + } + + function testBytes() public { + bytes memory test = "medusa"; + string memory expectedString = "0x6d6564757361"; + + // Call cheats.toString + string memory result = cheats.toString(test); + assert(keccak256(abi.encodePacked(result)) == keccak256(abi.encodePacked(expectedString))); + } +} +``` + +## Function Signatures + +```solidity +function toString(address) external returns (string memory); +function toString(bool) external returns (string memory); +function toString(uint256) external returns (string memory); +function toString(int256) external returns (string memory); +function toString(bytes32) external returns (string memory); +function toString(bytes) external returns (string memory); +``` diff --git a/docs/src/cheatcodes/warp.md b/docs/src/cheatcodes/warp.md new file mode 100644 index 00000000..22830645 --- /dev/null +++ b/docs/src/cheatcodes/warp.md @@ -0,0 +1,24 @@ +# warp + +## Description + +The `warp` cheatcode sets the `block.timestamp` + +## Example + +```solidity +// Obtain our cheat code contract reference. +IStdCheats cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +// Change value and verify. +cheats.warp(7); +assert(block.timestamp == 7); +cheats.warp(9); +assert(block.timestamp == 9); +``` + +## Function Signature + +```solidity +function warp(uint256) external; +``` diff --git a/docs/src/cli/completion.md b/docs/src/cli/completion.md new file mode 100644 index 00000000..242355db --- /dev/null +++ b/docs/src/cli/completion.md @@ -0,0 +1,21 @@ +# `completion` + +`medusa` provides the ability to generate autocompletion scripts for a given shell. +Once the autocompletion script is ran for a given shell, `medusa`'s commands and flags can be tab-autocompleted. +The following shells are supported: + +1. `bash` +2. `zsh` +3. `Powershell` + +To understand how to run the autocompletion script for a given shell, run the following command: + +```shell +medusa completion --help +``` + +Once you know how to run the autocompletion script, retrieve the script for that given shell using the following command: + +```shell +medusa completion +``` diff --git a/docs/src/cli/fuzz.md b/docs/src/cli/fuzz.md new file mode 100644 index 00000000..ed70d15a --- /dev/null +++ b/docs/src/cli/fuzz.md @@ -0,0 +1,131 @@ +# `fuzz` + +The `fuzz` command will initiate a fuzzing campaign: + +```shell +medusa fuzz [flags] +``` + +## Supported Flags + +### `--config` + +The `--config` flag allows you to specify the path for your [project configuration](../project_configuration/overview.md) +file. If the `--config` flag is not used, `medusa` will look for a [`medusa.json`](../static/medusa.json) file in the +current working directory. + +```shell +# Set config file path +medusa fuzz --out myConfig.json +``` + +### `--compilation-target` + +The `--compilation-target` flag allows you to specify the compilation target. If you are using `crytic-compile`, please review the +warning [here](../project_configuration/compilation_config.md#target) about changing the compilation target. + +```shell +# Set compilation target +medusa fuzz --target TestMyContract.sol +``` + +### `--workers` + +The `--workers` flag allows you to update the number of threads that will perform parallelized fuzzing (equivalent to +[`fuzzing.workers`](../project_configuration/fuzzing_config.md#workers)) + +```shell +# Set workers +medusa fuzz --workers 20 +``` + +### `--timeout` + +The `--timeout` flag allows you to update the duration of the fuzzing campaign (equivalent to +[`fuzzing.timeout`](../project_configuration/fuzzing_config.md#timeout)) + +```shell +# Set timeout +medusa fuzz --timeout 100 +``` + +### `--test-limit` + +The `--test-limit` flag allows you to update the number of transactions to run before stopping the fuzzing campaign +(equivalent to [`fuzzing.testLimit`](../project_configuration/fuzzing_config.md#testlimit)) + +```shell +# Set test limit +medusa fuzz --test-limit 100000 +``` + +### `--seq-len` + +The `--seq-len` flag allows you to update the length of a call sequence (equivalent to +[`fuzzing.callSequenceLength`](../project_configuration/fuzzing_config.md#callsequencelength)) + +```shell +# Set sequence length +medusa fuzz --seq-len 50 +``` + +### `--target-contracts` + +The `--target-contracts` flag allows you to update the target contracts for fuzzing (equivalent to +[`fuzzing.targetContracts`](../project_configuration/fuzzing_config.md#targetcontracts)) + +```shell +# Set target contracts +medusa fuzz --target-contracts "TestMyContract, TestMyOtherContract" +``` + +### `--corpus-dir` + +The `--corpus-dir` flag allows you to set the path for the corpus directory (equivalent to +[`fuzzing.corpusDirectory`](../project_configuration/fuzzing_config.md#corpusdirectory)) + +```shell +# Set corpus directory +medusa fuzz --corpus-dir corpus +``` + +### `--senders` + +The `--senders` flag allows you to update `medusa`'s senders (equivalent to +[`fuzzing.senderAddresses`](../project_configuration/fuzzing_config.md#senderaddresses)) + +```shell +# Set sender addresses +medusa fuzz --senders "0x50000,0x60000,0x70000" +``` + +### `--deployer` + +The `--deployer` flag allows you to update `medusa`'s contract deployer (equivalent to +[`fuzzing.deployerAddress`](../project_configuration/fuzzing_config.md#deployeraddress)) + +```shell +# Set deployer address +medusa fuzz --deployer "0x40000" +``` + +### `--trace-all` + +The `--trace-all` flag allows you to retrieve an execution trace for each element of a call sequence that triggered a test +failure (equivalent to +[`testing.traceAll`](../project_configuration/testing_config.md#traceall) + +```shell +# Trace each call +medusa fuzz --trace-all +``` + +### `--no-color` + +The `--no-color` flag disables colored console output (equivalent to +[`logging.NoColor`](../project_configuration/logging_config.md#nocolor)) + +```shell +# Disable colored output +medusa fuzz --no-color +``` diff --git a/docs/src/cli/init.md b/docs/src/cli/init.md new file mode 100644 index 00000000..e6f13cac --- /dev/null +++ b/docs/src/cli/init.md @@ -0,0 +1,36 @@ +# `init` + +The `init` command will generate the project configuration file within your current working directory: + +```shell +medusa init [platform] [flags] +``` + +By default, the project configuration file will be named `medusa.json`. You can learn more about `medusa`'s project +configuration [here](../project_configuration/overview.md) and also view an [example project configuration file](../static/medusa.json). + +Invoking this command without a `platform` argument will result in `medusa` using `crytic-compile` as the default compilation platform. +Currently, the only other supported platform is `solc`. If you are using a compilation platform such as Foundry or Hardhat, +it is best to use `crytic-compile`. + +## Supported Flags + +### `--out` + +The `--out` flag allows you to specify the output path for the project configuration file. Thus, you can name the file +something different from `medusa.json` or have the configuration file be placed elsewhere in your filesystem. + +```shell +# Set config file path +medusa init --out myConfig.json +``` + +### `--compilation-target` + +The `--compilation-target` flag allows you to specify the compilation target. If you are using `crytic-compile`, please review the +warning [here](../project_configuration/compilation_config.md#target) about changing the compilation target. + +```shell +# Set compilation target +medusa init --compilation-target TestMyContract.sol +``` diff --git a/docs/src/cli/overview.md b/docs/src/cli/overview.md new file mode 100644 index 00000000..b35fb74d --- /dev/null +++ b/docs/src/cli/overview.md @@ -0,0 +1,10 @@ +# CLI Overview + +The `medusa` CLI is used to perform parallelized fuzz testing of smart contracts. After you have `medusa` +[installed](../getting_started/installation.md), you can run `medusa help` in your terminal to view the available commands. + +The CLI supports three main commands with each command having a variety of flags: + +- [`medusa init`](./init.md) +- [`medusa fuzz`](./fuzz.md) +- [`medusa completion`](./completion.md) diff --git a/docs/src/console_logging.md b/docs/src/console_logging.md new file mode 100644 index 00000000..63d69510 --- /dev/null +++ b/docs/src/console_logging.md @@ -0,0 +1,55 @@ +# Console Logging + +Console logging in medusa is similar to the functionality found in Foundry or Hardhat (except for string formatting, +see [below](#differences-in-consolelogformatargs)). Note that if you are not using +Foundry or Hardhat as your compilation platform, you can retrieve the necessary `console.sol` library +[here](https://github.com/foundry-rs/forge-std/blob/master/src/console.sol). + +For more information on the available function signatures and general tips on console logging, please review [Foundry's +documentation](https://book.getfoundry.sh/reference/forge-std/console-log#console-logging). + +## Differences in `console.log(format[,...args])` + +The core functionality of string formatting is the same. If you want to string format an `int256`, the only supported function signature is: +`function log(string memory, int256) external;`. Otherwise, the supported argument types are `string`, `bool`, `address`, +and `uint256`. This capability is the same as in Foundry. + +The core difference in medusa's string formatting is the specifiers that are allowed for the +formatted string. The supported specifiers are as follows: + +- `%v`: The value will be printed in its default format. This will work for `uint256`, `int256`, `address`, + `bool`, and `string`. Using `%v` is the **recommended** specifier for all argument types. +- `%s`: The values will be converted into a human-readable string. This will work for `uint256`, `int256`, `address`, and + `string`. Contrary to Foundry or Hardhat, `%s` will not work for `bool`. Additionally, `uint256` and `int256` will _not_ + be provided in their hex-encoded format. This is the **recommended** specifier for projects that wish to maintain + compatibility with an existing fuzz test suite from Foundry. Special exceptions will need to be made for `bool` arguments. + For example, you could use the `console.logBool(bool)` function to separately log the `bool`. +- `%d`: This can be used for `uint256` and `int256`. +- `%i`: This specifier is not supported by medusa for `int256` and `uint256` +- `%e`: This specifier is not supported by medusa for `int256` and `uint256`. +- `%x`: This provides the hexadecimal representation of `int256` and `uint256`. +- `%o`: This specifier is not supported by medusa. `%o` in medusa will provide the base-8 representation of `int256` and + `uint256`. +- `%t`: This can be used for `bool`. +- `%%`: This will print out "%" and not consume an argument. + +If a specifier does not have a corresponding argument, the following is returned: + +```solidity +console.log("My name is %s %s", "medusa"); +// Returns: "My name is medusa %!s(MISSING)" +``` + +If there are more arguments than specifiers, the following is returned: + +```solidity +console.log("My name is %s", "medusa", "fuzzer"); +// Returns: "My name is medusa%!(EXTRA string=fuzzer)" +``` + +If only a format string with no arguments is provided, the string is returned with no formatting: + +```solidity +console.log("%% %s"); +// Returns: "%% %s" +``` diff --git a/docs/src/coverage_reports.md b/docs/src/coverage_reports.md new file mode 100644 index 00000000..cd24b564 --- /dev/null +++ b/docs/src/coverage_reports.md @@ -0,0 +1,3 @@ +# Coverage Reports + +WIP diff --git a/docs/src/faq.md b/docs/src/faq.md new file mode 100644 index 00000000..4d5ea1b9 --- /dev/null +++ b/docs/src/faq.md @@ -0,0 +1,16 @@ +# Frequently Asked Questions + +**Why create a new fuzzer if Echidna is already a great fuzzer?** + +With medusa, we are exploring a different EVM implementation and language for our smart contract fuzzer. While Echidna is already doing an amazing job, medusa offers the following advantages: + +- It is written in Go, easing the maintenance and allowing the creation of a native API for future integration into other projects. +- It uses geth as a base, ensuring the EVM equivalence. + +**Should I switch to medusa right away?** + +We do not recommend switching to medusa until it is extensively tested. However we encourage you to try it, and [let us know your experience](https://github.com/trailofbits/medusa/issues). In that sense, Echidna is our robust and well tested fuzzer, while medusa is our new exploratory fuzzer. [Follow us](https://twitter.com/trailofbits/) to hear updates about medusa as it grows in maturity. + +**Will all the previous available documentation from [secure-contracts.com](https://secure-contracts.com/) will apply to medusa?** + +In general, yes. All the information on testing approaches and techniques will apply for medusa. There are, however, different configuration options names and a few missing or different features in medusa from Echidna that we will be updating over time. diff --git a/docs/src/getting_started/first_steps.md b/docs/src/getting_started/first_steps.md new file mode 100644 index 00000000..7cd8456d --- /dev/null +++ b/docs/src/getting_started/first_steps.md @@ -0,0 +1,36 @@ +# First Steps + +After installation, you are ready to use `medusa` on your first codebase. This chapter will walk you through initializing +`medusa` for a project and then starting to fuzz. + +To initialize medusa for a project, `cd` into your project and run [`medusa init`](../cli/init.md): + +```shell +# Change working directory +cd my_project + +# Initialize medusa +medusa init +``` + +This will create a `medusa.json` file which holds a large number of [configuration options](../project_configuration/overview.md). +`medusa` will use this configuration file to determine how and what to fuzz. + +All there is left to do now is to run `medusa` on some fuzz tests: + +```shell +medusa fuzz --target-contracts "TestContract" --test-limit 10_000 +``` + +The `--target-contracts` flag tells `medusa` which contracts to run fuzz tests on. You can specify more than one +contract to fuzz test at once (e.g. `--target-contracts "TestContract, TestOtherContract"`). The `--test-limit` flag +tells `medusa` to execute `10_000` transactions before stopping the fuzzing campaign. + +> Note: The target contracts and the test limit can also be configured via the project configuration file, which is the +> **recommended** route. The `--target-contracts` flag is equivalent to the +> [`fuzzing.targetContracts`](../project_configuration/fuzzing_config.md#targetcontracts) configuration option and the +> `-test-limit` flag is equivalent to the [`fuzzing.testLimit`](../project_configuration/fuzzing_config.md#testlimit) +> configuration option. + +It is recommended to review the [Configuration Overview](../project_configuration/overview.md) next and learn more about +[`medusa`'s CLI](../cli/overview.md). diff --git a/docs/src/getting_started/installation.md b/docs/src/getting_started/installation.md new file mode 100644 index 00000000..531758ec --- /dev/null +++ b/docs/src/getting_started/installation.md @@ -0,0 +1,64 @@ +# Installation + +There are three main ways to install `medusa` at the moment. The first is using Homebrew, +building from source, or installing a precompiled binary. + +If you have any difficulty with installing `medusa`, please [open an issue](https://github.com/crytic/medusa/issues) on GitHub. + +## Installing with Homebrew + +Note that using Homebrew is only viable (and recommended) for macOS and Linux users. For Windows users, you must +[build from source](#building-from-source) or [install a precompiled binary](#precompiled-binaries). + +### Prerequisites + +Installation instructions for Homebrew can be found [here](https://brew.sh/). + +### Install `medusa` + +Run the following command to install `medusa`: + +```shell +brew install medusa +``` + +## Building from source + +### Prerequisites + +Before downloading `medusa`, you will need to download Golang and `crytic-compile`. + +- Installation instructions for Golang can be found [here](https://go.dev/doc/install) +- Installation instructions for `crytic-compile` can be found [here](https://github.com/crytic/crytic-compile#installation) + - Note that `crytic-compile` requires a Python environment. Installation instructions for Python can be found + [here](https://www.python.org/downloads/). + +### Build `medusa` + +Run the following commands to build `medusa` (this should work on all OSes): + +```shell +# Clone the repository +git clone https://github.com/crytic/medusa + +# Build medusa +cd medusa +go build -trimpath +``` + +You will now need to move the binary (`medusa` or `medusa.exe`) to somewhere in your `PATH` environment variable so that +it is accessible via the command line. Please review the instructions +[here](https://zwbetz.com/how-to-add-a-binary-to-your-path-on-macos-linux-windows/) (if you are a Windows user, we +recommend using the Windows GUI). + +## Precompiled binaries + +The precompiled binaries can be downloaded on `medusa`'s [GitHub releases page](https://github.com/crytic/medusa/releases). + +> **_NOTE:_** macOS may set the [quarantine extended attribute](https://superuser.com/questions/28384/what-should-i-do-about-com-apple-quarantine) +> on the downloaded zip file. To remove this attribute, run the following command: +> `sudo xattr -rd com.apple.quarantine `. + +Once installed, you will need to unzip the file and move the binary to somewhere in your `$PATH`. Please review the instructions +[here](https://zwbetz.com/how-to-add-a-binary-to-your-path-on-macos-linux-windows/) (if you are a Windows user, we +recommend using the Windows GUI). diff --git a/docs/src/project_configuration/chain_config.md b/docs/src/project_configuration/chain_config.md new file mode 100644 index 00000000..b101b56d --- /dev/null +++ b/docs/src/project_configuration/chain_config.md @@ -0,0 +1,25 @@ +# Chain Configuration + +The chain configuration defines the parameters for setting up `medusa`'s underlying blockchain. + +### `codeSizeCheckDisabled` + +- **Type**: Boolean +- **Description**: If `true`, the maximum code size check of 24576 bytes in `go-ethereum` is disabled. +- > 🚩 Setting `codeSizeCheckDisabled` to `false` is not recommended since it complicates the fuzz testing process. +- **Default**: `true` + +## Cheatcode Configuration + +### `cheatCodesEnabled` + +- **Type**: Boolean +- **Description**: Determines whether cheatcodes are enabled. +- **Default**: `true` + +### `enableFFI` + +- **Type**: Boolean +- **Description**: Determines whether the `ffi` cheatcode is enabled. + > 🚩 Enabling the `ffi` cheatcode may allow for arbitrary code execution on your machine. +- **Default**: `false` diff --git a/docs/src/project_configuration/compilation_config.md b/docs/src/project_configuration/compilation_config.md new file mode 100644 index 00000000..4e298fdf --- /dev/null +++ b/docs/src/project_configuration/compilation_config.md @@ -0,0 +1,59 @@ +# Compilation Configuration + +The compilation configuration defines the parameters to use while compiling a target file or project. + +### `platform` + +- **Type**: String +- **Description**: Refers to the type of platform to be used to compile the underlying target. Currently, + `crytic-compile` or `solc` can be used as the compilation platform. +- **Default**: `crytic-compile` + +### `platformConfig` + +- **Type**: Struct +- **Description**: This struct is a platform-dependent structure which offers parameters for compiling the underlying project. + See below for the structure of `platformConfig` for each compilation platform. +- **Default**: The `platformConfig` for `crytic-compile` is the default value for this struct. + +### `platformConfig` for `crytic-compile` + +#### `target` + +- **Type**: String +- **Description**: Refers to the target that is being compiled. + > 🚩 Note that if you are using a compilation platform, such as Foundry or Hardhat, the default value for `target`, `.`, + > should **not** be changed. The `.` is equivalent to telling `crytic-compile` that the entire project needs to compiled, + > including any dependencies and remappings. In fact, unless you want to compile a single file, that has no third-party + > imports from, for example, OpenZeppelin, the default value should not be changed. +- **Default**: `.` + +#### `solcVersion` + +- **Type**: String +- **Description**: Describes the version of `solc` that will be installed and then used for compilation. Note that if you + are using a compilation platform, such as Foundry or Hardhat, this option does not need to be set. +- **Default**: "" + +#### `exportDirectory` + +- **Type**: String +- **Description**: Describes the directory where all compilation artifacts should be stored after compilation. Leaving it + empty will lead to the compilation artifacts being stored in `crytic-export/`. +- **Default**: "" + +#### `args` + +- **Type**: [String] +- **Description**: Refers to any additional args that one may want to provide to `crytic-compile`. Run `crytic-compile --help` + to view all of its supported flags. For example, if you would like to specify `--compile-force-framework foundry`, the + `args` value will be `"args": ["--compile-force-framework", "foundry"]`. + > 🚩 The `--export-format` and `--export-dir` are already used during compilation with `crytic-compile`. + > Re-using these flags in `args` will cause the compilation to fail. + +### `platformConfig` for `solc` + +#### `target` + +- **Type**: String +- **Description**: Refers to the target that is being compiled. The target must be a single `.sol` file. diff --git a/docs/src/project_configuration/fuzzing_config.md b/docs/src/project_configuration/fuzzing_config.md new file mode 100644 index 00000000..0fa1f1e5 --- /dev/null +++ b/docs/src/project_configuration/fuzzing_config.md @@ -0,0 +1,205 @@ +# Fuzzing Configuration + +The fuzzing configuration defines the parameters for the fuzzing campaign. + +### `workers` + +- **Type**: Integer +- **Description**: The number of worker threads to parallelize fuzzing operations on. +- **Default**: 10 workers + +### `workerResetLimit` + +- **Type**: Integer +- **Description**: The number of call sequences a worker should process on its underlying chain before being fully reset, + freeing memory. After resetting, the worker will be re-created and continue processing of call sequences. + > 🚩 This setting, along with `workers` influence the speed and memory consumption of the fuzzer. Setting this value + > higher will result in greater memory consumption per worker. Setting it too high will result in the in-memory + > chain's database growing to a size that is slower to process. Setting it too low may result in frequent worker resets + > that are computationally expensive for complex contract deployments that need to be replayed during worker reconstruction. +- **Default**: 50 sequences + +### `timeout` + +- **Type**: Integer +- **Description**: The number of seconds before the fuzzing campaign should be terminated. If a zero value is provided, + the timeout will not be enforced. The timeout begins after compilation succeeds and the fuzzing campaign has started. +- **Default**: 0 seconds + +### `testLimit` + +- **Type**: Integer +- **Description**: The number of function calls to make before the fuzzing campaign should be terminated. If a zero value + is provided, no test limit will be enforced. +- **Default**: 0 calls + +### `callSequenceLength` + +- **Type**: Integer +- **Description**: The maximum number of function calls to generate in a single call sequence in the attempt to violate + properties. After every `callSequenceLength` function calls, the blockchain is reset for the next sequence of transactions. +- **Default**: 100 calls/sequence + +### `coverageEnabled` + +- **Type**: Boolean +- **Description**: Whether coverage-increasing call sequences should be saved for the fuzzer to mutate/re-use. + Enabling coverage allows for improved code exploration. +- **Default**: `true` + +### `corpusDirectory` + +- **Type**: String +- **Description**: The file path where the corpus should be saved. The corpus collects sequences during a fuzzing campaign + that help drive fuzzer features (e.g. a call sequence that increases code coverage is stored in the corpus). These sequences + can then be re-used/mutated by the fuzzer during the next fuzzing campaign. +- **Default**: "" + +### `targetContracts` + +- **Type**: [String] (e.g. `[FirstContract, SecondContract, ThirdContract]`) +- **Description**: The list of contracts that will be deployed on the blockchain and then targeted for fuzzing by `medusa`. + For single-contract compilations, this value can be left as `[]`. This, however, is rare since most projects are multi-contract compilations. + > 🚩 Note that the order specified in the array is the _order_ in which the contracts are deployed to the blockchain. + > Thus, if you have a `corpusDirectory` set up, and you change the order of the contracts in the array, the corpus may no + > longer work since the contract addresses of the target contracts will change. This may render the entire corpus useless. +- **Default**: `[]` + +### `targetContractBalances` + +- **Type**: [Base-16 Strings] (e.g. `[0x123, 0x456, 0x789]`) +- **Description**: The starting balance for each contract in `targetContracts`. If the `constructor` for a target contract + is marked `payable`, this configuration option can be used to send ether during contract deployment. Note that this array + has a one-to-one mapping to `targetContracts`. Thus, if `targetContracts` is `[A, B, C]` and `targetContractsBalances` is + `["0", "0xff", "0"]`, then `B` will have a starting balance of 255 wei and `A` and `C` will have zero wei. Note that the wei-value + has to be hex-encoded and _cannot_ have leading zeros. For an improved user-experience, the balances may be encoded as base-10 + format strings in the future. +- **Default**: `[]` + +### `constructorArgs` + +- **Type**: `{"contractName": {"variableName": _value}}` +- **Description**: If a contract in the `targetContracts` has a `constructor` that takes in variables, these can be specified here. + An example can be found [here](#using-constructorargs). +- **Default**: `{}` + +### `deployerAddress` + +- **Type**: Address +- **Description**: The address used to deploy contracts on startup, represented as a hex string. + > 🚩 Changing this address may render entries in the corpus invalid since the addresses of the target contracts will change. +- **Default**: `0x30000` + +### `senderAddresses` + +- **Type**: [Address] +- **Description**: Defines the account addresses used to send function calls to deployed contracts in the fuzzing campaign. + > 🚩 Changing these addresses may render entries in the corpus invalid since the sender(s) of corpus transactions may no + > longer be valid. +- **Default**: `[0x10000, 0x20000, 0x30000]` + +### `blockNumberDelayMax` + +- **Type**: Integer +- **Description**: Defines the maximum block number jump the fuzzer should make between test transactions. The fuzzer + will use this value to make the next block's `block.number` between `[1, blockNumberDelayMax]` more than that of the previous + block. Jumping `block.number` allows `medusa` to enter code paths that require a given number of blocks to pass. +- **Default**: `60_480` + +### `blockTimestampDelayMax` + +- **Type**: Integer +- **Description**: The number of the maximum block timestamp jump the fuzzer should make between test transactions. + The fuzzer will use this value to make the next block's `block.timestamp` between `[1, blockTimestampDelayMax]` more + than that of the previous block. Jumping `block.timestamp`time allows `medusa` to enter code paths that require a given amount of time to pass. +- **Default**: `604_800` + +### `blockGasLimit` + +- **Type**: Integer +- **Description**: The maximum amount of gas a block's transactions can use in total (thus defining max transactions per block). + > 🚩 It is advised not to change this naively, as a minimum must be set for the chain to operate. +- **Default**: `125_000_000` + +### `transactionGasLimit` + +- **Type**: Integer +- **Description**: Defines the amount of gas sent with each fuzzer-generated transaction. + > 🚩 It is advised not to change this naively, as a minimum must be set for the chain to operate. +- **Default**: `12_500_000` + +## Using `constructorArgs` + +There might be use cases where contracts in `targetContracts` have constructors that accept arguments. The `constructorArgs` +configuration option allows you to specify those arguments. `constructorArgs` is a nested dictionary that maps +contract name -> variable name -> variable value. Let's look at an example below: + +```solidity +// This contract is used to test deployment of contracts with constructor arguments. +contract TestContract { + struct Abc { + uint a; + bytes b; + } + + uint x; + bytes2 y; + Abc z; + + constructor(uint _x, bytes2 _y, Abc memory _z) { + x = _x; + y = _y; + z = _z; + } +} + +contract DependentOnTestContract { + address deployed; + + constructor(address _deployed) { + deployed = _deployed; + } +} +``` + +In the example above, we have two contracts `TestContract` and `DependentOnTestContract`. You will note that +`DependentOnTestContract` requires the deployment of `TestContract` _first_ so that it can accept the address of where +`TestContract` was deployed. On the other hand, `TestContract` requires `_x`, `_y`, and `_z`. Here is what the +`constructorArgs` value would look like for the above deployment: + +> **Note**: The example below has removed all the other project configuration options outside of `targetContracts` and +> `constructorArgs` + +```json +{ + "fuzzing": { + "targetContracts": ["TestContract", "DependentOnTestContract"], + "constructorArgs": { + "TestContract": { + "_x": "123456789", + "_y": "0x5465", + "_z": { + "a": "0x4d2", + "b": "0x54657374206465706c6f796d656e74207769746820617267756d656e7473" + } + }, + "DependentOnTestContract": { + "_deployed": "DeployedContract:TestContract" + } + } + } +} +``` + +First, let us look at `targetContracts`. As mentioned in the [documentation for `targetContracts`](#targetcontracts), +the order of the contracts in the array determine the order of deployment. This means that `TestContract` will be +deployed first, which is what we want. + +Now, let us look at `constructorArgs`. `TestContract`'s dictionary specifies the _exact name_ of the constructor argument +(e.g. `_x` or `_y`) with their associated value. Since `_z` is of type `TestContract.Abc`, `_z` is also a dictionary +that specifies each field in the `TestContract.Abc` struct. + +For `DependentOnTestContract`, the `_deployed` key has +a value of `DeployedContract:TestContract`. This tells `medusa` to look for a deployed contract that has the name +`TestContract` and provide its address as the value for `_deployed`. Thus, whenever you need a deployed contract's +address as an argument for another contract, you must follow the format `DeployedContract:`. diff --git a/docs/src/project_configuration/logging_config.md b/docs/src/project_configuration/logging_config.md new file mode 100644 index 00000000..fb0af649 --- /dev/null +++ b/docs/src/project_configuration/logging_config.md @@ -0,0 +1,25 @@ +# Logging Configuration + +The logging configuration defines the parameters for logging to console and/or file. + +### `level` + +- **Type**: String +- **Description**: The log level will determine which logs are emitted or discarded. If `level` is "info" then all logs + with informational level or higher will be logged. The supported values for `level` are "trace", "debug", "info", "warn", "error", + and "panic". +- **Default**: "info" + +### `logDirectory` + +- **Type**: String +- **Description**: Describes what directory log files should be outputted. Have a non-empty `logDirectory` value will + enable "file logging" which will result in logs to be output to both console and file. Note that the directory path is + _relative_ to the directory containing the project configuration file. +- **Default**: "" + +### `noColor` + +- **Type**: Boolean +- **Description**: Disables colored output to console. +- **Default**: `false` diff --git a/docs/src/project_configuration/overview.md b/docs/src/project_configuration/overview.md new file mode 100644 index 00000000..af7d3a46 --- /dev/null +++ b/docs/src/project_configuration/overview.md @@ -0,0 +1,49 @@ +# Configuration Overview + +`medusa`'s project configuration provides extensive and granular control over the execution of the fuzzer. The project +configuration is a `.json` file that is broken down into five core components. + +- [Fuzzing Configuration](./fuzzing_config.md): The fuzzing configuration dictates the parameters with which the fuzzer will execute. +- [Testing Configuration](./testing_config.md): The testing configuration dictates how and what `medusa` should fuzz test. +- [Chain Configuration](./chain_config.md): The chain configuration dictates how `medusa`'s underlying blockchain should be configured. +- [Compilation Configuration](./compilation_config.md): The compilation configuration dictates how to compile the fuzzing target. +- [Logging Configuration](./logging_config.md): The logging configuration dictates when and where to log events. + +To generate a project configuration file, run [`medusa init`](../cli/init.md). + +You can also view this [example project configuration file](../static/medusa.json) for visualization. + +## Recommended Configuration + +A common issue that first-time users face is identifying which configuration options to change. `medusa` provides an +incredible level of flexibility on how the fuzzer should run but this comes with a tradeoff of understanding the nuances +of what configuration options control what feature. Outlined below is a list of configuration options that we recommend +you become familiar with and change before starting to fuzz test. + +> **Note:** Having an [example project configuration file](../static/medusa.json) open will aid in visualizing which +> configuration options to change. + +### `fuzzing.targetContracts` + +Updating this configuration option is **required**! The `targetContracts` configuration option tells `medusa` which contracts +to fuzz test. You can specify one or more contracts for this option which is why it accepts an array +of strings. Let's say you have a fuzz testing contract called `TestStakingContract` that you want to test. +Then, you would set the value of `targetContracts` to `["TestStakingContract"]`. +You can learn more about this option [here](./fuzzing_config.md#targetcontracts). + +### `fuzzing.testLimit` + +Updating test limit is optional but recommended. Test limit determines how many transactions `medusa` will execute before +stopping the fuzzing campaign. By default, the `testLimit` is set to 0. This means that `medusa` will run indefinitely. +While you iterate over your fuzz tests, it is beneficial to have a non-zero value. Thus, it is recommended to update this +value to `10_000` or `100_000` depending on the use case. You can learn more about this option [here](./fuzzing_config.md#testlimit). + +### `fuzzing.corpusDirectory` + +Updating the corpus directory is optional but recommended. The corpus directory determines where corpus items should be +stored on disk. A corpus item is a sequence of transactions that increased `medusa`'s coverage of the system. Thus, these +corpus items are valuable to store so that they can be re-used for the next fuzzing campaign. Additionally, the directory +will also hold [coverage reports](TODO) which is a valuable tool for debugging and validation. For most cases, you may set +`corpusDirectory`'s value to "corpus". This will create a `corpus/` directory in the same directory as the `medusa.json` +file. +You can learn more about this option [here](./fuzzing_config.md#corpusdirectory). diff --git a/docs/src/project_configuration/testing_config.md b/docs/src/project_configuration/testing_config.md new file mode 100644 index 00000000..3adfe08c --- /dev/null +++ b/docs/src/project_configuration/testing_config.md @@ -0,0 +1,178 @@ +# Testing Configuration + +The testing configuration can be broken down into a few subcomponents: + +- **High-level configuration**: Configures global testing parameters, regardless of the type of testing. +- **Assertion testing configuration**: Configures what kind of EVM panics should be treated as a failing fuzz test. +- **Property testing configuration**: Configures what kind of function signatures should be treated as property tests. +- **Optimization testing configuration**: Configures what kind of function signatures should be treated as optimization tests. + +We will go over each subcomponent one-by-one: + +## High-level Configuration + +### `stopOnFailedTest` + +- **Type**: Boolean +- **Description**: Determines whether the fuzzer should stop execution after the first _failed_ test. If `false`, `medusa` + will continue fuzzing until either the [`testLimit`](./fuzzing_config.md#testlimit) is hit, the [`timeout`](./fuzzing_config.md#timeout) + is hit, or the user manually stops execution. +- **Default**: `true` + +### `stopOnFailedContractMatching` + +- **Type**: Boolean +- **Description**: Determines whether the fuzzer should stop execution if it is unable to match the bytecode of a dynamically + deployed contract. A dynamically deployed contract is one that is created during the fuzzing campaign + (versus one that is specified in the [`fuzzing.targetContracts`](./fuzzing_config.md#targetcontracts)). + Here is an example of a dynamically deployed contract: + +```solidity + +contract MyContract { + OtherContract otherContract; + constructor() { + // This is a dynamically deployed contract + otherContract = new otherContract(); + } +} +``` + +- **Default**: `false` + +### `stopOnNoTests` + +- **Type**: Boolean +- **Description**: Determines whether the fuzzer should stop execution if no tests are found + (property tests, assertion tests, optimization tests, or custom API-level tests). If `false` and no tests are found, + `medusa` will continue fuzzing until either the [`testLimit`](./fuzzing_config.md#testlimit) is hit, + the [`timeout`](./fuzzing_config.md#timeout) is hit, or the user manually stops execution. +- **Default**: `true` + +### `testAllContracts` + +- **Type**: Boolean +- **Description**: Determines whether all contracts should be tested (including dynamically deployed ones), rather than + just the contracts specified in the project configuration's [`fuzzing.targetContracts`](./fuzzing_config.md#targetcontracts). +- **Default**: `false` + +### `traceAll`: + +- **Type**: Boolean +- **Description**: Determines whether an [execution trace](TODO) should be attached to each element of a call sequence + that triggered a test failure. +- **Default**: `false` + +## Assertion Testing Configuration + +### `enabled` + +- **Type**: Boolean +- **Description**: Enable or disable assertion testing +- **Default**: `true` + +### `testViewMethods` + +- **Type**: Boolean +- **Description**: Whether `pure` / `view` functions should be tested for assertion failures. + > 🚩 Fuzzing `pure` and `view` functions is not currently implemented. Thus, enabling this option to `true` does not + > update the fuzzer's behavior. +- **Default**: `false` + +### `panicCodeConfig` + +- **Type**: Struct +- **Description**: This struct describes the various types of EVM-level panics that should be considered a "failing case". + By default, only an `assert(false)` is considered a failing case. However, these configuration options would allow a user + to treat arithmetic overflows or division by zero as failing cases as well. + +#### `failOnAssertion` + +- **Type**: Boolean +- **Description**: Triggering an assertion failure (e.g. `assert(false)`) should be treated as a failing case. +- **Default**: `true` + +#### `failOnCompilerInsertedPanic` + +- **Type**: Boolean +- **Description**: Triggering a compiler-inserted panic should be treated as a failing case. +- **Default**: `false` + +#### `failOnArithmeticUnderflow` + +- **Type**: Boolean +- **Description**: Arithmetic underflow or overflow should be treated as a failing case +- **Default**: `false` + +#### `failOnDivideByZero` + +- **Type**: Boolean +- **Description**: Dividing by zero should be treated as a failing case +- **Default**: `false` + +#### `failOnEnumTypeConversionOutOfBounds` + +- **Type**: Boolean +- **Description**: An out-of-bounds enum access should be treated as a failing case +- **Default**: `false` + +#### `failOnIncorrectStorageAccess` + +- **Type**: Boolean +- **Description**: An out-of-bounds storage access should be treated as a failing case +- **Default**: `false` + +#### `failOnPopEmptyArray` + +- **Type**: Boolean +- **Description**: A `pop()` operation on an empty array should be treated as a failing case +- **Default**: `false` + +#### `failOnOutOfBoundsArrayAccess` + +- **Type**: Boolean +- **Description**: An out-of-bounds array access should be treated as a failing case +- **Default**: `false` + +#### `failOnAllocateTooMuchMemory` + +- **Type**: Boolean +- **Description**: Overallocation/excessive memory usage should be treated as a failing case +- **Default**: `false` + +#### `failOnCallUninitializedVariable` + +- **Type**: Boolean +- **Description**: Calling an uninitialized variable should be treated as a failing case +- **Default**: `false` + +## Property Testing Configuration + +### `enabled` + +- **Type**: Boolean +- **Description**: Enable or disable property testing. +- **Default**: `true` + +### `testPrefixes` + +- **Type**: [String] +- **Description**: The list of prefixes that the fuzzer will use to determine whether a given function is a property test or not. + For example, if `property_` is a test prefix, then any function name in the form `property_*` may be a property test. + > **Note**: If you are moving over from Echidna, you can add `echidna_` as a test prefix to quickly port over the property tests from it. +- **Default**: `[property_]` + +## Optimization Testing Configuration + +### `enabled` + +- **Type**: Boolean +- **Description**: Enable or disable optimization testing. +- **Default**: `true` + +### `testPrefixes` + +- **Type**: [String] +- **Description**: The list of prefixes that the fuzzer will use to determine whether a given function is an optimization + test or not. For example, if `optimize_` is a test prefix, then any function name in the form `optimize_*` may be a property test. +- **Default**: `[optimize_]` diff --git a/docs/src/static/contract_deployment.png b/docs/src/static/contract_deployment.png new file mode 100644 index 0000000000000000000000000000000000000000..a021b01cca7460f816cbc3072f2217ab0a8b36e0 GIT binary patch literal 7790 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 SnoP`hyFt1SbZ~bZpZ*7Q<6tHL literal 0 HcmV?d00001 diff --git a/docs/src/static/coverage.png b/docs/src/static/coverage.png new file mode 100644 index 0000000000000000000000000000000000000000..f15676ea08840f0d56c8a2a7ff123ea715d675de GIT binary patch literal 36174 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 literal 0 HcmV?d00001 diff --git a/docs/src/static/custom.css b/docs/src/static/custom.css new file mode 100644 index 00000000..e4565d1a --- /dev/null +++ b/docs/src/static/custom.css @@ -0,0 +1,6 @@ +img[alt="medusa_logo"] { + width: 60%; + margin-left: auto; + margin-right: auto; + display: block; +} diff --git a/docs/src/static/function_level_testing_medusa.json b/docs/src/static/function_level_testing_medusa.json new file mode 100644 index 00000000..79f2aa21 --- /dev/null +++ b/docs/src/static/function_level_testing_medusa.json @@ -0,0 +1,72 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 1000, + "callSequenceLength": 1, + "corpusDirectory": "", + "coverageEnabled": true, + "targetContracts": ["TestDepositContract"], + "targetContractsBalances": ["0xfffffffffffffffffffffffffffffff"], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": ["property_"] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": ["optimize_"] + } + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": "test.sol", + "solcVersion": "", + "exportDirectory": "", + "args": [] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json new file mode 100644 index 00000000..2e8644b6 --- /dev/null +++ b/docs/src/static/medusa.json @@ -0,0 +1,72 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "callSequenceLength": 100, + "corpusDirectory": "", + "coverageEnabled": true, + "targetContracts": [], + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": ["0x10000", "0x20000", "0x30000"], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": true, + "testPrefixes": ["property_"] + }, + "optimizationTesting": { + "enabled": true, + "testPrefixes": ["optimize_"] + } + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": [] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/docs/src/static/medusa_logo.png b/docs/src/static/medusa_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..11c6061e8ee3bfbcbc453e0067f45a2e0ce0b824 GIT binary patch literal 110398 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�H2f!!6P0V8s?lb>mYtf>RoV$uH>Mcx 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

Ze;os+cTGmWQ1(k8)BG~Yku(y=3B!U_;|%=1 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 **Definition**: A random transaction is a call to a random method in one of the target contracts. Any input arguments +> to the method are fuzzed values. + +The second possibility is more nuanced. To understand how to mutate an existing call sequence from the corpus, we need +to first discuss the idea of coverage and what a corpus is. + +### Coverage and the corpus + +Tracking coverage is one of the most powerful features of `medusa`. + +> **Definition**: Coverage is a measure of what parts of the code have been executed by the fuzzer + +Coverage is tracked in a rather simple fashion. For each target contract, we maintain a byte array where the length of the +byte array is equal to the length of that contract's bytecode. If a certain transaction caused us to execute an opcode +that we had not executed before, we increased coverage of that contract. + +![Coverage Tracking Diagram](../static/coverage.png) + +As shown in the figure above, the `CALL` opcode was just executed causing the coverage array's value to be updated at that +index. The next natural question is, how do we harness this information to improve the fuzzer? + +This is where the idea of a **corpus** comes in. + +> **Definition**: The corpus is a structure that holds "interesting" or "coverage-increasing" call sequences. + +Thus, when `medusa` runs, if it finds a call sequence that increased its coverage of the system, it will add it to the corpus. +These call sequences are invaluable to `medusa` because they allowed it to explore a larger portion of the system. This is +what makes `medusa` a **coverage-guided fuzzer**. + +> **Definition**: A coverage-guided fuzzer is one that aims to maximize its coverage of the system. + +Tracking coverage and storing coverage-increasing sequences in the corpus also allows `medusa` to re-use these sequences. +This takes us back to the second possibility when generating call sequences: mutating an existing sequence from the corpus. + +The reason we re-use call sequences is that we know that the call sequence in question improved our coverage. So, we +might as well re-use it, **mutate** it, and then execute that mutated call sequence in hopes of further increasing our coverage. +There are a variety of mutational strategies that `medusa` employs. For example, `medusa` can take a call sequence from the corpus and append a new random +transaction at the end of it. This is called **mutational fuzzing**. + +> **Definition**: Mutational fuzzing is the practice of taking existing data samples and generating new variants of them +> (mutants). + +Now that we know what a call sequence is, how to generate them, and how to track coverage, we can finally discuss how +these call sequences are executed. + +### Executing the call sequence + +Call sequence execution happens in an _iterative_ fashion. Here is some pseudocode on how it happens: + +``` +# Generate a new call sequence or mutate one from the corpus +sequence = generator.NewCallSequence() + +# Iteratively execute each call in the call sequence +for i < len(sequence) { + # Retrieve the i-th element in the sequence + tx = sequence[i] + + # Run the transaction on the blockchain and retrieve the result + result = blockchain.executeTransaction(tx) + + # Update coverage + increasedCoverage = coverageTracker.updateCoverage() + + # If coverage increased, add sequence[:i+1] to the corpus + if increasedCoveraged { + corpus.addCallSequence(tx[:i+1]) + } + + # Check for invariant failures + encounteredFailure = tester.checkForInvariantFailures(result) + + # Let user know we had a failing test case + if encounteredFailure { + reportFailedTestCase() + } +} +``` + +The one portion of the above pseudocode that we did not discuss is checking for invariant failures. We will discuss +the different types of invariants and what an invariant failure means in the [next chapter](./invariants.md). + +## Resetting the blockchain + +The final step in the fuzzing lifecycle is resetting the blockchain. Resetting the blockchain is as simple as reverting +to the "initial deployment state" of the blockchain. Once we reset back to the "initial deployment state", we can now generate and execute +another call sequence! diff --git a/docs/src/testing/invariants.md b/docs/src/testing/invariants.md new file mode 100644 index 00000000..4994c81b --- /dev/null +++ b/docs/src/testing/invariants.md @@ -0,0 +1,69 @@ +# Types of Invariants + +As discussed in the [testing overview](./overview.md) chapter, invariants describe the "truths" of your system. These +are unchanging properties that arise from the design of a codebase. + +> **Note**: We will interchange the use of the word property and invariant often. For all intents and purposes, they +> mean the same thing. + +Defining and testing your invariants is critical to assessing the **expected system behavior**. + +We like to break down invariants into two general categories: function-level invariants and system-level invariants. +Note that there are other ways of defining and scoping invariants, but this distinction is generally sufficient to +start fuzz testing even the most complex systems. + +## Function-level invariants + +A function-level invariant can be defined as follows: + +> **Definition**: A function-level invariant is a property that arises from the execution of a specific function. + +Let's take the following function from a smart contract: + +```solidity +function deposit() public payable { + // Make sure that the total deposited amount does not exceed the limit + uint256 amount = msg.value; + require(totalDeposited + amount <= MAX_DEPOSIT_AMOUNT); + + // Update the user balance and total deposited + balances[msg.sender] += amount; + totalDeposited += amount; + + emit Deposit(msg.sender, amount, totalDeposited); +} +``` + +The `deposit` function has the following function-level invariants: + +1. The ETH balance of `msg.sender` must decrease by `amount`. +2. The ETH of `address(this)` must increase by `amount`. +3. `balances[msg.sender]` should increase by `amount`. +4. The `totalDeposited` value should increase by `amount`. + +Note that there other properties that can also be tested for but the above should highlight what a function-level +invariant is. In general, function-level invariants can be identified by assessing what must be true _before_ the execution +of a function and what must be true _after_ the execution of that same function. In the next chapter, we will write a +fuzz test to test the `deposit` function and how to use medusa to run that test. + +Let's now look at system-level invariants. + +## System-level invariants + +A system-level invariant can be defined as follows: + +> **Definition**: A system-level invariant is a property that holds true across the _entire_ execution of a system + +Thus, a system-level invariant is a lot more generalized than a function-level invariant. Here are two common examples +of a function-level invariant: + +1. The `xy=k` constant product formula should always hold for Uniswap pools +2. No user's balance should ever exceed the total supply for an ERC20 token. + +In the `deposit` function above, we also see the presence of a system-level invariant: + +**The `totalDeposited` amount should always be less than or equal to the `MAX_DEPOSIT_AMOUNT`**. + +Since the `totalDeposited` value can be affected by the presence of other functions in the system +(e.g. `withdraw` or `stake`), it is best tested at the system level instead of the function level. We will look at how +to write system-level invariants in the [Writing System-Level Invariants](./writing-system-level-invariants.md) chapter. diff --git a/docs/src/testing/overview.md b/docs/src/testing/overview.md new file mode 100644 index 00000000..0ee8181c --- /dev/null +++ b/docs/src/testing/overview.md @@ -0,0 +1,26 @@ +# Testing Overview + +This chapter discusses the overarching goal of smart contract fuzzing. + +Traditional fuzz testing (e.g. with [`AFL`](https://lcamtuf.coredump.cx/afl/)) aims to generally explore a binary by providing +random inputs in an effort to identify new system states or crash the program (please note that this is a pretty crude generalization). +This model, however, does not translate to the smart contract ecosystem since you cannot cause a smart contract to "crash". +A transaction that reverts, for example, is not equivalent to a binary crashing or panicking. + +Thus, with smart contracts, we have to change the fuzzing paradigm. When you hear of "fuzzing smart contracts", you are +not trying to crash the program but, instead, you are trying to validate the **invariants** of the program. + +> **Definition**: An invariant is a property that remains unchanged after one or more operations are applied to it. + +More generally, an invariant is a "truth" about some system. For smart contracts, this can take many faces. + +1. **Mathematical invariants**: `a + b = b + a`. The commutative property is an invariant and any Solidity math library + should uphold this property. +2. **ERC20 tokens**: The sum of all user balances should never exceed the total supply of the token. +3. **Automated market maker (e.g. Uniswap)**: `xy = k`. The constant-product formula is an invariant that maintains the + economic guarantees of AMMs such as Uniswap. + +> **Definition**: Smart contract fuzzing uses random sequences of transactions to test the invariants of the smart contract system. + +Before we explore how to identify, write, and test invariants, it is beneficial to understand how smart contract fuzzing +works under-the-hood. diff --git a/docs/src/testing/tips.md b/docs/src/testing/tips.md new file mode 100644 index 00000000..a2de2928 --- /dev/null +++ b/docs/src/testing/tips.md @@ -0,0 +1,32 @@ +## Tips for Testing with Medusa + +### General + +- **Use multiple testing modes:** Medusa supports property testing, assertion testing, and optimization testing. Use a combination of modes to thoroughly test your contracts. +- **Write clear and concise tests:** Your tests should be easy to read and understand. Avoid complex logic or unnecessary code. +- **Test edge cases:** Consider testing extreme values and unusual inputs to ensure your contracts handle them correctly. +- **Use a variety of test inputs:** Generate a diverse set of test inputs to cover a wide range of scenarios. +- **Monitor gas consumption:** Medusa can track gas consumption during testing. Use this information to identify areas where your contracts can be optimized. + +### Property Testing + +- **Choose meaningful properties:** The properties you test should be important invariants of your contract. + +### Assertion Testing + +- **Use assertions judiciously:** Assertions can be useful for catching errors, but they can also slow down testing. Use them only when necessary. +- **Test for both valid and invalid inputs:** Ensure your assertions check for both valid and invalid inputs to thoroughly test your contract's behavior. +- **Use pre-conditions and post-conditions to verify the state of the contract before and after a function call.:** Pre-conditions and post-conditions are assertions that can be used to verify the state of the contract before and after a function call. This can help to ensure that the function is called with the correct inputs, that it produces the expected outputs, and that the state of the contract is valid. + +### Optimization Testing + +- **Choose a meaningful optimization goal:** The goal of your optimization test should be to maximize a specific metric, such as the return value of a function. +- **Use a variety of optimization techniques:** Medusa supports multiple optimization techniques, such as genetic algorithms and simulated annealing. Consider using different techniques to find the best solution. + +### Additional Tips + +- **Use a configuration file:** A configuration file allows you to customize Medusa's behavior and specify additional testing parameters. +- **Use corpus and coverage information to improve the effectiveness of your fuzzing campaigns:** Corpus and coverage information can be used to improve the effectiveness of your fuzzing campaigns by providing feedback on the quality of the test inputs. +- **Run Medusa in parallel:** Medusa can run tests in parallel to speed up the testing process. +- **Review the test results carefully:** Medusa provides detailed test results. Take the time to review them carefully and identify any potential issues. +- **Use Medusa as part of your development process:** Integrate Medusa into your development workflow to regularly test your contracts and identify potential bugs early on. diff --git a/docs/src/testing/writing-function-level-invariants.md b/docs/src/testing/writing-function-level-invariants.md new file mode 100644 index 00000000..ebe4e856 --- /dev/null +++ b/docs/src/testing/writing-function-level-invariants.md @@ -0,0 +1,145 @@ +## Writing Function-Level Invariants + +This chapter will walk you through writing function-level fuzz tests for the `deposit` function that we saw in the [previous chapter](./invariants.md#function-level-invariants). + +Before we write the fuzz tests, let's look into how we would write a unit test for the `deposit` function: + +```solidity +function testDeposit() public { + // The amount of tokens to deposit + uint256 amount = 10 ether; + + // Retrieve balance of user before deposit + preBalance = depositContract.balances(address(this)); + + // Call the deposit contract (let's assume this contract has 10 ether) + depositContract.deposit{value: amount}(); + + // Assert post-conditions + assert(depositContract.balances(msg.sender) == preBalance + amount); + // Add other assertions here +} +``` + +What we will notice about the test above is that it _fixes_ the value that is being sent. It is unable to test how the +`deposit` function behaves across a variety of input spaces. Thus, a function-level fuzz test can be thought of as a +"unit test on steroids". Instead of fixing the `amount`, we let the fuzzer control the `amount` value to any number between +`[0, type(uint256).max]` and see how the system behaves to that. + +> **Note**: One of the core differences between a traditional unit test versus a fuzz test is that a fuzz test accepts input arguments that the fuzzer can control. + +### Writing a Fuzz Test for the `deposit` Function + +Here is what a fuzz test for the `deposit` function would look like: + +```solidity +function testDeposit(uint256 _amount) public { + // Let's bound the input to be _at most_ the ETH balance of this contract + // The amount value will now in between [0, address(this).balance] + uint256 amount = clampLte(_amount, address(this).balance); + + // Retrieve balance of user before deposit + uint256 preBalance = depositContract.balances(address(this)); + + // Call the deposit contract with a variable amount + depositContract.deposit{value: _amount}(); + + // Assert post-conditions + assert(depositContract.balances(address(this)) == preBalance + amount); + // Add other assertions here +} +``` + +Notice that we bounded the `_amount` variable to be less than or equal to the test contract's ETH balance. +This type of bounding is very common when writing fuzz tests. Bounding allows you to only test values that are reasonable. +If `address(this)` doesn't have enough ETH, it does not make sense to try and call the `deposit` function. Additionally, +although we only tested one of the function-level invariants from the [previous chapter](./invariants.md), writing the remaining +would follow a similar pattern as the one written above. + +## Running a function-level test with medusa + +Let's now run the above example with medusa. Here is the test code: + +```solidity +contract DepositContract { + // @notice MAX_DEPOSIT_AMOUNT is the maximum amount that can be deposited into this contract + uint256 public constant MAX_DEPOSIT_AMOUNT = 1_000_000e18; + + // @notice balances holds user balances + mapping(address => uint256) public balances; + + // @notice totalDeposited represents the current deposited amount across all users + uint256 public totalDeposited; + + // @notice Deposit event is emitted after a deposit occurs + event Deposit(address depositor, uint256 amount, uint256 totalDeposited); + + // @notice deposit allows user to deposit into the system + function deposit() public payable { + // Make sure that the total deposited amount does not exceed the limit + uint256 amount = msg.value; + require(totalDeposited + amount <= MAX_DEPOSIT_AMOUNT); + + // Update the user balance and total deposited + balances[msg.sender] += amount; + totalDeposited += amount; + + emit Deposit(msg.sender, amount, totalDeposited); + } +} + +contract TestDepositContract { + + // @notice depositContract is an instance of DepositContract + DepositContract depositContract; + + constructor() payable { + // Deploy the deposit contract + depositContract = new DepositContract(); + } + + // @notice testDeposit tests the DepositContract.deposit function + function testDeposit(uint256 _amount) public { + // Let's bound the input to be _at most_ the ETH balance of this contract + // The amount value will now in between [0, address(this).balance] + uint256 amount = clampLte(_amount, address(this).balance); + + // Retrieve balance of user before deposit + uint256 preBalance = depositContract.balances(address(this)); + + // Call the deposit contract with a variable amount + depositContract.deposit{value: _amount}(); + + // Assert post-conditions + assert(depositContract.balances(address(this)) == preBalance + amount); + // Add other assertions here + } + + // @notice clampLte returns a value between [a, b] + function clampLte(uint256 a, uint256 b) internal returns (uint256) { + if (!(a <= b)) { + uint256 value = a % (b + 1); + return value; + } + return a; + } + +} +``` + +To run this test contract, download the project configuration file [here](../static/function_level_testing_medusa.json), +rename it to `medusa.json`, and run: + +``` +medusa fuzz --config medusa.json +``` + +The following changes were made to the default project configuration file to allow this test to run: + +- `fuzzing.targetContracts`: The `fuzzing.targetContracts` value was updated to `["TestDepositContract"]`. +- `fuzzing.targetContractsBalances`: The `fuzzing.targetContractsBalances` was updated to `["0xfffffffffffffffffffffffffffffff"]` + to allow the `TestDepositContract` contract to have an ETH balance allowing the fuzzer to correctly deposit funds into the + `DepositContract`. +- `fuzzing.testLimit`: The `fuzzing.testLimit` was set to `1_000` to shorten the duration of the fuzzing campign. +- `fuzzing.callSequenceLength`: The `fuzzing.callSequenceLength` was set to `1` so that the `TestDepositContract` can be + reset with its full ETH balance after each transaction. diff --git a/docs/src/testing/writing-system-level-invariants.md b/docs/src/testing/writing-system-level-invariants.md new file mode 100644 index 00000000..5cf2b0e1 --- /dev/null +++ b/docs/src/testing/writing-system-level-invariants.md @@ -0,0 +1,3 @@ +## Writing System-Level Invariants with Medusa + +WIP diff --git a/docs/src/testing/writing-tests.md b/docs/src/testing/writing-tests.md new file mode 100644 index 00000000..86da3ec6 --- /dev/null +++ b/docs/src/testing/writing-tests.md @@ -0,0 +1,189 @@ +# Testing with `medusa` + +`medusa`, like Echidna, supports the following testing modes: + +1. [Property Mode](https://secure-contracts.com/program-analysis/echidna/introduction/how-to-test-a-property.html) +2. [Assertion Mode](https://secure-contracts.com/program-analysis/echidna/basic/assertion-checking.html) +3. [Optimization Mode](https://secure-contracts.com/program-analysis/echidna/advanced/optimization_mode.html) + +For more advanced information and documentation on how the various modes work and their pros/cons, check out [secure-contracts.com](https://secure-contracts.com/program-analysis/echidna/index.html) + +## Writing property tests + +Property tests are represented as functions within a Solidity contract whose names are prefixed with a prefix specified by the `testPrefixes` configuration option (`fuzz_` is the default test prefix). Additionally, they must take no arguments and return a `bool` indicating if the test succeeded. + +```solidity +contract TestXY { + uint x; + uint y; + + function setX(uint value) public { + x = value + 3; + } + + function setY(uint value) public { + y = value + 9; + } + + function fuzz_never_specific_values() public returns (bool) { + // ASSERTION: x should never be 10 at the same time y is 80 + return !(x == 10 && y == 80); + } +} +``` + +`medusa` deploys your contract containing property tests and generates a sequence of calls to execute against all publicly accessible methods. After each function call, it calls upon your property tests to ensure they return a `true` (success) status. + +### Testing in property-mode + +To begin a fuzzing campaign in property-mode, you can run `medusa fuzz` or `medusa fuzz --config [config_path]`. + +> **Note**: Learn more about running `medusa` with its CLI [here](./Command-Line-Interface.md). + +Invoking this fuzzing campaign, `medusa` will: + +- Compile the given targets +- Start the configured number of worker threads, each with their own local Ethereum test chain. +- Deploy all contracts to each worker's test chain. +- Begin to generate and send call sequences to update contract state. +- Check property tests all succeed after each call executed. + +Upon discovery of a failed property test, `medusa` will halt, reporting the call sequence used to violate any property test(s): + +``` +[FAILED] Property Test: TestXY.fuzz_never_specific_values() +Test "TestXY.fuzz_never_specific_values()" failed after the following call sequence: +1) TestXY.setY([71]) (gas=4712388, gasprice=1, value=0, sender=0x2222222222222222222222222222222222222222) +2) TestXY.setX([7]) (gas=4712388, gasprice=1, value=0, sender=0x3333333333333333333333333333333333333333) +``` + +## Writing assertion tests + +Although both property-mode and assertion-mode try to validate / invalidate invariants of the system, they do so in different ways. In property-mode, `medusa` will look for functions with a specific test prefix (e.g. `fuzz_`) and test those. In assertion-mode, `medusa` will test to see if a given call sequence can cause the Ethereum Virtual Machine (EVM) to "panic". The EVM has a variety of panic codes for different scenarios. For example, there is a unique panic code when an `assert(x)` statement returns `false` or when a division by zero is encountered. In assertion mode, which panics should or should not be treated as "failing test cases" can be toggled by updating the [Project Configuration](./Project-Configuration.md#fuzzing-configuration). By default, only `FailOnAssertion` is enabled. Check out the [Example Project Configuration File](https://github.com/crytic/medusa/wiki/Example-Project-Configuration-File) for a visualization of the various panic codes that can be enabled. An explanation of the various panic codes can be found in the [Solidity documentation](https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require). + +Please note that the behavior of assertion mode is different between `medusa` and Echidna. Echidna will only test for `assert(x)` statements while `medusa` provides additional flexibility. + +```solidity +contract TestContract { + uint x; + uint y; + + function setX(uint value) public { + x = value; + + // ASSERTION: x should be an even number + assert(x % 2 == 0); + } +} +``` + +During a call sequence, if `setX` is called with a `value` that breaks the assertion (e.g. `value = 3`), `medusa` will treat this as a failing property and report it back to the user. + +### Testing in assertion-mode + +To begin a fuzzing campaign in assertion-mode, you can run `medusa fuzz --assertion-mode` or `medusa fuzz --config [config_path] --assertion-mode`. + +> **Note**: Learn more about running `medusa` with its CLI [here](./Command-Line-Interface.md). + +Invoking this fuzzing campaign, `medusa` will: + +- Compile the given targets +- Start the configured number of worker threads, each with their own local Ethereum test chain. +- Deploy all contracts to each worker's test chain. +- Begin to generate and send call sequences to update contract state. +- Check to see if there any failing assertions after each call executed. + +Upon discovery of a failed assertion, `medusa` will halt, reporting the call sequence used to violate any assertions: + +``` +Fuzzer stopped, test results follow below ... +[FAILED] Assertion Test: TestContract.setX(uint256) +Test for method "TestContract.setX(uint256)" failed after the following call sequence resulted in an assertion: +1) TestContract.setX([102552480437485684723695021980667056378352338398148431990087576385563741034353]) (block=2, time=4, gas=12500000, gasprice=1, value=0, sender=0x1111111111111111111111111111111111111111) +``` + +## Writing optimization tests + +Optimization mode's goal is not to validate/invalidate properties but instead to maximize the return value of a function. Similar to property mode, these functions must be prefixed with a prefix specified by the `testPrefixes` configuration option (`optimize_` is the default test prefix). Additionally, they must take no arguments and return an `int256`. A good use case for optimization mode is to try to quantify the impact of a bug (e.g. a rounding error). + +```solidity +contract TestContract { + int256 input; + + function set(int256 _input) public { + input = _input; + } + + function optimize_opt_linear() public view returns (int256) { + if (input > -4242) return -input; + else return 0; + } +} +``` + +`medusa` deploys your contract containing optimization tests and generates a sequence of calls to execute against all publicly accessible methods. After each function call, it calls upon your otpimization tests to identify whether the return value of those tests are greater than the currently stored values. + +### Testing in optimization-mode + +To begin a fuzzing campaign in optimization-mode, you can run `medusa fuzz --optimization-mode` or `medusa fuzz --config [config_path] --optimization-mode`. + +> **Note**: Learn more about running `medusa` with its CLI [here](./Command-Line-Interface.md). + +Invoking this fuzzing campaign, `medusa` will: + +- Compile the given targets +- Start the configured number of worker threads, each with their own local Ethereum test chain. +- Deploy all contracts to each worker's test chain. +- Begin to generate and send call sequences to update contract state. +- Check to see if the return value of the optimization test is greater than the cached value. + - If the value is greater, update the cached value. + +Once the test limit or timeout for the fuzzing campaign has been reached, `medusa` will halt and report the call sequence that maximized the return value of the function: + +``` +Fuzzer stopped, test results follow below ... +[PASSED] Optimization Test: TestContract.optimize_opt_linear() +Optimization test "TestContract.optimize_opt_linear()" resulted in the maximum value: 4241 with the following sequence: +1) TestContract.set(-4241) (block=2, time=3, gas=12500000, gasprice=1, value=0, sender=0x0000000000000000000000000000000000010000) +``` + +## Testing with multiple modes + +Note that we can run `medusa` with one, many, or no modes enabled. Running `medusa fuzz --assertion-mode --optimization-mode` will run all three modes at the same time, since property-mode is enabled by default. If a project configuration file is used, any combination of the three modes can be toggled. In fact, all three modes can be disabled and `medusa` will still run. Please review the [Project Configuration](./Project-Configuration.md) wiki page and the [Project Configuration Example](/Example-Project-Configuration-File.md) for more information. + +```solidity +contract TestContract { + int256 input; + + function set(int256 _input) public { + input = _input; + } + + function failing_assert_method(uint value) public { + // ASSERTION: We always fail when you call this function. + assert(false); + } + + function fuzz_failing_property() public view returns (bool) { + // ASSERTION: fail immediately. + return false; + } + + function optimize_opt_linear() public view returns (int256) { + if (input > -4242) return -input; + else return 0; + } +} +``` + +Invoking a fuzzing campaign with `medusa fuzz --assertion-mode --optimization-mode` (note all three modes are enabled), `medusa` will: + +- Compile the given targets +- Start the configured number of worker threads, each with their own local Ethereum test chain. +- Deploy all contracts to each worker's test chain. +- Begin to generate and send call sequences to update contract state. +- Check to see: + - If property tests all succeed after each call executed. + - If a panic (which was enabled in the project configuration) has been triggered after each call. + - Whether the return value of the optimization test is greater than the cached value. + - Update the cached value if it is greater. diff --git a/docs/theme/favicon.png b/docs/theme/favicon.png new file mode 100755 index 0000000000000000000000000000000000000000..a72d998fb92c1a12c25604c6507712ee3657ce2c GIT binary patch literal 121411 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 z\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 L_@hK0AN&6RV$zYK literal 0 HcmV?d00001 diff --git a/docs/theme/favicon.svg b/docs/theme/favicon.svg new file mode 100755 index 00000000..78259cf9 --- /dev/null +++ b/docs/theme/favicon.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/theme/highlight.js b/docs/theme/highlight.js new file mode 100644 index 00000000..8d926897 --- /dev/null +++ b/docs/theme/highlight.js @@ -0,0 +1,6 @@ +/* + Highlight.js 10.1.1 (93fd0d73) + License: BSD-3-Clause + Copyright (c) 2006-2020, Ivan Sagalaev +*/ +var hljs=function(){"use strict";function e(n){Object.freeze(n);var t="function"==typeof n;return Object.getOwnPropertyNames(n).forEach((function(r){!Object.hasOwnProperty.call(n,r)||null===n[r]||"object"!=typeof n[r]&&"function"!=typeof n[r]||t&&("caller"===r||"callee"===r||"arguments"===r)||Object.isFrozen(n[r])||e(n[r])})),n}class n{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}ignoreMatch(){this.ignore=!0}}function t(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...n){var t={};for(const n in e)t[n]=e[n];return n.forEach((function(e){for(const n in e)t[n]=e[n]})),t}function a(e){return e.nodeName.toLowerCase()}var i=Object.freeze({__proto__:null,escapeHTML:t,inherit:r,nodeStream:function(e){var n=[];return function e(t,r){for(var i=t.firstChild;i;i=i.nextSibling)3===i.nodeType?r+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:r,node:i}),r=e(i,r),a(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:r,node:i}));return r}(e,0),n},mergeStreams:function(e,n,r){var i=0,s="",o=[];function l(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){s+=""}function d(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var g=l();if(s+=t(r.substring(i,g[0].offset)),i=g[0].offset,g===e){o.reverse().forEach(u);do{d(g.splice(0,1)[0]),g=l()}while(g===e&&g.length&&g[0].offset===i);o.reverse().forEach(c)}else"start"===g[0].event?o.push(g[0].node):o.pop(),d(g.splice(0,1)[0])}return s+t(r.substr(i))}});const s="",o=e=>!!e.kind;class l{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=t(e)}openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){o(e)&&(this.buffer+=s)}value(){return this.buffer}span(e){this.buffer+=``}}class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{c._collapse(e)}))}}class u extends c{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new l(this,this.options).value()}finalize(){return!0}}function d(e){return e?"string"==typeof e?e:e.source:null}const g="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",h={begin:"\\\\[\\s\\S]",relevance:0},f={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[h]},p={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[h]},b={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},m=function(e,n,t={}){var a=r({className:"comment",begin:e,end:n,contains:[]},t);return a.contains.push(b),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},v=m("//","$"),x=m("/\\*","\\*/"),E=m("#","$");var _=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:g,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=function(...e){return e.map(e=>d(e)).join("")}(n,/.*\b/,e.binary,/\b.*/)),r({className:"meta",begin:n,end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},BACKSLASH_ESCAPE:h,APOS_STRING_MODE:f,QUOTE_STRING_MODE:p,PHRASAL_WORDS_MODE:b,COMMENT:m,C_LINE_COMMENT_MODE:v,C_BLOCK_COMMENT_MODE:x,HASH_COMMENT_MODE:E,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:g,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[h,{begin:/\[/,end:/\]/,relevance:0,contains:[h]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{n.data._beginMatch!==e[1]&&n.ignoreMatch()}})}}),N="of and for in not or if then".split(" ");function w(e,n){return n?+n:function(e){return N.includes(e.toLowerCase())}(e)?0:1}const R=t,y=r,{nodeStream:k,mergeStreams:O}=i,M=Symbol("nomatch");return function(t){var a=[],i={},s={},o=[],l=!0,c=/(^(<[^>]+>|\t|)+|\n)/gm,g="Could not find the language '{}', did you forget to load/include a language module?";const h={disableAutodetect:!0,name:"Plain text",contains:[]};var f={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:u};function p(e){return f.noHighlightRe.test(e)}function b(e,n,t,r){var a={code:n,language:e};S("before:highlight",a);var i=a.result?a.result:m(a.language,a.code,t,r);return i.code=a.code,S("after:highlight",i),i}function m(e,t,a,s){var o=t;function c(e,n){var t=E.case_insensitive?n[0].toLowerCase():n[0];return Object.prototype.hasOwnProperty.call(e.keywords,t)&&e.keywords[t]}function u(){null!=y.subLanguage?function(){if(""!==A){var e=null;if("string"==typeof y.subLanguage){if(!i[y.subLanguage])return void O.addText(A);e=m(y.subLanguage,A,!0,k[y.subLanguage]),k[y.subLanguage]=e.top}else e=v(A,y.subLanguage.length?y.subLanguage:null);y.relevance>0&&(I+=e.relevance),O.addSublanguage(e.emitter,e.language)}}():function(){if(!y.keywords)return void O.addText(A);let e=0;y.keywordPatternRe.lastIndex=0;let n=y.keywordPatternRe.exec(A),t="";for(;n;){t+=A.substring(e,n.index);const r=c(y,n);if(r){const[e,a]=r;O.addText(t),t="",I+=a,O.addKeyword(n[0],e)}else t+=n[0];e=y.keywordPatternRe.lastIndex,n=y.keywordPatternRe.exec(A)}t+=A.substr(e),O.addText(t)}(),A=""}function h(e){return e.className&&O.openNode(e.className),y=Object.create(e,{parent:{value:y}})}function p(e){return 0===y.matcher.regexIndex?(A+=e[0],1):(L=!0,0)}var b={};function x(t,r){var i=r&&r[0];if(A+=t,null==i)return u(),0;if("begin"===b.type&&"end"===r.type&&b.index===r.index&&""===i){if(A+=o.slice(r.index,r.index+1),!l){const n=Error("0 width match regex");throw n.languageName=e,n.badRule=b.rule,n}return 1}if(b=r,"begin"===r.type)return function(e){var t=e[0],r=e.rule;const a=new n(r),i=[r.__beforeBegin,r["on:begin"]];for(const n of i)if(n&&(n(e,a),a.ignore))return p(t);return r&&r.endSameAsBegin&&(r.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),r.skip?A+=t:(r.excludeBegin&&(A+=t),u(),r.returnBegin||r.excludeBegin||(A=t)),h(r),r.returnBegin?0:t.length}(r);if("illegal"===r.type&&!a){const e=Error('Illegal lexeme "'+i+'" for mode "'+(y.className||"")+'"');throw e.mode=y,e}if("end"===r.type){var s=function(e){var t=e[0],r=o.substr(e.index),a=function e(t,r,a){let i=function(e,n){var t=e&&e.exec(n);return t&&0===t.index}(t.endRe,a);if(i){if(t["on:end"]){const e=new n(t);t["on:end"](r,e),e.ignore&&(i=!1)}if(i){for(;t.endsParent&&t.parent;)t=t.parent;return t}}if(t.endsWithParent)return e(t.parent,r,a)}(y,e,r);if(!a)return M;var i=y;i.skip?A+=t:(i.returnEnd||i.excludeEnd||(A+=t),u(),i.excludeEnd&&(A=t));do{y.className&&O.closeNode(),y.skip||y.subLanguage||(I+=y.relevance),y=y.parent}while(y!==a.parent);return a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),h(a.starts)),i.returnEnd?0:t.length}(r);if(s!==M)return s}if("illegal"===r.type&&""===i)return 1;if(B>1e5&&B>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return A+=i,i.length}var E=T(e);if(!E)throw console.error(g.replace("{}",e)),Error('Unknown language: "'+e+'"');var _=function(e){function n(n,t){return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=function(e){return RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map(e=>e[1]);this.matcherRe=n(function(e,n="|"){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+=n),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"===l[0][0]&&l[1]?a+="\\"+(+l[1]+s):(a+=l[0],"("===l[0]&&r++)}a+=")"}return a}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const n=this.matcherRe.exec(e);if(!n)return null;const t=n.findIndex((e,n)=>n>0&&void 0!==e),r=this.matchIndexes[t];return n.splice(0,t),Object.assign(n,r)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;const t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function i(e,n){const t=e.input[e.index-1],r=e.input[e.index+e[0].length];"."!==t&&"."!==r||n.ignoreMatch()}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return function t(s,o){const l=s;if(s.compiled)return l;s.compiled=!0,s.__beforeBegin=null,s.keywords=s.keywords||s.beginKeywords;let c=null;if("object"==typeof s.keywords&&(c=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=function(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach((function(n){r(n,e[n])})),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach((function(n){var r=n.split("|");t[r[0]]=[e,w(r[0],r[1])]}))}}(s.keywords,e.case_insensitive)),s.lexemes&&c)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l.keywordPatternRe=n(s.lexemes||c||/\w+/,!0),o&&(s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",s.__beforeBegin=i),s.begin||(s.begin=/\B|\b/),l.beginRe=n(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(l.endRe=n(s.end)),l.terminator_end=d(s.end)||"",s.endsWithParent&&o.terminator_end&&(l.terminator_end+=(s.end?"|":"")+o.terminator_end)),s.illegal&&(l.illegalRe=n(s.illegal)),void 0===s.relevance&&(s.relevance=1),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(e){return function(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map((function(n){return r(e,{variants:null},n)}))),e.cached_variants?e.cached_variants:function e(n){return!!n&&(n.endsWithParent||e(n.starts))}(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e}("self"===e?s:e)}))),s.contains.forEach((function(e){t(e,l)})),s.starts&&t(s.starts,o),l.matcher=function(e){const n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(l),l}(e)}(E),N="",y=s||_,k={},O=new f.__emitter(f);!function(){for(var e=[],n=y;n!==E;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>O.openNode(e))}();var A="",I=0,S=0,B=0,L=!1;try{for(y.matcher.considerAll();;){B++,L?L=!1:(y.matcher.lastIndex=S,y.matcher.considerAll());const e=y.matcher.exec(o);if(!e)break;const n=x(o.substring(S,e.index),e);S=e.index+n}return x(o.substr(S)),O.closeAllNodes(),O.finalize(),N=O.toHTML(),{relevance:I,value:N,language:e,illegal:!1,emitter:O,top:y}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:o.slice(S-100,S+100),mode:n.mode},sofar:N,relevance:0,value:R(o),emitter:O};if(l)return{illegal:!1,relevance:0,value:R(o),emitter:O,language:e,top:y,errorRaised:n};throw n}}function v(e,n){n=n||f.languages||Object.keys(i);var t=function(e){const n={relevance:0,emitter:new f.__emitter(f),value:R(e),illegal:!1,top:h};return n.emitter.addText(e),n}(e),r=t;return n.filter(T).filter(I).forEach((function(n){var a=m(n,e,!1);a.language=n,a.relevance>r.relevance&&(r=a),a.relevance>t.relevance&&(r=t,t=a)})),r.language&&(t.second_best=r),t}function x(e){return f.tabReplace||f.useBR?e.replace(c,e=>"\n"===e?f.useBR?"
":e:f.tabReplace?e.replace(/\t/g,f.tabReplace):e):e}function E(e){let n=null;const t=function(e){var n=e.className+" ";n+=e.parentNode?e.parentNode.className:"";const t=f.languageDetectRe.exec(n);if(t){var r=T(t[1]);return r||(console.warn(g.replace("{}",t[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?t[1]:"no-highlight"}return n.split(/\s+/).find(e=>p(e)||T(e))}(e);if(p(t))return;S("before:highlightBlock",{block:e,language:t}),f.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e;const r=n.textContent,a=t?b(t,r,!0):v(r),i=k(n);if(i.length){const e=document.createElement("div");e.innerHTML=a.value,a.value=O(i,k(e),r)}a.value=x(a.value),S("after:highlightBlock",{block:e,result:a}),e.innerHTML=a.value,e.className=function(e,n,t){var r=n?s[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),e.includes(r)||a.push(r),a.join(" ").trim()}(e.className,t,a.language),e.result={language:a.language,re:a.relevance,relavance:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance,relavance:a.second_best.relevance})}const N=()=>{if(!N.called){N.called=!0;var e=document.querySelectorAll("pre code");a.forEach.call(e,E)}};function T(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}function A(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>{s[e]=n})}function I(e){var n=T(e);return n&&!n.disableAutodetect}function S(e,n){var t=e;o.forEach((function(e){e[t]&&e[t](n)}))}Object.assign(t,{highlight:b,highlightAuto:v,fixMarkup:x,highlightBlock:E,configure:function(e){f=y(f,e)},initHighlighting:N,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",N,!1)},registerLanguage:function(e,n){var r=null;try{r=n(t)}catch(n){if(console.error("Language definition for '{}' could not be registered.".replace("{}",e)),!l)throw n;console.error(n),r=h}r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&A(r.aliases,{languageName:e})},listLanguages:function(){return Object.keys(i)},getLanguage:T,registerAliases:A,requireLanguage:function(e){var n=T(e);if(n)return n;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:I,inherit:y,addPlugin:function(e){o.push(e)}}),t.debugMode=function(){l=!1},t.safeMode=function(){l=!0},t.versionString="10.1.1";for(const n in _)"object"==typeof _[n]&&e(_[n]);return Object.assign(t,_),t}({})}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("php",function(){"use strict";return function(e){var r={begin:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},t={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},e.inherit(e.APOS_STRING_MODE,{illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]},i={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};return{aliases:["php","php3","php4","php5","php6","php7"],case_insensitive:!0,keywords:i,contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler"}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[e.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},t,{className:"keyword",begin:/\$this\b/},r,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,e.C_BLOCK_COMMENT_MODE,a,n]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"=>"},a,n]}}}());hljs.registerLanguage("nginx",function(){"use strict";return function(e){var n={className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/}/},{begin:"[\\$\\@]"+e.UNDERSCORE_IDENT_RE}]},a={endsWithParent:!0,keywords:{$pattern:"[a-z/_]+",literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[n]},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:"\\s\\^",end:"\\s|{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|{|;",returnEnd:!0},{begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},n]};return{name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{begin:e.UNDERSCORE_IDENT_RE+"\\s+{",returnBegin:!0,end:"{",contains:[{className:"section",begin:e.UNDERSCORE_IDENT_RE}],relevance:0},{begin:e.UNDERSCORE_IDENT_RE+"\\s",end:";|{",returnBegin:!0,contains:[{className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:a}],relevance:0}],illegal:"[^\\s\\}]"}}}());hljs.registerLanguage("csharp",function(){"use strict";return function(e){var n={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},i=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},t=e.inherit(s,{illegal:/\n/}),l={className:"subst",begin:"{",end:"}",keywords:n},r=e.inherit(l,{illegal:/\n/}),c={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},e.BACKSLASH_ESCAPE,r]},o={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]},g=e.inherit(o,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},r]});l.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],r.contains=[g,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];var d={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},i]},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},i,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{begin:e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,contains:[e.TITLE_MODE,E],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[d,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}}());hljs.registerLanguage("perl",function(){"use strict";return function(e){var n={$pattern:/[\w.]+/,keyword:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmget sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},t={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:n},s={begin:"->{",end:"}"},r={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},i=[e.BACKSLASH_ESCAPE,t,r],a=[r,e.HASH_COMMENT_MODE,e.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),s,{className:"string",contains:i,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",relevance:0,contains:[e.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[e.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];return t.contains=a,s.contains=a,{name:"Perl",aliases:["pl","pm"],keywords:n,contains:a}}}());hljs.registerLanguage("swift",function(){"use strict";return function(e){var i={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},n=e.COMMENT("/\\*","\\*/",{contains:["self"]}),t={className:"subst",begin:/\\\(/,end:"\\)",keywords:i,contains:[]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},r={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};return t.contains=[r],{name:"Swift",keywords:i,contains:[a,e.C_LINE_COMMENT_MODE,n,{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*[!?]"},{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*",relevance:0},r,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin://},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:i,contains:["self",r,a,e.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:i,end:"\\{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)\\b"},{beginKeywords:"import",end:/$/,contains:[e.C_LINE_COMMENT_MODE,n]}]}}}());hljs.registerLanguage("makefile",function(){"use strict";return function(e){var i={className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:"",relevance:10,contains:[a,i,t,s,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"",contains:[a,s,i,t]}]}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},n,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:")",end:">",keywords:{name:"style"},contains:[c],starts:{end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:")",end:">",keywords:{name:"script"},contains:[c],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}}}());hljs.registerLanguage("bash",function(){"use strict";return function(e){const s={};Object.assign(s,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[s]}]}]});const t={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},n={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,t]};t.contains.push(n);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,s]},i=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b-?[a-z\._]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[i,e.SHEBANG(),c,a,e.HASH_COMMENT_MODE,n,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}}());hljs.registerLanguage("c-like",function(){"use strict";return function(e){function t(e){return"(?:"+e+")?"}var n="(decltype\\(auto\\)|"+t("[a-zA-Z_]\\w*::")+"[a-zA-Z_]\\w*"+t("<.*?>")+")",r={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},a={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},i={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(a,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},o={className:"title",begin:t("[a-zA-Z_]\\w*::")+e.IDENT_RE,relevance:0},c=t("[a-zA-Z_]\\w*::")+e.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",literal:"true false nullptr NULL"},d=[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,i,a],_={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:l,contains:d.concat([{begin:/\(/,end:/\)/,keywords:l,contains:d.concat(["self"]),relevance:0}]),relevance:0},u={className:"function",begin:"("+n+"[\\*&\\s]+)+"+c,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:l,relevance:0},{begin:c,returnBegin:!0,contains:[o],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r,{begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r]}]},r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s]};return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],keywords:l,disableAutodetect:!0,illegal:"",keywords:l,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:l},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin://,contains:["self"]},e.TITLE_MODE]}]),exports:{preprocessor:s,strings:a,keywords:l}}}}());hljs.registerLanguage("coffeescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={keyword:e.concat(["then","unless","until","loop","by","when","and","or","is","isnt","not"]).filter((e=>n=>!e.includes(n))(["var","const","let","function","static"])).join(" "),literal:n.concat(["yes","no","on","off"]).join(" "),built_in:a.concat(["npm","print"]).join(" ")},i="[A-Za-z$_][0-9A-Za-z$_]*",s={className:"subst",begin:/#\{/,end:/}/,keywords:t},o=[r.BINARY_NUMBER_MODE,r.inherit(r.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[r.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[r.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[r.BACKSLASH_ESCAPE,s]},{begin:/"/,end:/"/,contains:[r.BACKSLASH_ESCAPE,s]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[s,r.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@"+i},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];s.contains=o;var c=r.inherit(r.TITLE_MODE,{begin:i}),l={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:t,contains:["self"].concat(o)}]};return{name:"CoffeeScript",aliases:["coffee","cson","iced"],keywords:t,illegal:/\/\*/,contains:o.concat([r.COMMENT("###","###"),r.HASH_COMMENT_MODE,{className:"function",begin:"^\\s*"+i+"\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[c,l]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[l]}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[c]},c]},{begin:i+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}());hljs.registerLanguage("ruby",function(){"use strict";return function(e){var n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",a={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},s={className:"doctag",begin:"@[A-Za-z]+"},i={begin:"#<",end:">"},r=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^\\=begin","^\\=end",{contains:[s],relevance:10}),e.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:a},t={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[e.BACKSLASH_ESCAPE,c]})]}]},b={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:a},d=[t,i,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+e.IDENT_RE+"::)?"+e.IDENT_RE}]}].concat(r)},{className:"function",beginKeywords:"def",end:"$|;",contains:[e.inherit(e.TITLE_MODE,{begin:n}),b].concat(r)},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[t,{begin:n}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:a},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[i,{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(r),relevance:0}].concat(r);c.contains=d,b.contains=d;var g=[{begin:/^\s*=>/,starts:{end:"$",contains:d}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:d}}];return{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:a,illegal:/\/\*/,contains:r.concat(g).concat(d)}}}());hljs.registerLanguage("yaml",function(){"use strict";return function(e){var n="true false yes no null",a="[\\w#;/?:@&=+$,.~*\\'()[\\]]+",s={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]},i=e.inherit(s,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={end:",",endsWithParent:!0,excludeEnd:!0,contains:[],keywords:n,relevance:0},t={begin:"{",end:"}",contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]",contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>]([0-9]?[+-])?[ ]*\\n( *)[\\S ]+\\n(\\2[\\S ]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"\\-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b"},t,g,s],c=[...b];return c.pop(),c.push(i),l.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml","YAML"],contains:b}}}());hljs.registerLanguage("d",function(){"use strict";return function(e){var a={$pattern:e.UNDERSCORE_IDENT_RE,keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},d="((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))",n="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",t={className:"number",begin:"\\b"+d+"(L|u|U|Lu|LU|uL|UL)?",relevance:0},_={className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|"+d+"(i|[fF]i|Li))",relevance:0},r={className:"string",begin:"'("+n+"|.)",end:"'",illegal:"."},i={className:"string",begin:'"',contains:[{begin:n,relevance:0}],end:'"[cwd]?'},s=e.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{name:"D",keywords:a,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},i,{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},_,t,r,{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}}());hljs.registerLanguage("properties",function(){"use strict";return function(e){var n="[ \\t\\f]*",t="("+n+"[:=]"+n+"|[ \\t\\f]+)",a="([^\\\\:= \\t\\f\\n]|\\\\.)+",s={end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+"+t,returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:s},{begin:a+t,returnBegin:!0,relevance:0,contains:[{className:"meta",begin:a,endsParent:!0,relevance:0}],starts:s},{className:"attr",relevance:0,begin:a+n+"$"}]}}}());hljs.registerLanguage("http",function(){"use strict";return function(e){var n="HTTP/[0-9\\.]+";return{name:"HTTP",aliases:["https"],illegal:"\\S",contains:[{begin:"^"+n,end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},{begin:"^[A-Z]+ (.*?) "+n+"$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:n},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}}}());hljs.registerLanguage("haskell",function(){"use strict";return function(e){var n={variants:[e.COMMENT("--","$"),e.COMMENT("{-","-}",{contains:["self"]})]},i={className:"meta",begin:"{-#",end:"#-}"},a={className:"meta",begin:"^#",end:"$"},s={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},l={begin:"\\(",end:"\\)",illegal:'"',contains:[i,a,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TITLE_MODE,{begin:"[_a-z][\\w']*"}),n]};return{name:"Haskell",aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[l,n],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[l,n],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",keywords:"class family instance where",contains:[s,l,n]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[i,s,l,{begin:"{",end:"}",contains:l.contains},n]},{beginKeywords:"default",end:"$",contains:[s,l,n]},{beginKeywords:"infix infixl infixr",end:"$",contains:[e.C_NUMBER_MODE,n]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[s,e.QUOTE_STRING_MODE,n]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},i,a,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,s,e.inherit(e.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}]}}}());hljs.registerLanguage("handlebars",function(){"use strict";function e(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(n){const a={"builtin-name":"action bindattr collection component concat debugger each each-in get hash if in input link-to loc log lookup mut outlet partial query-params render template textarea unbound unless view with yield"},t=/\[.*?\]/,s=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,i=e("(",/'.*?'/,"|",/".*?"/,"|",t,"|",s,"|",/\.|\//,")+"),r=e("(",t,"|",s,")(?==)"),l={begin:i,lexemes:/[\w.\/]+/},c=n.inherit(l,{keywords:{literal:"true false undefined null"}}),o={begin:/\(/,end:/\)/},m={className:"attr",begin:r,relevance:0,starts:{begin:/=/,end:/=/,starts:{contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,c,o]}}},d={contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,{begin:/as\s+\|/,keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},m,c,o],returnEnd:!0},g=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/\)/})});o.contains=[g];const u=n.inherit(l,{keywords:a,className:"name",starts:n.inherit(d,{end:/}}/})}),b=n.inherit(l,{keywords:a,className:"name"}),h=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/}}/})});return{name:"Handlebars",aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,skip:!0},n.COMMENT(/\{\{!--/,/--\}\}/),n.COMMENT(/\{\{!/,/\}\}/),{className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[u],starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[b]},{className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[u]},{className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[b]},{className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[h]},{className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[h]}]}}}());hljs.registerLanguage("rust",function(){"use strict";return function(e){var n="([ui](8|16|32|64|128|size)|f(32|64))?",t="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",literal:"true false Some None Ok Err",built_in:t},illegal:""}]}}}());hljs.registerLanguage("cpp",function(){"use strict";return function(e){var t=e.getLanguage("c-like").rawDefinition();return t.disableAutodetect=!1,t.name="C++",t.aliases=["cc","c++","h++","hpp","hh","hxx","cxx"],t}}());hljs.registerLanguage("ini",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(...n){return n.map(n=>e(n)).join("")}return function(a){var s={className:"number",relevance:0,variants:[{begin:/([\+\-]+)?[\d]+_[\d_]+/},{begin:a.NUMBER_RE}]},i=a.COMMENT();i.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];var t={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},l={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]},c={begin:/\[/,end:/\]/,contains:[i,r,t,l,s,"self"],relevance:0},g="("+[/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/].map(n=>e(n)).join("|")+")";return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[i,{className:"section",begin:/\[+/,end:/\]+/},{begin:n(g,"(\\s*\\.\\s*",g,")*",n("(?=",/\s*=\s*[^#\s]/,")")),className:"attr",starts:{end:/$/,contains:[i,c,r,t,l,s]}}]}}}());hljs.registerLanguage("objectivec",function(){"use strict";return function(e){var n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={$pattern:n,keyword:"@interface @class @protocol @implementation"};return{name:"Objective-C",aliases:["mm","objc","obj-c"],keywords:{$pattern:n,keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},illegal:"/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+_.keyword.split(" ").join("|")+")\\b",end:"({|$)",excludeEnd:!0,keywords:_,contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}}}());hljs.registerLanguage("apache",function(){"use strict";return function(e){var n={className:"number",begin:"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?"};return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,contains:[e.HASH_COMMENT_MODE,{className:"section",begin:"",contains:[n,{className:"number",begin:":\\d{1,5}"},e.inherit(e.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",{className:"number",begin:"[\\$%]\\d+"}]},n,{className:"number",begin:"\\d+"},e.QUOTE_STRING_MODE]}}],illegal:/\S/}}}());hljs.registerLanguage("java",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(e){return a("(",e,")?")}function a(...n){return n.map(n=>e(n)).join("")}function s(...n){return"("+n.map(n=>e(n)).join("|")+")"}return function(e){var t="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",i={className:"meta",begin:"@[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},r=e=>a("[",e,"]+([",e,"_]*[",e,"]+)?"),c={className:"number",variants:[{begin:`\\b(0[bB]${r("01")})[lL]?`},{begin:`\\b(0${r("0-7")})[dDfFlL]?`},{begin:a(/\b0[xX]/,s(a(r("a-fA-F0-9"),/\./,r("a-fA-F0-9")),a(r("a-fA-F0-9"),/\.?/),a(/\./,r("a-fA-F0-9"))),/([pP][+-]?(\d+))?/,/[fFdDlL]?/)},{begin:a(/\b/,s(a(/\d*\./,r("\\d")),r("\\d")),/[eE][+-]?[\d]+[dDfF]?/)},{begin:a(/\b/,r(/\d/),n(/\.?/),n(r(/\d/)),/[dDfFlL]?/)}],relevance:0};return{name:"Java",aliases:["jsp"],keywords:t,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:t,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:t,relevance:0,contains:[i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},c,i]}}}());hljs.registerLanguage("x86asm",function(){"use strict";return function(s){return{name:"Intel x86 Assembly",case_insensitive:!0,keywords:{$pattern:"[.%]?"+s.IDENT_RE,keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},contains:[s.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},s.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}}}());hljs.registerLanguage("kotlin",function(){"use strict";return function(e){var n={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},a={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"},i={className:"subst",begin:"\\${",end:"}",contains:[e.C_NUMBER_MODE]},s={className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},t={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[s,i]},{begin:"'",end:"'",illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,s,i]}]};i.contains.push(t);var r={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"},l={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[e.inherit(t,{className:"meta-string"})]}]},c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),o={variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]},d=o;return d.variants[1].contains=[o],o.variants[1].contains=[d],{name:"Kotlin",aliases:["kt"],keywords:n,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},a,r,l,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:n,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[o,e.C_LINE_COMMENT_MODE,c],relevance:0},e.C_LINE_COMMENT_MODE,c,r,l,t,e.C_NUMBER_MODE]},c]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},r,l]},t,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}}}());hljs.registerLanguage("armasm",function(){"use strict";return function(s){const e={variants:[s.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0}),s.COMMENT("[;@]","$",{relevance:0}),s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+s.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"},e,s.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}}());hljs.registerLanguage("go",function(){"use strict";return function(e){var n={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{name:"Go",aliases:["golang"],keywords:n,illegal:">>|\.\.\.) /},i={className:"subst",begin:/\{/,end:/\}/,keywords:n,illegal:/#/},s={begin:/\{\{/,relevance:0},r={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,s,i]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,i]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},l={className:"number",relevance:0,variants:[{begin:e.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:e.C_NUMBER_RE+"[lLjJ]?"}]},t={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",a,l,r,e.HASH_COMMENT_MODE]}]};return i.contains=[r,l,a],{name:"Python",aliases:["py","gyp","ipython"],keywords:n,illegal:/(<\/|->|\?)|=>/,contains:[a,l,{beginKeywords:"if",relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,t,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}());hljs.registerLanguage("shell",function(){"use strict";return function(s){return{name:"Shell Session",aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}}}());hljs.registerLanguage("scala",function(){"use strict";return function(e){var n={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},a={className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,n]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[n],relevance:10}]},s={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0},i={className:"class",beginKeywords:"class object trait type",end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},t]},l={className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[t]};return{name:"Scala",keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},s,l,i,e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}());hljs.registerLanguage("julia",function(){"use strict";return function(e){var r="[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",t={$pattern:r,keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi γ π φ ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},a={keywords:t,illegal:/<\//},n={className:"subst",begin:/\$\(/,end:/\)/,keywords:t},o={className:"variable",begin:"\\$"+r},i={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]},l={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],begin:"`",end:"`"},s={className:"meta",begin:"@"+r};return a.name="Julia",a.contains=[{className:"number",begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},i,l,s,{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},e.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}],n.contains=a.contains,a}}());hljs.registerLanguage("php-template",function(){"use strict";return function(n){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}());hljs.registerLanguage("scss",function(){"use strict";return function(e){var t={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},i={className:"number",begin:"#[0-9A-Fa-f]+"};return e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},t,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:":",end:";",contains:[t,i,e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},t,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,i,e.CSS_NUMBER_MODE]}]}}}());hljs.registerLanguage("r",function(){"use strict";return function(e){var n="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{name:"R",contains:[e.HASH_COMMENT_MODE,{begin:n,keywords:{$pattern:n,keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}}}());hljs.registerLanguage("sql",function(){"use strict";return function(e){var t=e.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}}());hljs.registerLanguage("c",function(){"use strict";return function(e){var n=e.getLanguage("c-like").rawDefinition();return n.name="C",n.aliases=["c","h"],n}}());hljs.registerLanguage("json",function(){"use strict";return function(n){var e={literal:"true false null"},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],t=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],a={end:",",endsWithParent:!0,excludeEnd:!0,contains:t,keywords:e},l={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(a,{begin:/:/})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(a)],illegal:"\\S"};return t.push(l,s),i.forEach((function(n){t.push(n)})),{name:"JSON",contains:t,keywords:e,illegal:"\\S"}}}());hljs.registerLanguage("python-repl",function(){"use strict";return function(n){return{aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}}}());hljs.registerLanguage("markdown",function(){"use strict";return function(n){const e={begin:"<",end:">",subLanguage:"xml",relevance:0},a={begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},i={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},s={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};i.contains.push(s),s.contains.push(i);var c=[e,a];return i.contains=i.contains.concat(c),s.contains=s.contains.concat(c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:c=c.concat(i,s)},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:c}]}]},e,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:c,end:"$"},{className:"code",variants:[{begin:"(`{3,})(.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})(.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}}());hljs.registerLanguage("javascript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function s(e){return r("(?=",e,")")}function r(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(t){var i="[A-Za-z$_][0-9A-Za-z$_]*",c={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/},o={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.join(" "),literal:n.join(" "),built_in:a.join(" ")},l={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:t.C_NUMBER_RE+"n?"}],relevance:0},E={className:"subst",begin:"\\$\\{",end:"\\}",keywords:o,contains:[]},d={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"xml"}},g={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"css"}},u={className:"string",begin:"`",end:"`",contains:[t.BACKSLASH_ESCAPE,E]};E.contains=[t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,l,t.REGEXP_MODE];var b=E.contains.concat([{begin:/\(/,end:/\)/,contains:["self"].concat(E.contains,[t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE])},t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE]),_={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:b};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:o,contains:[t.SHEBANG({binary:"node",relevance:5}),{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,t.C_LINE_COMMENT_MODE,t.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:i+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),t.C_BLOCK_COMMENT_MODE,l,{begin:r(/[{,\n]\s*/,s(r(/(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/,i+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:i+s("\\s*:"),relevance:0}]},{begin:"("+t.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[t.C_LINE_COMMENT_MODE,t.C_BLOCK_COMMENT_MODE,t.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+t.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:t.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:o,contains:b}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:""},{begin:c.begin,end:c.end}],subLanguage:"xml",contains:[{begin:c.begin,end:c.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[t.inherit(t.TITLE_MODE,{begin:i}),_],illegal:/\[|%/},{begin:/\$[(.]/},t.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},t.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?="+i+"\\()",end:/{/,keywords:"get set",contains:[t.inherit(t.TITLE_MODE,{begin:i}),{begin:/\(\)/},_]}],illegal:/#(?!!)/}}}());hljs.registerLanguage("typescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]).join(" "),literal:n.join(" "),built_in:a.concat(["any","void","number","boolean","string","object","never","enum"]).join(" ")},s={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},i={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:r.C_NUMBER_RE+"n?"}],relevance:0},o={className:"subst",begin:"\\$\\{",end:"\\}",keywords:t,contains:[]},c={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"xml"}},l={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"css"}},E={className:"string",begin:"`",end:"`",contains:[r.BACKSLASH_ESCAPE,o]};o.contains=[r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,i,r.REGEXP_MODE];var d={begin:"\\(",end:/\)/,keywords:t,contains:["self",r.QUOTE_STRING_MODE,r.APOS_STRING_MODE,r.NUMBER_MODE]},u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,s,d]};return{name:"TypeScript",aliases:["ts"],keywords:t,contains:[r.SHEBANG(),{className:"meta",begin:/^\s*['"]use strict['"]/},r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,i,{begin:"("+r.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,r.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+r.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:r.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:d.contains}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:t,contains:["self",r.inherit(r.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),u],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",u]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+r.IDENT_RE,relevance:0},s,d]}}}());hljs.registerLanguage("plaintext",function(){"use strict";return function(t){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}());hljs.registerLanguage("less",function(){"use strict";return function(e){var n="([\\w-]+|@{[\\w-]+})",a=[],s=[],t=function(e){return{className:"string",begin:"~?"+e+".*?"+e}},r=function(e,n,a){return{className:e,begin:n,relevance:a}},i={begin:"\\(",end:"\\)",contains:s,relevance:0};s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t("'"),t('"'),e.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},r("number","#[0-9A-Fa-f]+\\b"),i,r("variable","@@?[\\w-]+",10),r("variable","@{[\\w-]+}"),r("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});var c=s.concat({begin:"{",end:"}",contains:a}),l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(s)},o={begin:n+"\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:n,end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]},g={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:s,relevance:0}},d={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:c}},b={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:n,end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,l,r("keyword","all\\b"),r("variable","@{[\\w-]+}"),r("selector-tag",n+"%?",0),r("selector-id","#"+n),r("selector-class","\\."+n,0),r("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:c},{begin:"!important"}]};return a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,d,o,b),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:a}}}());hljs.registerLanguage("lua",function(){"use strict";return function(e){var t={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},a=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[t],relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},contains:a.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:a}].concat(a)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[t],relevance:5}])}}}());hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0}catch(e){return!1}}var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/;e()&&(a=a.source.replace(/\\b/g,"(?{var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c,contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0,contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}},solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i,HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e};const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:g}=o;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",E=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={keyword:"var bool string int uint "+E+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using global pragma contract interface library is abstract type assembly",literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years",built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4"},O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/},C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max"},relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:C,keywords:p}),w={className:"built_in",begin:(g()?"(?{"use strict";function e(){try{return!0}catch(e){return!1}}var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/;e()&&(a=a.source.replace(/\\b/g,"(?{var a=d(e),n=r(e),o=/[A-Za-z_$][A-Za-z_$0-9.]*/,c=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:o,keywords:t}),u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:o,keywords:t,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,n,s]},p={className:"operator",begin:/:=|->/};return{keywords:t,lexemes:o,contains:[a,n,i,l,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,p,{className:"function",lexemes:o,beginKeywords:"function",end:"{",excludeEnd:!0,contains:[c,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,p]}]}},solAposStringMode:d,solQuoteStringMode:r,HEX_APOS_STRING_MODE:i,HEX_QUOTE_STRING_MODE:l,SOL_NUMBER:s,isNegativeLookbehindAvailable:e};const{SOL_ASSEMBLY_KEYWORDS:o,baseAssembly:c,isNegativeLookbehindAvailable:u}=n;return e=>{var a={keyword:o.keyword+" object code data",built_in:o.built_in+" datasize dataoffset datacopy setimmutable loadimmutable linkersymbol memoryguard",literal:o.literal},s=/\bverbatim_[1-9]?[0-9]i_[1-9]?[0-9]o\b(?!\$)/;u()&&(s=s.source.replace(/\\b/,"(? 1 { // Format the string and add it to the list of logs consoleLogString = fmt.Sprintf(inputValues[0].(string), inputValues[1:]...) } else { diff --git a/fuzzing/valuegeneration/abi_values.go b/fuzzing/valuegeneration/abi_values.go index 616c8305..c0610efc 100644 --- a/fuzzing/valuegeneration/abi_values.go +++ b/fuzzing/valuegeneration/abi_values.go @@ -675,7 +675,7 @@ func DecodeJSONArgumentsFromMap(inputs abi.Arguments, values map[string]any, dep for i, input := range inputs { value, ok := values[input.Name] if !ok { - err := fmt.Errorf("constructor argument not provided for: name: %v", input.Name) + err := fmt.Errorf("value not not provided for argument: name: %v", input.Name) return nil, err } arg, err := decodeJSONArgument(&input.Type, value, deployedContractAddr) From 08a6eab7353b831f436ce8a317835a986640fc02 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 24 Jun 2024 17:08:01 -0500 Subject: [PATCH 063/109] fix: use the shrunken abi values as the msg's data (#374) --- fuzzing/fuzzer_worker.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 7ee4e93a..08bd5f6b 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -10,6 +10,7 @@ import ( fuzzerTypes "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/coverage" "github.com/crytic/medusa/fuzzing/valuegeneration" + "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "golang.org/x/exp/maps" @@ -466,6 +467,13 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri abiValuesMsgData.InputValues[j] = mutatedInput } + // Re-encode the ABI values as calldata. + abiData, err := abiValuesMsgData.Pack() + if err != nil { + logging.GlobalLogger.Panic("Failed to pack call message ABI values", err) + } + possibleShrunkSequence[i].Call.Data = abiData + // Test the shrunken sequence. validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) shrinkIteration++ From a9122ed73af55a3711892877b6642da51f31abe8 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 2 Jul 2024 10:24:43 -0500 Subject: [PATCH 064/109] chore: fix typos (#387) --- fuzzing/config/config.go | 2 +- fuzzing/test_case_optimization.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 2fbb1e64..d18b8551 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -288,7 +288,7 @@ func (p *ProjectConfig) Validate() error { // Verify that the sequence length is a positive number if p.Fuzzing.CallSequenceLength <= 0 { - return errors.New("project configuration must specify a positive number for the transaction sequence lengt") + return errors.New("project configuration must specify a positive number for the transaction sequence length") } // Verify the worker reset limit is a positive number diff --git a/fuzzing/test_case_optimization.go b/fuzzing/test_case_optimization.go index 8b3f4e41..7abf0b9f 100644 --- a/fuzzing/test_case_optimization.go +++ b/fuzzing/test_case_optimization.go @@ -2,15 +2,16 @@ package fuzzing import ( "fmt" + "math/big" + "strings" + "sync" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/logging" "github.com/crytic/medusa/logging/colors" "github.com/ethereum/go-ethereum/accounts/abi" - "math/big" - "strings" - "sync" ) // OptimizationTestCase describes a test being run by a OptimizationTestCaseProvider. @@ -47,7 +48,7 @@ func (t *OptimizationTestCase) Name() string { return fmt.Sprintf("Optimization Test: %s.%s", t.targetContract.Name(), t.targetMethod.Sig) } -// LogMessage obtains a buffer that represents the result of the AssertionTestCase. This buffer can be passed to a logger for +// LogMessage obtains a buffer that represents the result of the OptimizationTestCase. This buffer can be passed to a logger for // console or file logging. func (t *OptimizationTestCase) LogMessage() *logging.LogBuffer { buffer := logging.NewLogBuffer() From 98d3724e470a94e9c0162dc11526da1bcec619bc Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 2 Jul 2024 13:33:00 -0500 Subject: [PATCH 065/109] feat: call constant methods occasionally and allow assertion testing them (#363) * feat: call constant methods ocassionally and allow assertion testing them * add test --- .../project_configuration/testing_config.md | 2 - fuzzing/fuzzer.go | 8 +- fuzzing/fuzzer_test.go | 5 +- fuzzing/fuzzer_worker.go | 33 ++-- fuzzing/fuzzer_worker_sequence_generator.go | 13 +- .../assertions/assert_constant_method.sol | 148 ++++++++++++++++++ 6 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 fuzzing/testdata/contracts/assertions/assert_constant_method.sol diff --git a/docs/src/project_configuration/testing_config.md b/docs/src/project_configuration/testing_config.md index 3adfe08c..98047e72 100644 --- a/docs/src/project_configuration/testing_config.md +++ b/docs/src/project_configuration/testing_config.md @@ -75,8 +75,6 @@ contract MyContract { - **Type**: Boolean - **Description**: Whether `pure` / `view` functions should be tested for assertion failures. - > 🚩 Fuzzing `pure` and `view` functions is not currently implemented. Thus, enabling this option to `true` does not - > update the fuzzer's behavior. - **Default**: `false` ### `panicCodeConfig` diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 5697d1cf..e8908c3d 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/crytic/medusa/fuzzing/executiontracer" "math/big" "math/rand" "os" @@ -16,6 +15,8 @@ import ( "sync" "time" + "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/fuzzing/coverage" "github.com/crytic/medusa/logging" "github.com/crytic/medusa/logging/colors" @@ -707,7 +708,10 @@ func (f *Fuzzer) Start() error { // If StopOnNoTests is true and there are no test cases, then throw an error if f.config.Fuzzing.Testing.StopOnNoTests && len(f.testCases) == 0 { - err = fmt.Errorf("no tests of any kind (assertion/property/optimization/custom) have been identified for fuzzing") + err = fmt.Errorf("no assertion, property, optimization, or custom tests were found to fuzz") + if !f.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { + err = fmt.Errorf("no assertion, property, optimization, or custom tests were found to fuzz and testing view methods is disabled") + } f.logger.Error("Failed to start fuzzer", err) return err } diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 860ebd93..ce2aa536 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -2,11 +2,12 @@ package fuzzing import ( "encoding/hex" - "github.com/crytic/medusa/fuzzing/executiontracer" "math/big" "math/rand" "testing" + "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/events" "github.com/crytic/medusa/fuzzing/calls" @@ -73,6 +74,7 @@ func TestAssertionMode(t *testing.T) { "testdata/contracts/assertions/assert_outofbounds_array_access.sol", "testdata/contracts/assertions/assert_allocate_too_much_memory.sol", "testdata/contracts/assertions/assert_call_uninitialized_variable.sol", + "testdata/contracts/assertions/assert_constant_method.sol", } for _, filePath := range filePaths { runFuzzerTest(t, &fuzzerSolcFileTest{ @@ -88,6 +90,7 @@ func TestAssertionMode(t *testing.T) { config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnIncorrectStorageAccess = true config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnOutOfBoundsArrayAccess = true config.Fuzzing.Testing.AssertionTesting.PanicCodeConfig.FailOnPopEmptyArray = true + config.Fuzzing.Testing.AssertionTesting.TestViewMethods = true config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false }, diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 08bd5f6b..1413dd7e 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -12,6 +12,7 @@ import ( "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" + "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "golang.org/x/exp/maps" ) @@ -35,11 +36,18 @@ type FuzzerWorker struct { // deployedContracts describes a mapping of deployed contractDefinitions and the addresses they were deployed to. deployedContracts map[common.Address]*fuzzerTypes.Contract + // stateChangingMethods is a list of contract functions which are suspected of changing contract state // (non-read-only). A sequence of calls is generated by the FuzzerWorker, targeting stateChangingMethods // before executing tests. stateChangingMethods []fuzzerTypes.DeployedContractMethod + // pureMethods is a list of contract functions which are side-effect free with respect to the EVM (view and/or pure in terms of Solidity mutability). + pureMethods []fuzzerTypes.DeployedContractMethod + + // methodChooser uses a weighted selection algorithm to choose a method to call, prioritizing state changing methods over pure ones. + methodChooser *randomutils.WeightedRandomChooser[fuzzerTypes.DeployedContractMethod] + // randomProvider provides random data as inputs to decisions throughout the worker. randomProvider *rand.Rand // sequenceGenerator creates entirely new or mutated call sequences based on corpus call sequences, for use in @@ -86,6 +94,7 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) coverageTracer: nil, randomProvider: randomProvider, valueSet: valueSet, + methodChooser: randomutils.NewWeightedRandomChooser[fuzzerTypes.DeployedContractMethod](), } worker.sequenceGenerator = NewCallSequenceGenerator(worker, callSequenceGenConfig) worker.shrinkingValueMutator = shrinkingValueMutator @@ -175,8 +184,8 @@ func (fw *FuzzerWorker) onChainContractDeploymentAddedEvent(event chain.Contract // Set our deployed contract address in our deployed contract lookup, so we can reference it later. fw.deployedContracts[event.Contract.Address] = matchedDefinition - // Update our state changing methods - fw.updateStateChangingMethods() + // Update our methods + fw.updateMethods() // Emit an event indicating the worker detected a new contract deployment on its chain. err := fw.Events.ContractAdded.Publish(FuzzerWorkerContractAddedEvent{ @@ -206,8 +215,8 @@ func (fw *FuzzerWorker) onChainContractDeploymentRemovedEvent(event chain.Contra // Remove the contract from our deployed contracts mapping the worker maintains. delete(fw.deployedContracts, event.Contract.Address) - // Update our state changing methods - fw.updateStateChangingMethods() + // Update our methods + fw.updateMethods() // Emit an event indicating the worker detected the removal of a previously deployed contract on its chain. err := fw.Events.ContractDeleted.Publish(FuzzerWorkerContractDeletedEvent{ @@ -221,19 +230,25 @@ func (fw *FuzzerWorker) onChainContractDeploymentRemovedEvent(event chain.Contra return nil } -// updateStateChangingMethods updates the list of state changing methods used by the worker by re-evaluating them +// updateMethods updates the list of methods used by the worker by re-evaluating them // from the deployedContracts lookup. -func (fw *FuzzerWorker) updateStateChangingMethods() { - // Clear our list of state changing methods +func (fw *FuzzerWorker) updateMethods() { + // Clear our list of methods fw.stateChangingMethods = make([]fuzzerTypes.DeployedContractMethod, 0) + fw.pureMethods = make([]fuzzerTypes.DeployedContractMethod, 0) // Loop through each deployed contract for contractAddress, contractDefinition := range fw.deployedContracts { // If we deployed the contract, also enumerate property tests and state changing methods. for _, method := range contractDefinition.CompiledContract().Abi.Methods { - if !method.IsConstant() { - // Any non-constant method should be tracked as a state changing method. + // Any non-constant method should be tracked as a state changing method. + // We favor calling state changing methods over view methods. + if method.IsConstant() { + fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) + fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(1))) + } else { fw.stateChangingMethods = append(fw.stateChangingMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) + fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(100))) } } } diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index a931b04a..0ff65681 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -270,17 +270,20 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement return element, nil } -// generateNewElement generates a new call sequence element which targets a state changing method in a contract +// generateNewElement generates a new call sequence element which targets a method in a contract // deployed to the CallSequenceGenerator's parent FuzzerWorker chain, with fuzzed call data. // Returns the call sequence element, or an error if one was encountered. func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement, error) { - // Verify we have state changing methods to call - if len(g.worker.stateChangingMethods) == 0 { + // Verify we have state changing methods to call if we are not testing view/pure methods. + if len(g.worker.stateChangingMethods) == 0 && !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call") } - // Select a random method and sender - selectedMethod := &g.worker.stateChangingMethods[g.worker.randomProvider.Intn(len(g.worker.stateChangingMethods))] + selectedMethod, err := g.worker.methodChooser.Choose() + if err != nil { + return nil, err + } + selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] // Generate fuzzed parameters for the function call diff --git a/fuzzing/testdata/contracts/assertions/assert_constant_method.sol b/fuzzing/testdata/contracts/assertions/assert_constant_method.sol new file mode 100644 index 00000000..0ddd2fdf --- /dev/null +++ b/fuzzing/testdata/contracts/assertions/assert_constant_method.sol @@ -0,0 +1,148 @@ +// An assertion failure in the constant method should be detected +// Contract updated to solc > 0.8.0 and taken from https://github.com/crytic/echidna/blob/5757f8c3c07d0248cbe1728506ff0f8daccbef12/tests/solidity/assert/fullmath.sol +library FullMath { + /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv + function mulDiv( + uint256 a, + uint256 b, + uint256 denominator + ) internal pure returns (uint256 result) { + unchecked { + + + // 512-bit multiply [prod1 prod0] = a * b + // Compute the product mod 2**256 and mod 2**256 - 1 + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2**256 + prod0 + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(a, b, not(0)) + prod0 := mul(a, b) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division + if (prod1 == 0) { + require(denominator > 0); + assembly { + result := div(prod0, denominator) + } + return result; + } + + // Make sure the result is less than 2**256. + // Also prevents denominator == 0 + require(denominator > prod1); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0] + // Compute remainder using mulmod + uint256 remainder; + assembly { + remainder := mulmod(a, b, denominator) + } + // Subtract 256 bit number from 512 bit number + assembly { + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator + // Compute largest power of two divisor of denominator. + // Always >= 1. + uint256 twos = (0 - denominator) & denominator; + // Divide denominator by power of two + assembly { + denominator := div(denominator, twos) + } + + // Divide [prod1 prod0] by the factors of two + assembly { + prod0 := div(prod0, twos) + } + // Shift in bits from prod1 into prod0. For this we need + // to flip `twos` such that it is 2**256 / twos. + // If twos is zero, then it becomes one + assembly { + twos := add(div(sub(0, twos), twos), 1) + } + prod0 |= prod1 * twos; + + // Invert denominator mod 2**256 + // Now that denominator is an odd number, it has an inverse + // modulo 2**256 such that denominator * inv = 1 mod 2**256. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, denominator * inv = 1 mod 2**4 + uint256 inv = (3 * denominator) ^ 2; + // Now use Newton-Raphson iteration to improve the precision. + // Thanks to Hensel's lifting lemma, this also works in modular + // arithmetic, doubling the correct bits in each step. + inv *= 2 - denominator * inv; // inverse mod 2**8 + inv *= 2 - denominator * inv; // inverse mod 2**16 + inv *= 2 - denominator * inv; // inverse mod 2**32 + inv *= 2 - denominator * inv; // inverse mod 2**64 + inv *= 2 - denominator * inv; // inverse mod 2**128 + inv *= 2 - denominator * inv; // inverse mod 2**256 + + // Because the division is now exact we can divide by multiplying + // with the modular inverse of denominator. This will give us the + // correct result modulo 2**256. Since the precoditions guarantee + // that the outcome is less than 2**256, this is the final result. + // We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inv; + return result; + } + } + + /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + function mulDivRoundingUp( + uint256 a, + uint256 b, + uint256 denominator + ) internal pure returns (uint256 result) { + // BUG + unchecked { + return mulDiv(a, b, denominator) + (mulmod(a, b, denominator) > 0 ? 1 : 0); + } + } +} + +contract TestContract { + function checkMulDivRoundingUp( + uint256 x, + uint256 y, + uint256 d + ) external pure { + require(d > 0); + uint256 z = FullMath.mulDivRoundingUp(x, y, d); + if (x == 0 || y == 0) { + assert(z == 0); + return; + } + + // recompute x and y via mulDiv of the result of floor(x*y/d), should always be less than original inputs by < d + uint256 x2 = FullMath.mulDiv(z, d, y); + uint256 y2 = FullMath.mulDiv(z, d, x); + assert(x2 >= x); + assert(y2 >= y); + + assert(x2 - x < d); + assert(y2 - y < d); + } + fallback() external {} +} From 563b371a05336e3571a949b57c0fc39a35ec8752 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 2 Jul 2024 13:44:10 -0500 Subject: [PATCH 066/109] fix: re-encode calldata in mutator and refactor (#380) * fix: re-encode calldata in mutator and refactor * panic on misuse --- fuzzing/calls/call_message.go | 22 ++++++++++++++++++++- fuzzing/fuzzer_worker.go | 9 ++------- fuzzing/fuzzer_worker_sequence_generator.go | 3 +++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/fuzzing/calls/call_message.go b/fuzzing/calls/call_message.go index 47dc0263..6b6dfa2f 100644 --- a/fuzzing/calls/call_message.go +++ b/fuzzing/calls/call_message.go @@ -1,6 +1,8 @@ package calls import ( + "math/big" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/logging" "github.com/ethereum/go-ethereum/common" @@ -8,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/core" coreTypes "github.com/ethereum/go-ethereum/core/types" "golang.org/x/exp/slices" - "math/big" ) // The following directives will be picked up by the `go generate` command to generate JSON marshaling code from @@ -126,6 +127,25 @@ func NewCallMessageWithAbiValueData(from common.Address, to *common.Address, non } } +// WithDataAbiValues resets the call message's data and ABI values, ensuring the values are in sync and +// reusing the other existing fields. +func (m *CallMessage) WithDataAbiValues(abiData *CallMessageDataAbiValues) { + if abiData == nil { + logging.GlobalLogger.Panic("Method ABI and data should always be defined") + } + + // Pack the ABI value data + var data []byte + var err error + data, err = abiData.Pack() + if err != nil { + logging.GlobalLogger.Panic("Failed to pack call message ABI values", err) + } + // Set our data and ABI values + m.DataAbiValues = abiData + m.Data = data +} + // FillFromTestChainProperties populates gas limit, price, nonce, and other fields automatically based on the worker's // underlying test chain properties if they are not yet set. func (m *CallMessage) FillFromTestChainProperties(chain *chain.TestChain) { diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 1413dd7e..344063f8 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -10,7 +10,6 @@ import ( fuzzerTypes "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/coverage" "github.com/crytic/medusa/fuzzing/valuegeneration" - "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" @@ -482,12 +481,8 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri abiValuesMsgData.InputValues[j] = mutatedInput } - // Re-encode the ABI values as calldata. - abiData, err := abiValuesMsgData.Pack() - if err != nil { - logging.GlobalLogger.Panic("Failed to pack call message ABI values", err) - } - possibleShrunkSequence[i].Call.Data = abiData + // Re-encode the message's calldata + possibleShrunkSequence[i].Call.WithDataAbiValues(abiValuesMsgData) // Test the shrunken sequence. validShrunkSequence, err := fw.testShrunkenCallSequence(possibleShrunkSequence, shrinkRequest) diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index 0ff65681..edc3b224 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -460,5 +460,8 @@ func prefetchModifyCallFuncMutate(sequenceGenerator *CallSequenceGenerator, elem } abiValuesMsgData.InputValues[i] = mutatedInput } + // Re-encode the message's calldata + element.Call.WithDataAbiValues(abiValuesMsgData) + return nil } From 35dcc746df0d71e9343d35206d1213fd75616bf6 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 2 Jul 2024 14:10:19 -0500 Subject: [PATCH 067/109] excluding compilation platform fallsback to default (#362) * fix: excluding compilation platform fallsback to default * warn if coverage is disabled --------- Co-authored-by: anishnaik --- cmd/fuzz.go | 12 +++++++++--- fuzzing/config/config.go | 9 +++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cmd/fuzz.go b/cmd/fuzz.go index 188f5d95..c9905b84 100644 --- a/cmd/fuzz.go +++ b/cmd/fuzz.go @@ -2,12 +2,13 @@ package cmd import ( "fmt" - "github.com/crytic/medusa/cmd/exitcodes" - "github.com/crytic/medusa/logging/colors" "os" "os/signal" "path/filepath" + "github.com/crytic/medusa/cmd/exitcodes" + "github.com/crytic/medusa/logging/colors" + "github.com/crytic/medusa/fuzzing" "github.com/crytic/medusa/fuzzing/config" "github.com/spf13/cobra" @@ -102,7 +103,8 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { if existenceError == nil { // Try to read the configuration file and throw an error if something goes wrong cmdLogger.Info("Reading the configuration file at: ", colors.Bold, configPath, colors.Reset) - projectConfig, err = config.ReadProjectConfigFromFile(configPath) + // Use the default compilation platform if the config file doesn't specify one + projectConfig, err = config.ReadProjectConfigFromFile(configPath, DefaultCompilationPlatform) if err != nil { cmdLogger.Error("Failed to run the fuzz command", err) return err @@ -144,6 +146,10 @@ func cmdRunFuzz(cmd *cobra.Command, args []string) error { return err } + if !projectConfig.Fuzzing.CoverageEnabled { + cmdLogger.Warn("Disabling coverage may limit efficacy of fuzzing. Consider enabling coverage for better results.") + } + // Create our fuzzing fuzzer, fuzzErr := fuzzing.NewFuzzer(*projectConfig) if fuzzErr != nil { diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index d18b8551..8cbab7dd 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -3,14 +3,15 @@ package config import ( "encoding/json" "errors" + "math/big" + "os" + "github.com/crytic/medusa/chain/config" "github.com/crytic/medusa/compilation" "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/rs/zerolog" - "math/big" - "os" ) // The following directives will be picked up by the `go generate` command to generate JSON marshaling code from @@ -234,7 +235,7 @@ type FileLoggingConfig struct { // 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) { +func ReadProjectConfigFromFile(path string, platform string) (*ProjectConfig, error) { // Read our project configuration file data b, err := os.ReadFile(path) if err != nil { @@ -242,7 +243,7 @@ func ReadProjectConfigFromFile(path string) (*ProjectConfig, error) { } // Parse the project configuration - projectConfig, err := GetDefaultProjectConfig("") + projectConfig, err := GetDefaultProjectConfig(platform) if err != nil { return nil, err } From cc5d85ef493322eac34f8c7c12cb93c2ac2341da Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 2 Jul 2024 14:21:00 -0500 Subject: [PATCH 068/109] feat: display test cases discovered by the fuzzer (#382) * feat: display test cases discovered by the fuzzer * check whether optimization test case has started --- fuzzing/fuzzer.go | 3 +++ fuzzing/test_case_optimization.go | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index e8908c3d..ad6546c7 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -246,6 +246,9 @@ func (f *Fuzzer) RegisterTestCase(testCase TestCase) { f.testCasesLock.Lock() defer f.testCasesLock.Unlock() + // Display what is being tested + f.logger.Info(testCase.LogMessage().Elements()...) + // Append our test case to our list f.testCases = append(f.testCases, testCase) } diff --git a/fuzzing/test_case_optimization.go b/fuzzing/test_case_optimization.go index 7abf0b9f..3c785047 100644 --- a/fuzzing/test_case_optimization.go +++ b/fuzzing/test_case_optimization.go @@ -55,10 +55,12 @@ func (t *OptimizationTestCase) LogMessage() *logging.LogBuffer { // Note that optimization tests will always pass buffer.Append(colors.GreenBold, fmt.Sprintf("[%s] ", t.Status()), colors.Bold, t.Name(), colors.Reset, "\n") - buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" resulted in the maximum value: ", t.targetContract.Name(), t.targetMethod.Sig)) - buffer.Append(colors.Bold, t.value, colors.Reset, "\n") - buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") - buffer.Append(t.CallSequence().Log().Elements()...) + if t.Status() != TestCaseStatusNotStarted { + buffer.Append(fmt.Sprintf("Test for method \"%s.%s\" resulted in the maximum value: ", t.targetContract.Name(), t.targetMethod.Sig)) + buffer.Append(colors.Bold, t.value, colors.Reset, "\n") + buffer.Append(colors.Bold, "[Call Sequence]", colors.Reset, "\n") + buffer.Append(t.CallSequence().Log().Elements()...) + } // If an execution trace is attached then add it to the message if t.optimizationTestTrace != nil { buffer.Append(colors.Bold, "[Optimization Test Execution Trace]", colors.Reset, "\n") From 294906b54fafefe0b3d7335340e242a019ea71b9 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 17 Jul 2024 16:56:03 -0500 Subject: [PATCH 069/109] feat: support cancun fork (#397) Upgrades to go-ethereum 1.14.6 to support the Cancun fork, migrates our tracing to the new native tracing, and make our use of the stateDB compatible with the API change. Previously, we were committing the trie for every tx and so we could just restore to that intermediate state and re-execute the tx. Now, the stateDB expects commits to be tied to a block, so I have taken the approach of re-executing the entire tx sequence from the post-deployment state. --------- Co-authored-by: anishnaik --- .github/workflows/ci.yml | 6 +- chain/cheat_code_tracer.go | 142 +++--- chain/console_log_cheat_code_contract.go | 3 +- chain/standard_cheat_code_contract.go | 92 ++-- chain/test_chain.go | 212 +++++---- chain/test_chain_deployments_tracer.go | 156 +++---- chain/test_chain_test.go | 14 +- chain/test_chain_tracer.go | 127 ++++-- chain/types/message_results.go | 2 - chain/vendored/apply_transaction.go | 16 +- compilation/abiutils/solidity_errors.go | 10 +- compilation/types/compiled_contract.go | 4 +- fuzzing/calls/call_sequence.go | 9 +- fuzzing/calls/call_sequence_execution.go | 51 ++- fuzzing/corpus/corpus.go | 16 +- fuzzing/coverage/coverage_tracer.go | 127 +++--- fuzzing/executiontracer/execution_trace.go | 3 +- fuzzing/executiontracer/execution_tracer.go | 144 +++--- fuzzing/fuzzer.go | 54 ++- fuzzing/fuzzer_hooks.go | 9 +- fuzzing/fuzzer_test.go | 5 +- fuzzing/fuzzer_worker.go | 20 +- fuzzing/test_case_assertion_provider.go | 10 +- fuzzing/test_case_optimization_provider.go | 15 +- fuzzing/test_case_property_provider.go | 10 +- go.mod | 101 +++-- go.sum | 471 ++++++-------------- logging/logger_test.go | 5 +- 28 files changed, 900 insertions(+), 934 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea20f899..861b85ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "^1.18.1" + go-version: "^1.22" # disable caching during release (tag) builds cache: ${{ !startsWith(github.ref, 'refs/tags/') }} @@ -129,7 +129,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "^1.18.1" + go-version: "^1.22" - name: Actionlint run: | @@ -197,7 +197,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "^1.18.1" + go-version: "^1.22" - uses: actions/setup-node@v4 with: diff --git a/chain/cheat_code_tracer.go b/chain/cheat_code_tracer.go index 699ddc16..208b7115 100644 --- a/chain/cheat_code_tracer.go +++ b/chain/cheat_code_tracer.go @@ -1,10 +1,14 @@ package chain import ( + "math/big" + "github.com/crytic/medusa/chain/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + coretypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "math/big" + "github.com/ethereum/go-ethereum/eth/tracers" ) // cheatCodeTracer represents an EVM.Logger which tracks and patches EVM execution state to enable extended @@ -18,13 +22,16 @@ type cheatCodeTracer struct { callDepth uint64 // evm refers to the EVM instance last captured. - evm *vm.EVM + evmContext *tracing.VMContext // callFrames represents per-call-frame data deployment information being captured by the tracer. callFrames []*cheatCodeTracerCallFrame // results stores the tracer output after a transaction has concluded. results *cheatCodeTracerResults + + // nativeTracer is the underlying tracer interface that the cheatcode tracer follows + nativeTracer *TestChainTracer } // cheatCodeTracerCallFrame represents per-call-frame data traced by a cheatCodeTracer. @@ -57,13 +64,14 @@ type cheatCodeTracerCallFrame struct { // vmOp describes the current call frame's last instruction executed. vmOp vm.OpCode // vmScope describes the current call frame's scope context. - vmScope *vm.ScopeContext + vmScope tracing.OpContext // vmReturnData describes the current call frame's return data (set on exit). vmReturnData []byte // vmErr describes the current call frame's returned error (set on exit), nil if no error. vmErr error } +// cheatCodeTracerResults holds the hooks that need to be executed when the chain reverts. type cheatCodeTracerResults struct { // onChainRevertHooks describes hooks which are to be executed when the chain reverts. onChainRevertHooks types.GenericHookFuncs @@ -72,9 +80,25 @@ type cheatCodeTracerResults struct { // newCheatCodeTracer creates a cheatCodeTracer and returns it. func newCheatCodeTracer() *cheatCodeTracer { tracer := &cheatCodeTracer{} + innerTracer := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: tracer.OnTxStart, + OnTxEnd: tracer.OnTxEnd, + OnEnter: tracer.OnEnter, + OnExit: tracer.OnExit, + OnOpcode: tracer.OnOpcode, + }, + } + tracer.nativeTracer = &TestChainTracer{Tracer: innerTracer, CaptureTxEndSetAdditionalResults: tracer.CaptureTxEndSetAdditionalResults} + return tracer } +// NativeTracer returns the underlying TestChainTracer. +func (t *cheatCodeTracer) NativeTracer() *TestChainTracer { + return t.nativeTracer +} + // bindToChain is called by the TestChain which created the tracer to set its reference. // Note: This is done because of the cheat code system's dependency on the genesis block, as well as chain's dependency // on it, which prevents the chain being set in the tracer on initialization. @@ -98,80 +122,78 @@ func (t *cheatCodeTracer) CurrentCallFrame() *cheatCodeTracerCallFrame { return t.callFrames[t.callDepth] } -// CaptureTxStart is called upon the start of transaction execution, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureTxStart(gasLimit uint64) { +// OnTxStart is called upon the start of transaction execution, as defined by tracers.Tracer. +func (t *cheatCodeTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) { // Reset our capture state t.callDepth = 0 t.callFrames = make([]*cheatCodeTracerCallFrame, 0) t.results = &cheatCodeTracerResults{ onChainRevertHooks: nil, } + // Store our evm reference + t.evmContext = vm } -// CaptureTxEnd is called upon the end of transaction execution, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureTxEnd(restGas uint64) { +// OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer +func (t *cheatCodeTracer) OnTxEnd(*coretypes.Receipt, error) { } -// CaptureStart initializes the tracing operation for the top of a call frame, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - // Store our evm reference - t.evm = env +// OnEnter initializes the tracing operation for the top of a call frame, as defined by tracers.Tracer. +func (t *cheatCodeTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Check to see if this is the top level call frame + isTopLevelFrame := depth == 0 + var callFrameData *cheatCodeTracerCallFrame + if isTopLevelFrame { + // Create our call frame struct to track data for this initial entry call frame. + callFrameData = &cheatCodeTracerCallFrame{} + } else { + // We haven't updated our call depth yet, so obtain the "previous" call frame (current for now) + previousCallFrame := t.CurrentCallFrame() + + // Create our call frame struct to track data for this initial entry call frame. + // We forward our "next frame hooks" to this frame, then clear them from the previous frame. + callFrameData = &cheatCodeTracerCallFrame{ + onFrameExitRestoreHooks: previousCallFrame.onNextFrameExitRestoreHooks, + } + previousCallFrame.onNextFrameExitRestoreHooks = nil + + // Increase our call depth now that we're entering a new call frame. + t.callDepth++ + } - // Create our call frame struct to track data for this initial entry call frame. - callFrameData := &cheatCodeTracerCallFrame{} + // Append our new call frame t.callFrames = append(t.callFrames, callFrameData) + + // Note: We do not execute events for "next frame enter" here, as we do not yet have scope information. + // Those events are executed when the first EVM instruction is executed in the new scope. } -// CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { +// OnExit is called after a call to finalize tracing completes for the top of a call frame, as defined by tracers.Tracer. +func (t *cheatCodeTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { // Execute all current call frame exit hooks exitingCallFrame := t.callFrames[t.callDepth] exitingCallFrame.onFrameExitRestoreHooks.Execute(false, true) - exitingCallFrame.onTopFrameExitRestoreHooks.Execute(false, true) - // If we didn't encounter an error in this call frame, we push our upward propagating revert events up one frame. - if err == nil { - // Store these revert hooks in our results. - t.results.onChainRevertHooks = append(t.results.onChainRevertHooks, exitingCallFrame.onChainRevertRestoreHooks...) + var parentCallFrame *cheatCodeTracerCallFrame + if depth == 0 { + // If this is the top-level call frame, execute all of its exit hooks + exitingCallFrame.onTopFrameExitRestoreHooks.Execute(false, true) } else { - // We hit an error, so a revert occurred before this tx was committed. - exitingCallFrame.onChainRevertRestoreHooks.Execute(false, true) + // If not, retrieve the parent call frame + parentCallFrame = t.callFrames[t.callDepth-1] } // We're exiting the current frame, so remove our frame data. t.callFrames = t.callFrames[:t.callDepth] -} - -// CaptureEnter is called upon entering of the call frame, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - // We haven't updated our call depth yet, so obtain the "previous" call frame (current for now) - previousCallFrame := t.CurrentCallFrame() - // Increase our call depth now that we're entering a new call frame. - t.callDepth++ - - // Create our call frame struct to track data for this initial entry call frame. - // We forward our "next frame hooks" to this frame, then clear them from the previous frame. - callFrameData := &cheatCodeTracerCallFrame{ - onFrameExitRestoreHooks: previousCallFrame.onNextFrameExitRestoreHooks, - } - previousCallFrame.onNextFrameExitRestoreHooks = nil - t.callFrames = append(t.callFrames, callFrameData) - - // Note: We do not execute events for "next frame enter" here, as we do not yet have scope information. - // Those events are executed when the first EVM instruction is executed in the new scope. -} - -// CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - // Execute all current call frame exit hooks - exitingCallFrame := t.callFrames[t.callDepth] - exitingCallFrame.onFrameExitRestoreHooks.Execute(false, true) - parentCallFrame := t.callFrames[t.callDepth-1] - - // If we didn't encounter an error in this call frame, we push our upward propagating revert events up one frame. - if err == nil { + // If we didn't encounter an error in this call frame, we push our upward propagating restore events up one frame. + if err == nil && depth == 0 { + // Since this is the top call frame, we add the revert events to the results of the tracer and return early + t.results.onChainRevertHooks = append(t.results.onChainRevertHooks, exitingCallFrame.onChainRevertRestoreHooks...) + return + } else if err == nil { + // Propagate hooks up to the parent call frame parentCallFrame.onTopFrameExitRestoreHooks = append(parentCallFrame.onTopFrameExitRestoreHooks, exitingCallFrame.onTopFrameExitRestoreHooks...) parentCallFrame.onChainRevertRestoreHooks = append(parentCallFrame.onChainRevertRestoreHooks, exitingCallFrame.onChainRevertRestoreHooks...) } else { @@ -179,22 +201,19 @@ func (t *cheatCodeTracer) CaptureExit(output []byte, gasUsed uint64, err error) exitingCallFrame.onChainRevertRestoreHooks.Execute(false, true) } - // We're exiting the current frame, so remove our frame data. - t.callFrames = t.callFrames[:t.callDepth] - // Decrease our call depth now that we've exited a call frame. t.callDepth-- } -// CaptureState records data from an EVM state update, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, vmErr error) { +// OnOpcode records data from an EVM state update, as defined by tracers.Tracer. +func (t *cheatCodeTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { // Set our current frame information. currentCallFrame := t.CurrentCallFrame() currentCallFrame.vmPc = pc - currentCallFrame.vmOp = op + currentCallFrame.vmOp = vm.OpCode(op) currentCallFrame.vmScope = scope currentCallFrame.vmReturnData = rData - currentCallFrame.vmErr = vmErr + currentCallFrame.vmErr = err // We execute our entered next frame hooks here (from our previous call frame), as we now have scope information. if t.callDepth > 0 { @@ -202,11 +221,6 @@ func (t *cheatCodeTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64 } } -// CaptureFault records an execution fault, as defined by vm.EVMLogger. -func (t *cheatCodeTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { - -} - // CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this // tracer is used during transaction execution (block creation), the results can later be queried from the block. // This method will only be called on the added tracer if it implements the extended TestChainTracer interface. diff --git a/chain/console_log_cheat_code_contract.go b/chain/console_log_cheat_code_contract.go index 1c20dedb..ee086c59 100644 --- a/chain/console_log_cheat_code_contract.go +++ b/chain/console_log_cheat_code_contract.go @@ -1,10 +1,11 @@ package chain import ( + "strconv" + "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "strconv" ) // ConsoleLogContractAddress is the address for the console.log precompile contract diff --git a/chain/standard_cheat_code_contract.go b/chain/standard_cheat_code_contract.go index 144c7e77..97eb3288 100644 --- a/chain/standard_cheat_code_contract.go +++ b/chain/standard_cheat_code_contract.go @@ -4,14 +4,18 @@ import ( "crypto/ecdsa" "encoding/hex" "fmt" - "github.com/crytic/medusa/utils" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "math/big" "os/exec" "strconv" "strings" + + "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/holiman/uint256" ) // StandardCheatcodeContractAddress is the address for the standard cheatcode contract @@ -73,7 +77,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "warp", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes until the transaction exits. - originalTime := tracer.evm.Context.Time + originalTime := tracer.chain.pendingBlockContext.Time // Retrieve new timestamp and make sure it is LEQ max value of an uint64 newTime := inputs[0].(*big.Int) @@ -82,10 +86,10 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, } // Set the time - tracer.evm.Context.Time = newTime.Uint64() + tracer.chain.pendingBlockContext.Time = newTime.Uint64() tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { // Reset the time - tracer.evm.Context.Time = originalTime + tracer.chain.pendingBlockContext.Time = originalTime }) return nil, nil }, @@ -96,10 +100,10 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "roll", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes until the transaction exits. - original := new(big.Int).Set(tracer.evm.Context.BlockNumber) - tracer.evm.Context.BlockNumber.Set(inputs[0].(*big.Int)) + original := new(big.Int).Set(tracer.chain.pendingBlockContext.BlockNumber) + tracer.chain.pendingBlockContext.BlockNumber.Set(inputs[0].(*big.Int)) tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { - tracer.evm.Context.BlockNumber.Set(original) + tracer.chain.pendingBlockContext.BlockNumber.Set(original) }) return nil, nil }, @@ -110,38 +114,28 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "fee", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes until the transaction exits. - original := new(big.Int).Set(tracer.evm.Context.BaseFee) - tracer.evm.Context.BaseFee.Set(inputs[0].(*big.Int)) + original := new(big.Int).Set(tracer.chain.pendingBlockContext.BaseFee) + tracer.chain.pendingBlockContext.BaseFee.Set(inputs[0].(*big.Int)) tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { - tracer.evm.Context.BaseFee.Set(original) + tracer.chain.pendingBlockContext.BaseFee.Set(original) }) return nil, nil }, ) - // Difficulty: Sets VM block number + // Difficulty: Updates difficulty contract.addMethod( "difficulty", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes until the transaction exits. - - // Obtain our spoofed difficulty spoofedDifficulty := inputs[0].(*big.Int) spoofedDifficultyHash := common.BigToHash(spoofedDifficulty) - - // Change difficulty in block context. - originalDifficulty := new(big.Int).Set(tracer.evm.Context.Difficulty) - tracer.evm.Context.Difficulty.Set(spoofedDifficulty) - tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { - tracer.evm.Context.Difficulty.Set(originalDifficulty) - }) + originalRandom := tracer.chain.pendingBlockContext.Random // In newer evm versions, block.difficulty uses opRandom instead of opDifficulty. - // TODO: Check chain config here to see if the EVM version is 'Paris' or the consensus upgrade occurred. - originalRandom := tracer.evm.Context.Random - tracer.evm.Context.Random = &spoofedDifficultyHash + tracer.chain.pendingBlockContext.Random = &spoofedDifficultyHash tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { - tracer.evm.Context.Random = originalRandom + tracer.chain.pendingBlockContext.Random = originalRandom }) return nil, nil }, @@ -152,7 +146,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "chainId", abi.Arguments{{Type: typeUint256}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes unless this code path reverts or the whole transaction is reverted in the chain. - chainConfig := tracer.evm.ChainConfig() + chainConfig := tracer.chain.pendingBlockChainConfig original := chainConfig.ChainID chainConfig.ChainID = inputs[0].(*big.Int) tracer.CurrentCallFrame().onChainRevertRestoreHooks.Push(func() { @@ -169,7 +163,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, account := inputs[0].(common.Address) slot := inputs[1].([32]byte) value := inputs[2].([32]byte) - tracer.evm.StateDB.SetState(account, slot, value) + tracer.chain.State().SetState(account, slot, value) return nil, nil }, ) @@ -180,7 +174,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { account := inputs[0].(common.Address) slot := inputs[1].([32]byte) - value := tracer.evm.StateDB.GetState(account, slot) + value := tracer.chain.State().GetState(account, slot) return []any{value}, nil }, ) @@ -191,7 +185,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { account := inputs[0].(common.Address) code := inputs[1].([]byte) - tracer.evm.StateDB.SetCode(account, code) + tracer.chain.State().SetCode(account, code) return nil, nil }, ) @@ -202,9 +196,9 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { account := inputs[0].(common.Address) newBalance := inputs[1].(*big.Int) - originalBalance := tracer.evm.StateDB.GetBalance(account) - diff := new(big.Int).Sub(newBalance, originalBalance) - tracer.evm.StateDB.AddBalance(account, diff) + newBalanceUint256 := new(uint256.Int) + newBalanceUint256.SetFromBig(newBalance) + tracer.chain.State().SetBalance(account, newBalanceUint256, tracing.BalanceChangeUnspecified) return nil, nil }, ) @@ -214,7 +208,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "getNonce", abi.Arguments{{Type: typeAddress}}, abi.Arguments{{Type: typeUint64}}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { account := inputs[0].(common.Address) - nonce := tracer.evm.StateDB.GetNonce(account) + nonce := tracer.chain.State().GetNonce(account) return []any{nonce}, nil }, ) @@ -225,7 +219,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { account := inputs[0].(common.Address) nonce := inputs[1].(uint64) - tracer.evm.StateDB.SetNonce(account, nonce) + tracer.chain.State().SetNonce(account, nonce) return nil, nil }, ) @@ -235,10 +229,10 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "coinbase", abi.Arguments{{Type: typeAddress}}, abi.Arguments{}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { // Maintain our changes until the transaction exits. - original := tracer.evm.Context.Coinbase - tracer.evm.Context.Coinbase = inputs[0].(common.Address) + original := tracer.chain.pendingBlockContext.Coinbase + tracer.chain.pendingBlockContext.Coinbase = inputs[0].(common.Address) tracer.CurrentCallFrame().onTopFrameExitRestoreHooks.Push(func() { - tracer.evm.Context.Coinbase = original + tracer.chain.pendingBlockContext.Coinbase = original }) return nil, nil }, @@ -255,10 +249,12 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, // We entered the scope we want to prank, store the original value, patch, and add a hook to restore it // when this frame is exited. prankCallFrame := tracer.CurrentCallFrame() - original := prankCallFrame.vmScope.Contract.CallerAddress - prankCallFrame.vmScope.Contract.CallerAddress = inputs[0].(common.Address) + // We can cast OpContext to ScopeContext because that is the type passed to OnOpcode. + scopeContext := prankCallFrame.vmScope.(*vm.ScopeContext) + original := scopeContext.Caller() + scopeContext.Contract.CallerAddress = inputs[0].(common.Address) prankCallFrame.onFrameExitRestoreHooks.Push(func() { - prankCallFrame.vmScope.Contract.CallerAddress = original + scopeContext.Contract.CallerAddress = original }) }) return nil, nil @@ -274,10 +270,12 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, cheatCodeCallerFrame := tracer.PreviousCallFrame() // Store the original value, patch, and add a hook to restore it when this frame is exited. - original := cheatCodeCallerFrame.vmScope.Contract.CallerAddress - cheatCodeCallerFrame.vmScope.Contract.CallerAddress = inputs[0].(common.Address) + // We can cast OpContext to ScopeContext because that is the type passed to OnOpcode. + scopeContext := cheatCodeCallerFrame.vmScope.(*vm.ScopeContext) + original := scopeContext.Caller() + scopeContext.Contract.CallerAddress = inputs[0].(common.Address) cheatCodeCallerFrame.onFrameExitRestoreHooks.Push(func() { - cheatCodeCallerFrame.vmScope.Contract.CallerAddress = original + scopeContext.Contract.CallerAddress = original }) return nil, nil }, @@ -287,7 +285,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, contract.addMethod( "snapshot", abi.Arguments{}, abi.Arguments{{Type: typeUint256}}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { - snapshotID := tracer.evm.StateDB.Snapshot() + snapshotID := tracer.chain.State().Snapshot() return []any{snapshotID}, nil }, @@ -298,7 +296,7 @@ func getStandardCheatCodeContract(tracer *cheatCodeTracer) (*CheatCodeContract, "revertTo", abi.Arguments{{Type: typeUint256}}, abi.Arguments{{Type: typeBool}}, func(tracer *cheatCodeTracer, inputs []any) ([]any, *cheatCodeRawReturnData) { snapshotID := inputs[0].(*big.Int) - tracer.evm.StateDB.RevertToSnapshot(int(snapshotID.Int64())) + tracer.chain.State().RevertToSnapshot(int(snapshotID.Int64())) return []any{true}, nil }, diff --git a/chain/test_chain.go b/chain/test_chain.go index a23ac5d9..66e7cc5d 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -3,11 +3,17 @@ package chain import ( "errors" "fmt" + "math/big" + "sort" + "github.com/crytic/medusa/chain/config" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/triedb" + "github.com/ethereum/go-ethereum/triedb/hashdb" + "github.com/holiman/uint256" "golang.org/x/exp/maps" - "math/big" - "sort" chainTypes "github.com/crytic/medusa/chain/types" "github.com/crytic/medusa/chain/vendored" @@ -19,9 +25,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" ) // TestChain represents a simulated Ethereum chain used for testing. It maintains blocks in-memory and strips away @@ -35,6 +39,14 @@ type TestChain struct { // pendingBlock is a block currently under construction by the chain which has not yet been committed. pendingBlock *chainTypes.Block + // pendingBlockContext is the vm.BlockContext for the current pending block. This is used by cheatcodes to override the EVM + // interpreter's behavior. This should be set when a new EVM is created by the test chain e.g. using vm.NewEVM. + pendingBlockContext *vm.BlockContext + + // pendingBlockChainConfig is params.ChainConfig for the current pending block. This is used by cheatcodes to override + // the chain ID. This should be set when a new EVM is created by the test chain e.g. using vm.NewEVM. + pendingBlockChainConfig *params.ChainConfig + // BlockGasLimit defines the maximum amount of gas that can be consumed by transactions in a block. // Transactions which push the block gas usage beyond this limit will not be added to a block without error. BlockGasLimit uint64 @@ -64,14 +76,11 @@ type TestChain struct { // This is constructed over the kvstore. db ethdb.Database - // keyValueStore represents the underlying key-value store used to construct the db. - keyValueStore *memorydb.Database - - // callTracerRouter forwards vm.EVMLogger and TestChainTracer calls to any instances added to it. This + // callTracerRouter forwards tracers.Tracer and TestChainTracer calls to any instances added to it. This // router is used for non-state changing calls. callTracerRouter *TestChainTracerRouter - // transactionTracerRouter forwards vm.EVMLogger and TestChainTracer calls to any instances added to it. This + // transactionTracerRouter forwards tracers.Tracer and TestChainTracer calls to any instances added to it. This // router is used for transaction execution when constructing blocks. transactionTracerRouter *TestChainTracerRouter @@ -82,20 +91,21 @@ type TestChain struct { // NewTestChain creates a simulated Ethereum backend used for testing, or returns an error if one occurred. // This creates a test chain with a test chain configuration and the provided genesis allocation and config. // If a nil config is provided, a default one is used. -func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestChainConfig) (*TestChain, error) { +func NewTestChain(genesisAlloc types.GenesisAlloc, testChainConfig *config.TestChainConfig) (*TestChain, error) { // Copy our chain config, so it is not shared across chains. chainConfig, err := utils.CopyChainConfig(params.TestChainConfig) if err != nil { return nil, err } - // TODO: go-ethereum doesn't set shanghai start time for THEIR test `ChainConfig` struct. + // TODO: go-ethereum doesn't set cancun start time for THEIR test `ChainConfig` struct. // Note: We have our own `TestChainConfig` definition that is different (second argument in this function). // We should allow the user to provide a go-ethereum `ChainConfig` to do custom fork selection, inside of our // `TestChainConfig` definition. Or we should wrap it in our own struct to simplify the options and not pollute // our overall medusa project config. - shanghaiTime := uint64(0) - chainConfig.ShanghaiTime = &shanghaiTime + cancunTime := uint64(0) + chainConfig.ShanghaiTime = &cancunTime + chainConfig.CancunTime = &cancunTime // Create our genesis definition with our default chain config. genesisDefinition := &core.Genesis{ @@ -127,19 +137,20 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh // Obtain our VM extensions from our config vmConfigExtensions := testChainConfig.GetVMConfigExtensions() - // Obtain our cheatcode providers - cheatTracer, cheatContracts, err := getCheatCodeProviders() - if err != nil { - return nil, err - } - // Add all cheat code contract addresses to the genesis config. This is done because cheat codes are implemented // as pre-compiles, but we still want code to exist at these addresses, because smart contracts compiled with // newer solidity versions perform code size checks prior to external calls. // Additionally, add the pre-compiled cheat code contract to our vm extensions. + var cheatTracer *cheatCodeTracer if testChainConfig.CheatCodeConfig.CheatCodesEnabled { + // Obtain our cheatcode providers + var cheatContracts []*CheatCodeContract + cheatTracer, cheatContracts, err = getCheatCodeProviders() + if err != nil { + return nil, err + } for _, cheatContract := range cheatContracts { - genesisDefinition.Alloc[cheatContract.address] = core.GenesisAccount{ + genesisDefinition.Alloc[cheatContract.address] = types.Account{ Balance: big.NewInt(0), Code: []byte{0xFF}, } @@ -148,19 +159,22 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh } // Create an in-memory database - keyValueStore := memorydb.New() - db := rawdb.NewDatabase(keyValueStore) + db := rawdb.NewMemoryDatabase() + dbConfig := &triedb.Config{ + HashDB: hashdb.Defaults, + // TODO Add cleanCacheSize of 256 depending on the resolution of this issue https://github.com/ethereum/go-ethereum/issues/30099 + // PathDB: pathdb.Defaults, + } + trieDB := triedb.NewDatabase(db, dbConfig) // Commit our genesis definition to get a genesis block. - genesisBlock := genesisDefinition.MustCommit(db) + genesisBlock := genesisDefinition.MustCommit(db, trieDB) // Convert our genesis block (go-ethereum type) to a test chain block. testChainGenesisBlock := chainTypes.NewBlock(genesisBlock.Header()) // Create our state database over-top our database. - stateDatabase := state.NewDatabaseWithConfig(db, &trie.Config{ - Cache: 256, // this is important in keeping the database performant, so it does not need to fetch repetitively. - }) + stateDatabase := state.NewDatabaseWithConfig(db, dbConfig) // Create a tracer forwarder to support the addition of multiple tracers for transaction and call execution. transactionTracerRouter := NewTestChainTracerRouter() @@ -172,7 +186,6 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh BlockGasLimit: genesisBlock.Header().GasLimit, blocks: []*chainTypes.Block{testChainGenesisBlock}, pendingBlock: nil, - keyValueStore: keyValueStore, db: db, state: nil, stateDatabase: stateDatabase, @@ -184,9 +197,9 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh } // Add our internal tracers to this chain. - chain.AddTracer(newTestChainDeploymentsTracer(), true, false) + chain.AddTracer(newTestChainDeploymentsTracer().NativeTracer(), true, false) if testChainConfig.CheatCodeConfig.CheatCodesEnabled { - chain.AddTracer(cheatTracer, true, true) + chain.AddTracer(cheatTracer.NativeTracer(), true, true) cheatTracer.bindToChain(chain) } @@ -195,6 +208,9 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh if err != nil { return nil, err } + + // Set our state database logger e.g. to monitor OnCodeChange events. + stateDB.SetLogger(transactionTracerRouter.NativeTracer().Tracer.Hooks) chain.state = stateDB return chain, nil } @@ -204,7 +220,7 @@ func NewTestChain(genesisAlloc core.GenesisAlloc, testChainConfig *config.TestCh // object from being freed by the garbage collector and causes a severe memory leak. func (t *TestChain) Close() { // Reset the state DB's cache - t.stateDatabase.TrieDB().ResetCache() + t.stateDatabase.TrieDB().Close() } // Clone recreates the current TestChain state into a new instance. This simply reconstructs the block/chain state @@ -240,7 +256,7 @@ func (t *TestChain) Clone(onCreateFunc func(chain *TestChain) error) (*TestChain // Now add each transaction/message to it. messages := t.blocks[i].Messages for j := 0; j < len(messages); j++ { - err = targetChain.PendingBlockAddTx(messages[j]) + err = targetChain.PendingBlockAddTx(messages[j], nil) if err != nil { return nil, err } @@ -265,9 +281,9 @@ func (t *TestChain) Clone(onCreateFunc func(chain *TestChain) error) (*TestChain return targetChain, nil } -// AddTracer adds a given vm.EVMLogger or TestChainTracer to the TestChain. If directed, the tracer will be attached +// AddTracer adds a given tracers.Tracer or TestChainTracer to the TestChain. If directed, the tracer will be attached // for transactions and/or non-state changing calls made via CallContract. -func (t *TestChain) AddTracer(tracer vm.EVMLogger, txs bool, calls bool) { +func (t *TestChain) AddTracer(tracer *TestChainTracer, txs bool, calls bool) { if txs { t.transactionTracerRouter.AddTracer(tracer) } @@ -543,7 +559,7 @@ func (t *TestChain) RevertToBlockNumber(blockNumber uint64) error { // It takes an optional state argument, which is the state to execute the message over. If not provided, the // current pending state (or committed state if none is pending) will be used instead. // The state executed over may be a pending block state. -func (t *TestChain) CallContract(msg *core.Message, state *state.StateDB, additionalTracers ...vm.EVMLogger) (*core.ExecutionResult, error) { +func (t *TestChain) CallContract(msg *core.Message, state *state.StateDB, additionalTracers ...*TestChainTracer) (*core.ExecutionResult, error) { // If our provided state is nil, use our current chain state. if state == nil { state = t.state @@ -553,8 +569,7 @@ func (t *TestChain) CallContract(msg *core.Message, state *state.StateDB, additi snapshot := state.Snapshot() // Set infinite balance to the fake caller account - from := state.GetOrNewStateObject(msg.From) - from.SetBalance(math.MaxBig256) + state.SetBalance(msg.From, uint256.MustFromBig(math.MaxBig256), tracing.BalanceChangeUnspecified) // Create our transaction and block contexts for the vm txContext := core.NewEVMTxContext(msg) @@ -563,27 +578,52 @@ func (t *TestChain) CallContract(msg *core.Message, state *state.StateDB, additi // Create a new call tracer router that incorporates any additional tracers provided just for this call, while // still calling our internal tracers. extendedTracerRouter := NewTestChainTracerRouter() - extendedTracerRouter.AddTracer(t.callTracerRouter) + extendedTracerRouter.AddTracer(t.callTracerRouter.NativeTracer()) extendedTracerRouter.AddTracers(additionalTracers...) // Create our EVM instance. evm := vm.NewEVM(blockContext, txContext, state, t.chainConfig, vm.Config{ - //Debug: true, - Tracer: extendedTracerRouter, + Tracer: extendedTracerRouter.NativeTracer().Tracer.Hooks, NoBaseFee: true, ConfigExtensions: t.vmConfigExtensions, }) + // Set our block context and chain config in order for cheatcodes to override what EVM interpreter sees. + t.pendingBlockContext = &evm.Context + t.pendingBlockChainConfig = evm.ChainConfig() + + // Create a tx from our msg, for hashing/receipt purposes + tx := utils.MessageToTransaction(msg) + // Need to explicitly call OnTxStart hook + if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxStart != nil { + evm.Config.Tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) + } // Fund the gas pool, so it can execute endlessly (no block gas limit). gasPool := new(core.GasPool).AddGas(math.MaxUint64) // Perform our state transition to obtain the result. - res, err := core.NewStateTransition(evm, msg, gasPool).TransitionDb() + msgResult, err := core.ApplyMessage(evm, msg, gasPool) // Revert to our state snapshot to undo any changes. state.RevertToSnapshot(snapshot) - return res, err + // Gather receipt for OnTxEnd + receipt := &types.Receipt{Type: tx.Type()} + if msgResult.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + receipt.TxHash = tx.Hash() + receipt.GasUsed = msgResult.UsedGas + + // HACK: use OnTxEnd to store the execution trace. + // Need to explicitly call OnTxEnd + if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxEnd != nil { + evm.Config.Tracer.OnTxEnd(receipt, err) + } + + return msgResult, err } // PendingBlock describes the current pending block which is being constructed and awaiting commitment to the chain. @@ -687,16 +727,21 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi // PendingBlockAddTx takes a message (internal txs) and adds it to the current pending block, updating the header // with relevant execution information. If a pending block was not created, an error is returned. -// Returns the constructed block, or an error if one occurred. -func (t *TestChain) PendingBlockAddTx(message *core.Message) error { +// Returns an error if one occurred. +func (t *TestChain) PendingBlockAddTx(message *core.Message, getTracerFn func(txIndex int, txHash common.Hash) *tracers.Tracer) error { + // Caller can specify any tracer they wish to use for this transaction + if getTracerFn == nil { + // TODO: Figure out whether it is possible to identify _which_ transaction you want to trace (versus all) + getTracerFn = func(txIndex int, txHash common.Hash) *tracers.Tracer { + return t.transactionTracerRouter.NativeTracer().Tracer + } + } + // If we don't have a pending block, return an error if t.pendingBlock == nil { return errors.New("could not add tx to the chain's pending block because no pending block was created") } - // Obtain our state root hash prior to execution. - previousStateRoot := t.pendingBlock.Header.Root - // Create a gas pool indicating how much gas can be spent executing the transaction. gasPool := new(core.GasPool).AddGas(t.pendingBlock.Header.GasLimit - t.pendingBlock.Header.GasUsed) @@ -706,28 +751,39 @@ func (t *TestChain) PendingBlockAddTx(message *core.Message) error { // Create a new context to be used in the EVM environment blockContext := newTestChainBlockContext(t, t.pendingBlock.Header) - // Create our EVM instance. - evm := vm.NewEVM(blockContext, core.NewEVMTxContext(message), t.state, t.chainConfig, vm.Config{ - //Debug: true, - Tracer: t.transactionTracerRouter, + // Create our VM config + vmConfig := vm.Config{ NoBaseFee: true, ConfigExtensions: t.vmConfigExtensions, - }) + } + + // Set the tracer to be used in the vm config + tracer := getTracerFn(len(t.pendingBlock.Messages), tx.Hash()) + if tracer != nil { + vmConfig.Tracer = tracer.Hooks + } + + // Set tx context + t.state.SetTxContext(tx.Hash(), len(t.pendingBlock.Messages)) + + // Create our EVM instance. + evm := vm.NewEVM(blockContext, core.NewEVMTxContext(message), t.state, t.chainConfig, vmConfig) + + // Set our block context and chain config in order for cheatcodes to override what EVM interpreter sees. + t.pendingBlockContext = &evm.Context + t.pendingBlockChainConfig = evm.ChainConfig() // Apply our transaction var usedGas uint64 - t.state.SetTxContext(tx.Hash(), len(t.pendingBlock.Messages)) receipt, executionResult, err := vendored.EVMApplyTransaction(message, t.chainConfig, &t.pendingBlock.Header.Coinbase, gasPool, t.state, t.pendingBlock.Header.Number, t.pendingBlock.Hash, tx, &usedGas, evm) + if err != nil { - // If we encountered an error, reset our state, as we couldn't add the tx. - t.state, _ = state.New(t.pendingBlock.Header.Root, t.stateDatabase, nil) return fmt.Errorf("test chain state write error when adding tx to pending block: %v", err) } // Create our message result messageResult := &chainTypes.MessageResults{ - PreStateRoot: previousStateRoot, - PostStateRoot: common.Hash{}, + PostStateRoot: common.BytesToHash(receipt.PostState), ExecutionResult: executionResult, Receipt: receipt, AdditionalResults: make(map[string]any, 0), @@ -736,31 +792,10 @@ func (t *TestChain) PendingBlockAddTx(message *core.Message) error { // For every tracer we have, we call upon them to set their results for this transaction now. t.transactionTracerRouter.CaptureTxEndSetAdditionalResults(messageResult) - // Write state changes to database. - // NOTE: If this completes without an error, we know we didn't hit the block gas limit or other errors, so we are - // safe to update the block header afterwards. - root, err := t.state.Commit(t.chainConfig.IsEIP158(t.pendingBlock.Header.Number)) - if err != nil { - return fmt.Errorf("test chain state write error: %v", err) - } - if err := t.state.Database().TrieDB().Commit(root, false); err != nil { - // If we encountered an error, reset our state, as we couldn't add the tx. - t.state, _ = state.New(t.pendingBlock.Header.Root, t.stateDatabase, nil) - return fmt.Errorf("test chain trie write error: %v", err) - } - // Update our gas used in the block header t.pendingBlock.Header.GasUsed += receipt.GasUsed - // Update our block's bloom filter t.pendingBlock.Header.Bloom.Add(receipt.Bloom.Bytes()) - - // Update the header's state root hash, as well as our message result's - // Note: You could also retrieve the root without committing by using - // state.IntermediateRoot(config.IsEIP158(parentBlockNumber)). - t.pendingBlock.Header.Root = root - messageResult.PostStateRoot = root - // Update our block's transactions and results. t.pendingBlock.Messages = append(t.pendingBlock.Messages, message) t.pendingBlock.MessageResults = append(t.pendingBlock.MessageResults, messageResult) @@ -792,6 +827,25 @@ func (t *TestChain) PendingBlockCommit() error { return fmt.Errorf("could not commit chain's pending block, as no pending block was created") } + // Perform a state commit to obtain the root hash for our block. + root, err := t.state.Commit(t.pendingBlock.Header.Number.Uint64(), true) + t.pendingBlock.Header.Root = root + + if err != nil { + return err + } + + // Committing the state invalidates the cached tries and we need to reload the state. + // Otherwise, methods such as FillFromTestChainProperties will not work correctly. + t.state, err = state.New(root, t.stateDatabase, nil) + if err != nil { + return err + } + + // Discard the test chain's reference to the EVM interpreter's block context and chain config. + t.pendingBlockContext = nil + t.pendingBlockChainConfig = nil + // Append our new block to our chain. t.blocks = append(t.blocks, t.pendingBlock) @@ -800,7 +854,7 @@ func (t *TestChain) PendingBlockCommit() error { t.pendingBlock = nil // Emit our event for committing a new block as the chain head - err := t.Events.PendingBlockCommitted.Publish(PendingBlockCommittedEvent{ + err = t.Events.PendingBlockCommitted.Publish(PendingBlockCommittedEvent{ Chain: t, Block: pendingBlock, }) @@ -821,6 +875,8 @@ func (t *TestChain) PendingBlockDiscard() error { // Clear our pending block, but keep a copy of it to emit our event pendingBlock := t.pendingBlock t.pendingBlock = nil + t.pendingBlockContext = nil + t.pendingBlockChainConfig = nil // Emit our contract change events for the messages reverted err := t.emitContractChangeEvents(true, pendingBlock.MessageResults...) diff --git a/chain/test_chain_deployments_tracer.go b/chain/test_chain_deployments_tracer.go index d8bec11f..2687356d 100644 --- a/chain/test_chain_deployments_tracer.go +++ b/chain/test_chain_deployments_tracer.go @@ -1,10 +1,15 @@ package chain import ( + "math/big" + "github.com/crytic/medusa/chain/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + coretypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "math/big" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/params/forks" ) // testChainDeploymentsTracer implements TestChainTracer, capturing information regarding contract deployments and @@ -17,16 +22,16 @@ type testChainDeploymentsTracer struct { // callDepth refers to the current EVM depth during tracing. callDepth uint64 - // evm refers to the EVM instance last captured. - evm *vm.EVM + // evm refers to the last tracing.VMContext captured. + evmContext *tracing.VMContext // pendingCallFrames represents per-call-frame data deployment information being captured by the tracer. // This is committed as each call frame succeeds, so that contract deployments which later encountered an error // and reverted are not considered. The index of each element in the array represents its call frame depth. pendingCallFrames []*testChainDeploymentsTracerCallFrame - // selfDestructDestroysCode indicates whether the SELFDESTRUCT opcode is configured to remove contract code. - selfDestructDestroysCode bool + // nativeTracer is the underlying tracer interface that the deployment tracer follows + nativeTracer *TestChainTracer } // testChainDeploymentsTracerCallFrame represents per-call-frame data traced by a testChainDeploymentsTracer. @@ -37,37 +42,57 @@ type testChainDeploymentsTracerCallFrame struct { // newTestChainDeploymentsTracer creates a testChainDeploymentsTracer func newTestChainDeploymentsTracer() *testChainDeploymentsTracer { - tracer := &testChainDeploymentsTracer{ - selfDestructDestroysCode: true, // TODO: Update this when new EIP is introduced by checking the chain config. + tracer := &testChainDeploymentsTracer{} + innerTracer := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: tracer.OnTxStart, + OnTxEnd: tracer.OnTxEnd, + OnEnter: tracer.OnEnter, + OnExit: tracer.OnExit, + OnOpcode: tracer.OnOpcode, + }, } + tracer.nativeTracer = &TestChainTracer{Tracer: innerTracer, CaptureTxEndSetAdditionalResults: tracer.CaptureTxEndSetAdditionalResults} + return tracer + +} + +// NativeTracer returns the underlying TestChainTracer. +func (t *testChainDeploymentsTracer) NativeTracer() *TestChainTracer { + return t.nativeTracer } -// CaptureTxStart is called upon the start of transaction execution, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureTxStart(gasLimit uint64) { - // Reset our capture state - t.callDepth = 0 +// OnTxStart is called upon the start of transaction execution, as defined by tracers.Tracer. +func (t *testChainDeploymentsTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) { + // Reset our tracer state t.results = make([]types.DeployedContractBytecodeChange, 0) t.pendingCallFrames = make([]*testChainDeploymentsTracerCallFrame, 0) + + // Store our evm reference + t.evmContext = vm } -// CaptureTxEnd is called upon the end of transaction execution, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureTxEnd(restGas uint64) { +// OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer. +func (t *testChainDeploymentsTracer) OnTxEnd(receipt *coretypes.Receipt, err error) { } -// CaptureStart initializes the tracing operation for the top of a call frame, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - // Store our evm reference - t.evm = env - - // Create our call frame struct to track data for this initial entry call frame. +// OnEnter is called upon entering of the call frame, as defined by tracers.Tracer. +func (t *testChainDeploymentsTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Create our call frame struct to track data for this call frame. callFrameData := &testChainDeploymentsTracerCallFrame{} t.pendingCallFrames = append(t.pendingCallFrames, callFrameData) + // Update call depth if this is not the top-level call frame + isTopLevelFrame := depth == 0 + if !isTopLevelFrame { + t.callDepth++ + } + // If this is a contract creation, record the `to` address as a pending deployment (if it succeeds upon exit, // we commit it). - if create { + if typ == byte(vm.CREATE) || typ == byte(vm.CREATE2) { callFrameData.results = append(callFrameData.results, types.DeployedContractBytecodeChange{ Contract: &types.DeployedContractBytecode{ Address: to, @@ -75,104 +100,65 @@ func (t *testChainDeploymentsTracer) CaptureStart(env *vm.EVM, from common.Addre RuntimeBytecode: nil, }, Creation: true, - DynamicCreation: false, + DynamicCreation: !isTopLevelFrame, // If we're not at the top level, this is a dynamic creation. SelfDestructed: false, Destroyed: false, }) } } -// CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { +// OnExit is called after a call to finalize tracing completes for the top of a call frame, as defined by tracers.Tracer. +func (t *testChainDeploymentsTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + // Check to see if this is the top level call frame + isTopLevelFrame := depth == 0 + // Fetch runtime bytecode for all deployments in this frame which did not record one, before exiting. // We had to fetch it upon exit as it does not exist during creation of course. for _, contractChange := range t.pendingCallFrames[t.callDepth].results { if contractChange.Creation && contractChange.Contract.RuntimeBytecode == nil { - contractChange.Contract.RuntimeBytecode = t.evm.StateDB.GetCode(contractChange.Contract.Address) + contractChange.Contract.RuntimeBytecode = t.evmContext.StateDB.GetCode(contractChange.Contract.Address) } } - // If we didn't encounter an error in this call frame, we're at the end, so we commit all results. - if err == nil { + // If we didn't encounter any errors and this is the top level call frame, commit all the results + if isTopLevelFrame { t.results = append(t.results, t.pendingCallFrames[t.callDepth].results...) - } - - // We're exiting the current frame, so remove our frame data. - t.pendingCallFrames = t.pendingCallFrames[:t.callDepth] -} - -// CaptureEnter is called upon entering of the call frame, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - // Increase our call depth now that we're entering a new call frame. - t.callDepth++ - - // Create our call frame struct to track data for this initial entry call frame. - callFrameData := &testChainDeploymentsTracerCallFrame{} - t.pendingCallFrames = append(t.pendingCallFrames, callFrameData) - - // If this is a contract creation, record the `to` address as a pending deployment (if it succeeds upon exit, - // we commit it). - if typ == vm.CREATE || typ == vm.CREATE2 { - callFrameData.results = append(callFrameData.results, types.DeployedContractBytecodeChange{ - Contract: &types.DeployedContractBytecode{ - Address: to, - InitBytecode: input, - RuntimeBytecode: nil, - }, - Creation: true, - DynamicCreation: true, - SelfDestructed: false, - Destroyed: false, - }) - } -} - -// CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - // Fetch runtime bytecode for all deployments in this frame which did not record one, before exiting. - // We had to fetch it upon exit as it does not exist during creation of course. - for _, contractChange := range t.pendingCallFrames[t.callDepth].results { - if contractChange.Creation && contractChange.Contract.RuntimeBytecode == nil { - contractChange.Contract.RuntimeBytecode = t.evm.StateDB.GetCode(contractChange.Contract.Address) + } else { + // If we didn't encounter an error in this call frame, we push our captured data up one frame. + if err == nil { + t.pendingCallFrames[t.callDepth-1].results = append(t.pendingCallFrames[t.callDepth-1].results, t.pendingCallFrames[t.callDepth].results...) } - } - // If we didn't encounter an error in this call frame, we push our captured data up one frame. - if err == nil { - t.pendingCallFrames[t.callDepth-1].results = append(t.pendingCallFrames[t.callDepth-1].results, t.pendingCallFrames[t.callDepth].results...) + // We're exiting the current frame, so remove our frame data and decrement the call depth. + t.pendingCallFrames = t.pendingCallFrames[:t.callDepth] + t.callDepth-- } - // We're exiting the current frame, so remove our frame data. - t.pendingCallFrames = t.pendingCallFrames[:t.callDepth] - - // Decrease our call depth now that we've exited a call frame. - t.callDepth-- } -// CaptureState records data from an EVM state update, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, vmErr error) { +// OnOpcode records data from an EVM state update, as defined by tracers.Tracer. +func (t *testChainDeploymentsTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { // If we encounter a SELFDESTRUCT operation, record the change to our contract in our results. - if op == vm.SELFDESTRUCT { + if op == byte(vm.SELFDESTRUCT) { callFrameData := t.pendingCallFrames[t.callDepth] + addr := scope.Address() + code := t.evmContext.StateDB.GetCode(addr) callFrameData.results = append(callFrameData.results, types.DeployedContractBytecodeChange{ Contract: &types.DeployedContractBytecode{ - Address: scope.Contract.Address(), + Address: addr, InitBytecode: nil, - RuntimeBytecode: t.evm.StateDB.GetCode(scope.Contract.Address()), + RuntimeBytecode: code, }, Creation: false, DynamicCreation: false, SelfDestructed: true, - Destroyed: t.selfDestructDestroysCode, + // Check if this is a new contract (not previously deployed and self destructed). + // https://github.com/ethereum/go-ethereum/blob/8d42e115b1cae4f09fd02b71c06ec9c85f22ad4f/core/state/statedb.go#L504-L506 + Destroyed: t.evmContext.ChainConfig.LatestFork(t.evmContext.Time) < forks.Cancun || !t.evmContext.StateDB.Exist(addr), }) } } -// CaptureFault records an execution fault, as defined by vm.EVMLogger. -func (t *testChainDeploymentsTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { - -} - // CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this // tracer is used during transaction execution (block creation), the results can later be queried from the block. // This method will only be called on the added tracer if it implements the extended TestChainTracer interface. diff --git a/chain/test_chain_test.go b/chain/test_chain_test.go index 5c8e6449..693260fc 100644 --- a/chain/test_chain_test.go +++ b/chain/test_chain_test.go @@ -64,12 +64,12 @@ func createChain(t *testing.T) (*TestChain, []common.Address) { assert.NoError(t, err) // NOTE: Sharing GenesisAlloc between nodes will result in some accounts not being funded for some reason. - genesisAlloc := make(core.GenesisAlloc) + genesisAlloc := make(types.GenesisAlloc) // Fund all of our sender addresses in the genesis block initBalance := new(big.Int).Div(abi.MaxInt256, big.NewInt(2)) for _, sender := range senders { - genesisAlloc[sender] = core.GenesisAccount{ + genesisAlloc[sender] = types.Account{ Balance: initBalance, } } @@ -260,7 +260,7 @@ func TestChainDynamicDeployments(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg) + err = chain.PendingBlockAddTx(&msg, nil) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -385,7 +385,7 @@ func TestChainDeploymentWithArgs(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg) + err = chain.PendingBlockAddTx(&msg, nil) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -494,7 +494,7 @@ func TestChainCloning(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg) + err = chain.PendingBlockAddTx(&msg, nil) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -588,7 +588,7 @@ func TestChainCallSequenceReplayMatchSimple(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg) + err = chain.PendingBlockAddTx(&msg, nil) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -627,7 +627,7 @@ func TestChainCallSequenceReplayMatchSimple(t *testing.T) { _, err := recreatedChain.PendingBlockCreate() assert.NoError(t, err) for _, message := range chain.blocks[i].Messages { - err = recreatedChain.PendingBlockAddTx(message) + err = recreatedChain.PendingBlockAddTx(message, nil) assert.NoError(t, err) } err = recreatedChain.PendingBlockCommit() diff --git a/chain/test_chain_tracer.go b/chain/test_chain_tracer.go index fc21a5cc..775cd07f 100644 --- a/chain/test_chain_tracer.go +++ b/chain/test_chain_tracer.go @@ -1,118 +1,148 @@ package chain import ( + "math/big" + "github.com/crytic/medusa/chain/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + coretypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" "golang.org/x/exp/slices" - "math/big" ) -// TestChainTracer is an extended vm.EVMLogger which can be used with a TestChain to store any captured +// TestChainTracer is an extended tracers.Tracer which can be used with a TestChain to store any captured // information within call results, recorded in each block produced. -type TestChainTracer interface { - // EVMLogger is extended by this logger. - vm.EVMLogger +type TestChainTracer struct { + // tracers.Tracer is extended by this logger. + *tracers.Tracer // CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this // tracer is used during transaction execution (block creation), the results can later be queried from the block. // This method will only be called on the added tracer if it implements the extended TestChainTracer interface. - CaptureTxEndSetAdditionalResults(results *types.MessageResults) + CaptureTxEndSetAdditionalResults func(results *types.MessageResults) } -// TestChainTracerRouter acts as a vm.EVMLogger or TestChainTracer splitter, allowing multiple tracers to be used in +// TestChainTracerRouter acts as a tracers.Tracer, allowing multiple tracers to be used in // place of one. When this tracer receives callback, it calls upon its underlying tracers. type TestChainTracerRouter struct { - // tracers refers to the internally recorded vm.EVMLogger instances to route all calls to. - tracers []vm.EVMLogger + // tracers refers to the internally recorded tracers.Tracer instances to route all calls to. + tracers []*TestChainTracer + nativeTracer *TestChainTracer } // NewTestChainTracerRouter returns a new TestChainTracerRouter instance with no registered tracers. func NewTestChainTracerRouter() *TestChainTracerRouter { - return &TestChainTracerRouter{ - tracers: make([]vm.EVMLogger, 0), + tracer := &TestChainTracerRouter{ + tracers: make([]*TestChainTracer, 0), } + innerTracer := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: tracer.OnTxStart, + OnTxEnd: tracer.OnTxEnd, + OnEnter: tracer.OnEnter, + OnExit: tracer.OnExit, + OnOpcode: tracer.OnOpcode, + }, + } + tracer.nativeTracer = &TestChainTracer{Tracer: innerTracer, CaptureTxEndSetAdditionalResults: tracer.CaptureTxEndSetAdditionalResults} + + return tracer + } -// AddTracer adds a vm.EVMLogger or TestChainTracer to the TestChainTracerRouter, so all vm.EVMLogger relates calls +// NativeTracer returns the underlying TestChainTracer. +func (t *TestChainTracerRouter) NativeTracer() *TestChainTracer { + return t.nativeTracer +} + +// AddTracer adds a TestChainTracer to the TestChainTracerRouter so that all other tracing.Hooks calls are forwarded. // are forwarded to it. -func (t *TestChainTracerRouter) AddTracer(tracer vm.EVMLogger) { +func (t *TestChainTracerRouter) AddTracer(tracer *TestChainTracer) { t.AddTracers(tracer) } -// AddTracers adds vm.EVMLogger implementations to the TestChainTracerRouter so all other method calls are forwarded -// to them. -func (t *TestChainTracerRouter) AddTracers(tracers ...vm.EVMLogger) { +// AddTracers adds TestChainTracers to the TestChainTracerRouter so that all other tracing.Hooks calls are forwarded. +func (t *TestChainTracerRouter) AddTracers(tracers ...*TestChainTracer) { t.tracers = append(t.tracers, tracers...) } -// Tracers returns the vm.EVMLogger instances added to the TestChainTracerRouter. -func (t *TestChainTracerRouter) Tracers() []vm.EVMLogger { +// Tracers returns the tracers.Tracer instances added to the TestChainTracerRouter. +func (t *TestChainTracerRouter) Tracers() []*TestChainTracer { return slices.Clone(t.tracers) } -// CaptureTxStart is called upon the start of transaction execution, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureTxStart(gasLimit uint64) { +// OnTxStart is called upon the start of transaction execution, as defined by tracers.Tracer. +func (t *TestChainTracerRouter) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - tracer.CaptureTxStart(gasLimit) + if tracer.OnTxStart != nil { + tracer.OnTxStart(vm, tx, from) + } } } -// CaptureTxEnd is called upon the end of transaction execution, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureTxEnd(restGas uint64) { +// OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer. +func (t *TestChainTracerRouter) OnTxEnd(receipt *coretypes.Receipt, err error) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - tracer.CaptureTxEnd(restGas) + if tracer.OnTxEnd != nil { + tracer.OnTxEnd(receipt, err) + } } } -// CaptureStart initializes the tracing operation for the top of a call frame, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { +// OnEnter initializes the tracing operation for the top of a call frame, as defined by tracers.Tracer. +func (t *TestChainTracerRouter) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - tracer.CaptureStart(env, from, to, create, input, gas, value) + if tracer.OnEnter != nil { + tracer.OnEnter(depth, typ, from, to, input, gas, value) + } } } -// CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureEnd(output []byte, gasUsed uint64, err error) { +// OnExit is called after a call to finalize tracing completes for the top of a call frame, as defined by tracers.Tracer. +func (t *TestChainTracerRouter) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - tracer.CaptureEnd(output, gasUsed, err) + if tracer.OnExit != nil { + tracer.OnExit(depth, output, gasUsed, err, reverted) + } } } -// CaptureEnter is called upon entering of the call frame, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +// OnOpcode records data from an EVM state update, as defined by tracers.Tracer. +func (t *TestChainTracerRouter) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - tracer.CaptureEnter(typ, from, to, input, gas, value) - } -} + if tracer.OnOpcode != nil { -// CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureExit(output []byte, gasUsed uint64, err error) { - // Call the underlying method for each registered tracer. - for _, tracer := range t.tracers { - tracer.CaptureExit(output, gasUsed, err) + tracer.OnOpcode(pc, op, gas, cost, scope, rData, depth, err) + } } } -// CaptureState records data from an EVM state update, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, vmErr error) { +// OnFault records an execution fault, as defined by tracers.Tracer. +func (t *TestChainTracerRouter) OnFault(pc uint64, op byte, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - tracer.CaptureState(pc, op, gas, cost, scope, rData, depth, vmErr) + if tracer.OnFault != nil { + tracer.OnFault(pc, op, gas, cost, scope, depth, err) + } } } -// CaptureFault records an execution fault, as defined by vm.EVMLogger. -func (t *TestChainTracerRouter) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { +func (t *TestChainTracerRouter) OnCodeChange(a common.Address, prevCodeHash common.Hash, prev []byte, codeHash common.Hash, code []byte) { // Call the underlying method for each registered tracer. + for _, tracer := range t.tracers { - tracer.CaptureFault(pc, op, gas, cost, scope, depth, err) + if tracer.OnCodeChange != nil { + tracer.OnCodeChange(a, prevCodeHash, prev, codeHash, code) + } } + } // CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this @@ -121,9 +151,8 @@ func (t *TestChainTracerRouter) CaptureFault(pc uint64, op vm.OpCode, gas, cost func (t *TestChainTracerRouter) CaptureTxEndSetAdditionalResults(results *types.MessageResults) { // Call the underlying method for each registered tracer. for _, tracer := range t.tracers { - // Try to cast each tracer to a TestChainTracer and forward the call to it. - if testChainTracer, ok := tracer.(TestChainTracer); ok { - testChainTracer.CaptureTxEndSetAdditionalResults(results) + if tracer.CaptureTxEndSetAdditionalResults != nil { + tracer.CaptureTxEndSetAdditionalResults(results) } } } diff --git a/chain/types/message_results.go b/chain/types/message_results.go index 57ac7fcc..4ce232cc 100644 --- a/chain/types/message_results.go +++ b/chain/types/message_results.go @@ -9,8 +9,6 @@ import ( // MessageResults represents metadata obtained from the execution of a CallMessage in a Block. // This contains results such as contracts deployed, and other variables tracked by a chain.TestChain. type MessageResults struct { - // PreStateRoot refers to the state root hash prior to the execution of this transaction. - PreStateRoot common.Hash // PostStateRoot refers to the state root hash after the execution of this transaction. PostStateRoot common.Hash diff --git a/chain/vendored/apply_transaction.go b/chain/vendored/apply_transaction.go index 2044bf02..6b1366bd 100644 --- a/chain/vendored/apply_transaction.go +++ b/chain/vendored/apply_transaction.go @@ -35,13 +35,23 @@ import ( // This executes on an underlying EVM and returns a transaction receipt, or an error if one occurs. // Additional changes: // - Exposed core.ExecutionResult as a return value. -func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, *ExecutionResult, error) { +func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (receipt *types.Receipt, result *ExecutionResult, err error) { + // Apply the OnTxStart and OnTxEnd hooks + if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxStart != nil { + evm.Config.Tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) + if evm.Config.Tracer.OnTxEnd != nil { + defer func() { + evm.Config.Tracer.OnTxEnd(receipt, err) + }() + } + } + // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) // Apply the transaction to the current state (included in the env). - result, err := ApplyMessage(evm, msg, gp) + result, err = ApplyMessage(evm, msg, gp) if err != nil { return nil, nil, err } @@ -57,7 +67,7 @@ func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *commo // Create a new receipt for the transaction, storing the intermediate root and gas used // by the tx. - receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} + receipt = &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} if result.Failed() { receipt.Status = types.ReceiptStatusFailed } else { diff --git a/compilation/abiutils/solidity_errors.go b/compilation/abiutils/solidity_errors.go index fd848f99..88ce7163 100644 --- a/compilation/abiutils/solidity_errors.go +++ b/compilation/abiutils/solidity_errors.go @@ -2,10 +2,12 @@ package abiutils import ( "bytes" + "errors" "fmt" + "math/big" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/core/vm" - "math/big" ) // An enum is defined below providing all `Panic(uint)` error codes returned in return data when the VM encounters @@ -37,7 +39,7 @@ func GetSolidityPanicCode(returnError error, returnData []byte, backwardsCompati } // Verify we have a revert, and our return data fits exactly the selector + uint256 - if returnError == vm.ErrExecutionReverted && len(returnData) == 4+32 { + if errors.Is(returnError, vm.ErrExecutionReverted) && len(returnData) == 4+32 { uintType, _ := abi.NewType("uint256", "", nil) panicReturnDataAbi := abi.NewMethod("Panic", "Panic", abi.Function, "", false, false, []abi.Argument{ {Name: "", Type: uintType, Indexed: false}, @@ -61,7 +63,7 @@ func GetSolidityPanicCode(returnError error, returnData []byte, backwardsCompati // If the error and return data are not representative of an Error, then nil is returned. func GetSolidityRevertErrorString(returnError error, returnData []byte) *string { // Verify we have a revert, and our return data fits the selector + additional data. - if returnError == vm.ErrExecutionReverted && len(returnData) > 4 { + if errors.Is(returnError, vm.ErrExecutionReverted) && len(returnData) > 4 { stringType, _ := abi.NewType("string", "", nil) errorReturnDataAbi := abi.NewMethod("Error", "Error", abi.Function, "", false, false, []abi.Argument{ {Name: "", Type: stringType, Indexed: false}, @@ -88,7 +90,7 @@ func GetSolidityRevertErrorString(returnError error, returnData []byte) *string func GetSolidityCustomRevertError(contractAbi *abi.ABI, returnError error, returnData []byte) (*abi.Error, []any) { // If no ABI was given or a revert was not encountered, no custom error can be extracted, or may exist, // respectively. - if returnError != vm.ErrExecutionReverted || contractAbi == nil { + if !errors.Is(returnError, vm.ErrExecutionReverted) || contractAbi == nil { return nil, nil } diff --git a/compilation/types/compiled_contract.go b/compilation/types/compiled_contract.go index 37becf15..8cb9f57a 100644 --- a/compilation/types/compiled_contract.go +++ b/compilation/types/compiled_contract.go @@ -4,9 +4,10 @@ import ( "bytes" "encoding/json" "fmt" + "strings" + "github.com/ethereum/go-ethereum/accounts/abi" "golang.org/x/exp/slices" - "strings" ) // CompiledContract represents a single contract unit from a smart contract compilation. @@ -36,7 +37,6 @@ func (c *CompiledContract) IsMatch(initBytecode []byte, runtimeBytecode []byte) // Check if we can compare init and runtime bytecode canCompareInit := len(initBytecode) > 0 && len(c.InitBytecode) > 0 canCompareRuntime := len(runtimeBytecode) > 0 && len(c.RuntimeBytecode) > 0 - // First try matching runtime bytecode contract metadata. if canCompareRuntime { // First we try to match contracts with contract metadata embedded within the smart contract. diff --git a/fuzzing/calls/call_sequence.go b/fuzzing/calls/call_sequence.go index f4e06c4f..25ef88d6 100644 --- a/fuzzing/calls/call_sequence.go +++ b/fuzzing/calls/call_sequence.go @@ -277,14 +277,9 @@ func (cse *CallSequenceElement) AttachExecutionTrace(chain *chain.TestChain, con return fmt.Errorf("failed to resolve execution trace as the chain reference is nil, indicating the call sequence element has never been executed") } - // Obtain the state prior to executing this transaction. - state, err := chain.StateFromRoot(cse.ChainReference.MessageResults().PreStateRoot) - if err != nil { - return fmt.Errorf("failed to resolve execution trace due to error loading root hash from database: %v", err) - } - + var err error // Perform our call with the given trace - _, cse.ExecutionTrace, err = executiontracer.CallWithExecutionTrace(chain, contractDefinitions, cse.Call.ToCoreMessage(), state) + _, cse.ExecutionTrace, err = executiontracer.CallWithExecutionTrace(chain, contractDefinitions, cse.Call.ToCoreMessage(), nil) if err != nil { return fmt.Errorf("failed to resolve execution trace due to error replaying the call: %v", err) } diff --git a/fuzzing/calls/call_sequence_execution.go b/fuzzing/calls/call_sequence_execution.go index 824c9c8e..14fc1ecc 100644 --- a/fuzzing/calls/call_sequence_execution.go +++ b/fuzzing/calls/call_sequence_execution.go @@ -2,7 +2,13 @@ package calls import ( "fmt" + "github.com/crytic/medusa/chain" + "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/eth/tracers" ) // ExecuteCallSequenceFetchElementFunc describes a function that is called to obtain the next call sequence element to @@ -22,7 +28,7 @@ type ExecuteCallSequenceExecutionCheckFunc func(currentExecutedSequence CallSequ // A "post element executed check" function is provided to check whether execution should stop after each element is // executed. // Returns the call sequence which was executed and an error if one occurs. -func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc) (CallSequence, error) { +func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc, getTracerFn func(txIndex int, txHash common.Hash) *tracers.Tracer) (CallSequence, error) { // If there is no fetch element function provided, throw an error if fetchElementFunc == nil { return nil, fmt.Errorf("could not execute call sequence on chain as the 'fetch element function' provided was nil") @@ -84,7 +90,8 @@ func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc Exe } // Try to add our transaction to this block. - err = chain.PendingBlockAddTx(callSequenceElement.Call.ToCoreMessage()) + err = chain.PendingBlockAddTx(callSequenceElement.Call.ToCoreMessage(), getTracerFn) + if err != nil { // If we encountered a block gas limit error, this tx is too expensive to fit in this block. // If there are other transactions in the block, this makes sense. The block is "full". @@ -161,6 +168,42 @@ func ExecuteCallSequence(chain *chain.TestChain, callSequence CallSequence) (Cal return nil, nil } - // Execute our provided call sequence iteratively. - return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil) + return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil, nil) +} + +// ExecuteCallSequenceWithTracer attaches an executiontracer.ExecutionTracer to ExecuteCallSequenceIteratively and attaches execution traces to the call sequence elements. +func ExecuteCallSequenceWithExecutionTracer(testChain *chain.TestChain, contractDefinitions contracts.Contracts, callSequence CallSequence, verboseTracing bool) (CallSequence, error) { + // Create a new execution tracer + executionTracer := executiontracer.NewExecutionTracer(contractDefinitions, testChain.CheatCodeContracts()) + defer executionTracer.Close() + getTracerFunc := func(txIndex int, txHash common.Hash) *tracers.Tracer { + return executionTracer.NativeTracer().Tracer + } + + // Execute our sequence with a simple fetch operation provided to obtain each element. + fetchElementFunc := func(currentIndex int) (*CallSequenceElement, error) { + if currentIndex < len(callSequence) { + return callSequence[currentIndex], nil + } + return nil, nil + } + + // Execute the call sequence + executedCallSeq, err := ExecuteCallSequenceIteratively(testChain, fetchElementFunc, nil, getTracerFunc) + + // By default, we only trace the last element in the call sequence. + traceFrom := len(callSequence) - 1 + // If verbose tracing is enabled, we want to trace all elements in the call sequence. + if verboseTracing { + traceFrom = 0 + } + + // Attach the execution trace for each requested call sequence element + for ; traceFrom < len(callSequence); traceFrom++ { + callSequenceElement := callSequence[traceFrom] + hash := utils.MessageToTransaction(callSequenceElement.Call.ToCoreMessage()).Hash() + callSequenceElement.ExecutionTrace = executionTracer.GetTrace(hash) + } + + return executedCallSeq, err } diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 819da24a..011b414d 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -3,6 +3,11 @@ package corpus import ( "bytes" "fmt" + "math/big" + "path/filepath" + "sync" + "time" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/coverage" @@ -11,10 +16,6 @@ import ( "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" - "math/big" - "path/filepath" - "sync" - "time" "github.com/crytic/medusa/fuzzing/contracts" ) @@ -209,7 +210,7 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe } // Execute each call sequence, populating runtime data and collecting coverage data along the way. - _, err = calls.ExecuteCallSequenceIteratively(testChain, fetchElementFunc, executionCheckFunc) + _, err = calls.ExecuteCallSequenceIteratively(testChain, fetchElementFunc, executionCheckFunc, nil) // If we failed to replay a sequence and measure coverage due to an unexpected error, report it. if err != nil { @@ -227,8 +228,7 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe } // Revert chain state to our starting point to test the next sequence. - err = testChain.RevertToBlockNumber(baseBlockNumber) - if err != nil { + if err := testChain.RevertToBlockNumber(baseBlockNumber); err != nil { return fmt.Errorf("failed to reset the chain while seeding coverage: %v\n", err) } } @@ -258,7 +258,7 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions // Clone our test chain, adding listeners for contract deployment events from genesis. testChain, err := baseTestChain.Clone(func(newChain *chain.TestChain) error { // After genesis, prior to adding other blocks, we attach our coverage tracer - newChain.AddTracer(coverageTracer, true, false) + newChain.AddTracer(coverageTracer.NativeTracer(), true, false) // We also track any contract deployments, so we can resolve contract/method definitions for corpus call // sequences. diff --git a/fuzzing/coverage/coverage_tracer.go b/fuzzing/coverage/coverage_tracer.go index 4652fde4..fd32309b 100644 --- a/fuzzing/coverage/coverage_tracer.go +++ b/fuzzing/coverage/coverage_tracer.go @@ -1,11 +1,16 @@ package coverage import ( + "math/big" + + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/chain/types" "github.com/crytic/medusa/logging" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/tracing" + coretypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" - "math/big" + "github.com/ethereum/go-ethereum/eth/tracers" ) // coverageTracerResultsKey describes the key to use when storing tracer results in call message results, or when @@ -31,7 +36,7 @@ func RemoveCoverageTracerResults(messageResults *types.MessageResults) { delete(messageResults.AdditionalResults, coverageTracerResultsKey) } -// CoverageTracer implements vm.EVMLogger to collect information such as coverage maps +// CoverageTracer implements tracers.Tracer to collect information such as coverage maps // for fuzzing campaigns from EVM execution traces. type CoverageTracer struct { // coverageMaps describes the execution coverage recorded. Call frames which errored are not recorded. @@ -41,7 +46,12 @@ type CoverageTracer struct { callFrameStates []*coverageTracerCallFrameState // callDepth refers to the current EVM depth during tracing. - callDepth uint64 + callDepth int + + evmContext *tracing.VMContext + + // nativeTracer is the underlying tracer used to capture EVM execution. + nativeTracer *chain.TestChainTracer } // coverageTracerCallFrameState tracks state across call frames in the tracer. @@ -62,110 +72,107 @@ func NewCoverageTracer() *CoverageTracer { coverageMaps: NewCoverageMaps(), callFrameStates: make([]*coverageTracerCallFrameState, 0), } + nativeTracer := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: tracer.OnTxStart, + OnEnter: tracer.OnEnter, + OnExit: tracer.OnExit, + OnOpcode: tracer.OnOpcode, + }, + } + tracer.nativeTracer = &chain.TestChainTracer{Tracer: nativeTracer, CaptureTxEndSetAdditionalResults: tracer.CaptureTxEndSetAdditionalResults} + return tracer } -// CaptureTxStart is called upon the start of transaction execution, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureTxStart(gasLimit uint64) { +// NativeTracer returns the underlying TestChainTracer. +func (t *CoverageTracer) NativeTracer() *chain.TestChainTracer { + return t.nativeTracer +} + +// CaptureTxStart is called upon the start of transaction execution, as defined by tracers.Tracer. +func (t *CoverageTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) { // Reset our call frame states t.callDepth = 0 t.coverageMaps = NewCoverageMaps() t.callFrameStates = make([]*coverageTracerCallFrameState, 0) + t.evmContext = vm } -// CaptureTxEnd is called upon the end of transaction execution, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureTxEnd(restGas uint64) { -} - -// CaptureStart initializes the tracing operation for the top of a call frame, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - // Create our state tracking struct for this frame. - t.callFrameStates = append(t.callFrameStates, &coverageTracerCallFrameState{ - create: create, - pendingCoverageMap: NewCoverageMaps(), - }) -} - -// CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { - // If we encountered an error in this call frame, mark all coverage as reverted. - if err != nil { - _, revertCoverageErr := t.callFrameStates[t.callDepth].pendingCoverageMap.RevertAll() - if revertCoverageErr != nil { - logging.GlobalLogger.Panic("Coverage tracer failed to update revert coverage map during capture end", revertCoverageErr) - } - } +// OnEnter initializes the tracing operation for the top of a call frame, as defined by tracers.Tracer. +func (t *CoverageTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Check to see if this is the top level call frame + isTopLevelFrame := depth == 0 - // Commit all our coverage maps up one call frame. - _, _, coverageUpdateErr := t.coverageMaps.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) - if coverageUpdateErr != nil { - logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map during capture end", coverageUpdateErr) + // Increment call frame depth if it is not the top level call frame + if !isTopLevelFrame { + t.callDepth++ } - // Pop the state tracking struct for this call frame off the stack. - t.callFrameStates = t.callFrameStates[:t.callDepth] -} - -// CaptureEnter is called upon entering of the call frame, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - // Increase our call depth now that we're entering a new call frame. - t.callDepth++ - // Create our state tracking struct for this frame. t.callFrameStates = append(t.callFrameStates, &coverageTracerCallFrameState{ - create: typ == vm.CREATE || typ == vm.CREATE2, + create: typ == byte(vm.CREATE) || typ == byte(vm.CREATE2), pendingCoverageMap: NewCoverageMaps(), }) } -// CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureExit(output []byte, gasUsed uint64, err error) { +// OnExit is called after a call to finalize tracing completes for the top of a call frame, as defined by tracers.Tracer. +func (t *CoverageTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { + // Check to see if this is the top level call frame + isTopLevelFrame := depth == 0 + // If we encountered an error in this call frame, mark all coverage as reverted. if err != nil { _, revertCoverageErr := t.callFrameStates[t.callDepth].pendingCoverageMap.RevertAll() if revertCoverageErr != nil { - logging.GlobalLogger.Panic("Coverage tracer failed to update revert coverage map during capture exit", revertCoverageErr) + logging.GlobalLogger.Panic("Coverage tracer failed to update revert coverage map during capture end", revertCoverageErr) } } // Commit all our coverage maps up one call frame. - _, _, coverageUpdateErr := t.callFrameStates[t.callDepth-1].pendingCoverageMap.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) + var coverageUpdateErr error + if isTopLevelFrame { + // Update the final coverage map if this is the top level call frame + _, _, coverageUpdateErr = t.coverageMaps.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) + } else { + // Move coverage up one call frame + _, _, coverageUpdateErr = t.callFrameStates[t.callDepth-1].pendingCoverageMap.Update(t.callFrameStates[t.callDepth].pendingCoverageMap) + + // Pop the state tracking struct for this call frame off the stack and decrement the call depth + t.callFrameStates = t.callFrameStates[:t.callDepth] + t.callDepth-- + } if coverageUpdateErr != nil { - logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map during capture exit", coverageUpdateErr) + logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map during capture end", coverageUpdateErr) } - // Pop the state tracking struct for this call frame off the stack. - t.callFrameStates = t.callFrameStates[:t.callDepth] - - // Decrease our call depth now that we've exited a call frame. - t.callDepth-- } -// CaptureState records data from an EVM state update, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, vmErr error) { +// OnOpcode records data from an EVM state update, as defined by tracers.Tracer. +func (t *CoverageTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { // Obtain our call frame state tracking struct callFrameState := t.callFrameStates[t.callDepth] // If there is code we're executing, collect coverage. - if len(scope.Contract.Code) > 0 { + address := scope.Address() + code := t.evmContext.StateDB.GetCode(address) + codeSize := len(code) + if codeSize > 0 { + // Obtain our contract coverage map lookup hash. if callFrameState.lookupHash == nil { - lookupHash := getContractCoverageMapHash(scope.Contract.Code, callFrameState.create) + lookupHash := getContractCoverageMapHash(code, callFrameState.create) callFrameState.lookupHash = &lookupHash } // Record coverage for this location in our map. - _, coverageUpdateErr := callFrameState.pendingCoverageMap.SetAt(scope.Contract.Address(), *callFrameState.lookupHash, len(scope.Contract.Code), pc) + _, coverageUpdateErr := callFrameState.pendingCoverageMap.SetAt(address, *callFrameState.lookupHash, codeSize, pc) if coverageUpdateErr != nil { logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map while tracing state", coverageUpdateErr) } } } -// CaptureFault records an execution fault, as defined by vm.EVMLogger. -func (t *CoverageTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { -} - // CaptureTxEndSetAdditionalResults can be used to set additional results captured from execution tracing. If this // tracer is used during transaction execution (block creation), the results can later be queried from the block. // This method will only be called on the added tracer if it implements the extended TestChainTracer interface. diff --git a/fuzzing/executiontracer/execution_trace.go b/fuzzing/executiontracer/execution_trace.go index e85306bf..76953094 100644 --- a/fuzzing/executiontracer/execution_trace.go +++ b/fuzzing/executiontracer/execution_trace.go @@ -2,6 +2,7 @@ package executiontracer import ( "encoding/hex" + "errors" "fmt" "regexp" "strings" @@ -238,7 +239,7 @@ func (t *ExecutionTrace) generateCallFrameExitElements(callFrame *CallFrame) []a } // Check if this is a generic revert. - if callFrame.ReturnError == vm.ErrExecutionReverted { + if errors.Is(callFrame.ReturnError, vm.ErrExecutionReverted) { elements = append(elements, colors.RedBold, "[revert]", colors.Reset, "\n") return elements } diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index 17ec57fe..651f9379 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -5,28 +5,35 @@ import ( "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/contracts" + "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + coretypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" "golang.org/x/exp/slices" ) // CallWithExecutionTrace obtains an execution trace for a given call, on the provided chain, using the state // provided. If a nil state is provided, the current chain state will be used. // Returns the ExecutionTrace for the call or an error if one occurs. -func CallWithExecutionTrace(chain *chain.TestChain, contractDefinitions contracts.Contracts, msg *core.Message, state *state.StateDB) (*core.ExecutionResult, *ExecutionTrace, error) { +func CallWithExecutionTrace(testChain *chain.TestChain, contractDefinitions contracts.Contracts, msg *core.Message, state *state.StateDB) (*core.ExecutionResult, *ExecutionTrace, error) { // Create an execution tracer - executionTracer := NewExecutionTracer(contractDefinitions, chain.CheatCodeContracts()) + executionTracer := NewExecutionTracer(contractDefinitions, testChain.CheatCodeContracts()) + defer executionTracer.Close() // Call the contract on our chain with the provided state. - executionResult, err := chain.CallContract(msg, state, executionTracer) + executionResult, err := testChain.CallContract(msg, state, executionTracer.NativeTracer()) if err != nil { return nil, nil, err } // Obtain our trace - trace := executionTracer.Trace() + hash := utils.MessageToTransaction(msg).Hash() + trace := executionTracer.GetTrace(hash) // Return the trace return executionResult, trace, nil @@ -35,15 +42,14 @@ func CallWithExecutionTrace(chain *chain.TestChain, contractDefinitions contract // ExecutionTracer records execution information into an ExecutionTrace, containing information about each call // scope entered and exited. type ExecutionTracer struct { - // callDepth refers to the current EVM depth during tracing. - callDepth uint64 - // evm refers to the EVM instance last captured. - evm *vm.EVM + evmContext *tracing.VMContext // trace represents the current execution trace captured by this tracer. trace *ExecutionTrace + traceMap map[common.Hash]*ExecutionTrace + // currentCallFrame references the current call frame being traced. currentCallFrame *CallFrame @@ -53,11 +59,13 @@ type ExecutionTracer struct { // cheatCodeContracts represents the cheat code contract definitions to match for execution traces. cheatCodeContracts map[common.Address]*chain.CheatCodeContract - // onNextCaptureState refers to methods which should be executed the next time CaptureState executes. - // CaptureState is called prior to execution of an instruction. This allows actions to be performed + // onNextCaptureState refers to methods which should be executed the next time OnOpcode executes. + // OnOpcode is called prior to execution of an instruction. This allows actions to be performed // after some state is captured, on the next state capture (e.g. detecting a log instruction, but // using this structure to execute code later once the log is committed). onNextCaptureState []func() + + nativeTracer *chain.TestChainTracer } // NewExecutionTracer creates a ExecutionTracer and returns it. @@ -65,30 +73,57 @@ func NewExecutionTracer(contractDefinitions contracts.Contracts, cheatCodeContra tracer := &ExecutionTracer{ contractDefinitions: contractDefinitions, cheatCodeContracts: cheatCodeContracts, + traceMap: make(map[common.Hash]*ExecutionTrace), + } + innerTracer := &tracers.Tracer{ + Hooks: &tracing.Hooks{ + OnTxStart: tracer.OnTxStart, + OnEnter: tracer.OnEnter, + OnTxEnd: tracer.OnTxEnd, + OnExit: tracer.OnExit, + OnOpcode: tracer.OnOpcode, + }, } + tracer.nativeTracer = &chain.TestChainTracer{Tracer: innerTracer, CaptureTxEndSetAdditionalResults: nil} + return tracer } -// Trace returns the currently recording or last recorded execution trace by the tracer. -func (t *ExecutionTracer) Trace() *ExecutionTrace { - return t.trace +// NativeTracer returns the underlying TestChainTracer. +func (t *ExecutionTracer) NativeTracer() *chain.TestChainTracer { + return t.nativeTracer + +} + +// Close sets the traceMap to nil and should be called after the execution tracer is finish being used. +func (t *ExecutionTracer) Close() { + t.traceMap = nil } -// CaptureTxStart is called upon the start of transaction execution, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureTxStart(gasLimit uint64) { +// GetTrace returns the currently recording or last recorded execution trace by the tracer. +func (t *ExecutionTracer) GetTrace(txHash common.Hash) *ExecutionTrace { + if trace, ok := t.traceMap[txHash]; ok { + return trace + } + return nil +} + +// OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer. +func (t *ExecutionTracer) OnTxEnd(receipt *coretypes.Receipt, err error) { + t.traceMap[receipt.TxHash] = t.trace +} + +// OnTxStart is called upon the start of transaction execution, as defined by tracers.Tracer. +func (t *ExecutionTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) { // Reset our capture state - t.callDepth = 0 t.trace = newExecutionTrace(t.contractDefinitions) t.currentCallFrame = nil t.onNextCaptureState = nil + // Store our evm reference + t.evmContext = vm } -// CaptureTxEnd is called upon the end of transaction execution, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureTxEnd(restGas uint64) { - -} - -// resolveConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if +// resolveCallFrameConstructorArgs resolves previously unresolved constructor argument ABI data from the call data, if // the call frame provided represents a contract deployment. func (t *ExecutionTracer) resolveCallFrameConstructorArgs(callFrame *CallFrame, contract *contracts.Contract) { // If this is a contract creation and the constructor ABI argument data has not yet been resolved, do so now. @@ -142,6 +177,7 @@ func (t *ExecutionTracer) resolveCallFrameContractDefinitions(callFrame *CallFra if codeContract != nil { callFrame.CodeContractName = codeContract.Name() callFrame.CodeContractAbi = &codeContract.CompiledContract().Abi + callFrame.ExecutedCode = true } } } @@ -152,12 +188,12 @@ func (t *ExecutionTracer) captureEnteredCallFrame(fromAddress common.Address, to // Create our call frame struct to track data for this call frame we entered. callFrameData := &CallFrame{ SenderAddress: fromAddress, - ToAddress: toAddress, + ToAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in OnOpcode) and the contract's address is overridden by delegatecall. ToContractName: "", ToContractAbi: nil, ToInitBytecode: nil, ToRuntimeBytecode: nil, - CodeAddress: toAddress, // Note: Set temporarily, overwritten if code executes (in CaptureState). + CodeAddress: toAddress, CodeContractName: "", CodeContractAbi: nil, CodeRuntimeBytecode: nil, @@ -192,7 +228,7 @@ func (t *ExecutionTracer) captureExitedCallFrame(output []byte, err error) { if t.currentCallFrame.ToRuntimeBytecode == nil { // As long as this isn't a failed contract creation, we should be able to fetch "to" byte code on exit. if !t.currentCallFrame.IsContractCreation() || err == nil { - t.currentCallFrame.ToRuntimeBytecode = t.evm.StateDB.GetCode(t.currentCallFrame.ToAddress) + t.currentCallFrame.ToRuntimeBytecode = t.evmContext.StateDB.GetCode(t.currentCallFrame.ToAddress) } } if t.currentCallFrame.CodeRuntimeBytecode == nil { @@ -201,7 +237,7 @@ func (t *ExecutionTracer) captureExitedCallFrame(output []byte, err error) { if t.currentCallFrame.CodeAddress == t.currentCallFrame.ToAddress { t.currentCallFrame.CodeRuntimeBytecode = t.currentCallFrame.ToRuntimeBytecode } else { - t.currentCallFrame.CodeRuntimeBytecode = t.evm.StateDB.GetCode(t.currentCallFrame.CodeAddress) + t.currentCallFrame.CodeRuntimeBytecode = t.evmContext.StateDB.GetCode(t.currentCallFrame.CodeAddress) } } @@ -216,41 +252,20 @@ func (t *ExecutionTracer) captureExitedCallFrame(output []byte, err error) { t.currentCallFrame = t.currentCallFrame.ParentCallFrame } -// CaptureStart initializes the tracing operation for the top of a call frame, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { - // Store our evm reference - t.evm = env - +// OnEnter initializes the tracing operation for the top of a call frame, as defined by tracers.Tracer. +func (t *ExecutionTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { // Capture that a new call frame was entered. - t.captureEnteredCallFrame(from, to, input, create, value) + t.captureEnteredCallFrame(from, to, input, (typ == byte(vm.CREATE) || typ == byte(vm.CREATE2)), value) } -// CaptureEnd is called after a call to finalize tracing completes for the top of a call frame, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureEnd(output []byte, gasUsed uint64, err error) { +// OnExit is called after a call to finalize tracing completes for the top of a call frame, as defined by tracers.Tracer. +func (t *ExecutionTracer) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { // Capture that the call frame was exited. t.captureExitedCallFrame(output, err) } -// CaptureEnter is called upon entering of the call frame, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureEnter(typ vm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { - // Increase our call depth now that we're entering a new call frame. - t.callDepth++ - - // Capture that a new call frame was entered. - t.captureEnteredCallFrame(from, to, input, typ == vm.CREATE || typ == vm.CREATE2, value) -} - -// CaptureExit is called upon exiting of the call frame, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureExit(output []byte, gasUsed uint64, err error) { - // Capture that the call frame was exited. - t.captureExitedCallFrame(output, err) - - // Decrease our call depth now that we've exited a call frame. - t.callDepth-- -} - -// CaptureState records data from an EVM state update, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, depth int, vmErr error) { +// OnOpcode records data from an EVM state update, as defined by tracers.Tracer. +func (t *ExecutionTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { // Execute all "on next capture state" events and clear them. for _, eventHandler := range t.onNextCaptureState { eventHandler() @@ -262,34 +277,27 @@ func (t *ExecutionTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64 // be appropriately represented in this structure. The information populated earlier on frame enter represents // the raw call data, before delegate transformations are applied, etc. if !t.currentCallFrame.ExecutedCode { - t.currentCallFrame.SenderAddress = scope.Contract.CallerAddress - t.currentCallFrame.ToAddress = scope.Contract.Address() - if scope.Contract.CodeAddr != nil { - t.currentCallFrame.CodeAddress = *scope.Contract.CodeAddr - } - + t.currentCallFrame.SenderAddress = scope.Caller() + // This is not always the "to" address, but the current address e.g. for delegatecall. + t.currentCallFrame.ToAddress = scope.Address() // Mark code as having executed in this scope, so we don't set these values again (as cheat codes may affect it). // We also want to know if a given call scope executed code, or simply represented a value transfer call. t.currentCallFrame.ExecutedCode = true } // If we encounter a SELFDESTRUCT operation, record the operation. - if op == vm.SELFDESTRUCT { + if op == byte(vm.SELFDESTRUCT) { t.currentCallFrame.SelfDestructed = true } // If a log operation occurred, add a deferred operation to capture it. - if op == vm.LOG0 || op == vm.LOG1 || op == vm.LOG2 || op == vm.LOG3 || op == vm.LOG4 { + // TODO: Move this to OnLog + if op == byte(vm.LOG0) || op == byte(vm.LOG1) || op == byte(vm.LOG2) || op == byte(vm.LOG3) || op == byte(vm.LOG4) { t.onNextCaptureState = append(t.onNextCaptureState, func() { - logs := t.evm.StateDB.(*state.StateDB).Logs() + logs := t.evmContext.StateDB.(*state.StateDB).Logs() if len(logs) > 0 { t.currentCallFrame.Operations = append(t.currentCallFrame.Operations, logs[len(logs)-1]) } }) } } - -// CaptureFault records an execution fault, as defined by vm.EVMLogger. -func (t *ExecutionTracer) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) { - -} diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index ad6546c7..337e4e64 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -35,7 +35,6 @@ import ( "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" ) @@ -313,18 +312,18 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { // Create our genesis allocations. // NOTE: Sharing GenesisAlloc between chains will result in some accounts not being funded for some reason. - genesisAlloc := make(core.GenesisAlloc) + genesisAlloc := make(types.GenesisAlloc) // Fund all of our sender addresses in the genesis block initBalance := new(big.Int).Div(abi.MaxInt256, big.NewInt(2)) // TODO: make this configurable for _, sender := range f.senders { - genesisAlloc[sender] = core.GenesisAccount{ + genesisAlloc[sender] = types.Account{ Balance: initBalance, } } // Fund our deployer address in the genesis block - genesisAlloc[f.deployer] = core.GenesisAccount{ + genesisAlloc[f.deployer] = types.Account{ Balance: initBalance, } @@ -340,15 +339,15 @@ func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { // all compiled contract definitions. This includes any successful compilations as a result of the Fuzzer.config // definitions, as well as those added by Fuzzer.AddCompilationTargets. The contract deployment order is defined by // the Fuzzer.config. -func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) { +func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*executiontracer.ExecutionTrace, error) { // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, // we can infer the target contracts. Otherwise, we report an error. if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { if len(fuzzer.contractDefinitions) == 1 { fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { - return fmt.Errorf("missing target contracts (update fuzzing.targetContracts in the project config " + - "or use the --target-contracts CLI flag)"), nil + return nil, fmt.Errorf("missing target contracts (update fuzzing.targetContracts in the project config " + + "or use the --target-contracts CLI flag)") } } @@ -364,12 +363,12 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (err if len(contract.CompiledContract().Abi.Constructor.Inputs) > 0 { jsonArgs, ok := fuzzer.config.Fuzzing.ConstructorArgs[contractName] if !ok { - return fmt.Errorf("constructor arguments for contract %s not provided", contractName), nil + return nil, fmt.Errorf("constructor arguments for contract %s not provided", contractName) } decoded, err := valuegeneration.DecodeJSONArgumentsFromMap(contract.CompiledContract().Abi.Constructor.Inputs, jsonArgs, deployedContractAddr) if err != nil { - return err, nil + return nil, err } args = decoded } @@ -377,7 +376,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (err // Construct our deployment message/tx data field msgData, err := contract.CompiledContract().GetDeploymentMessageData(args) if err != nil { - return fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err), nil + return nil, fmt.Errorf("initial contract deployment failed for contract \"%v\", error: %v", contractName, err) } // If our project config has a non-zero balance for this target contract, retrieve it @@ -394,20 +393,19 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (err // Create a new pending block we'll commit to chain block, err := testChain.PendingBlockCreate() if err != nil { - return err, nil + return nil, err } // Add our transaction to the block - // Add our transaction to the block - err = testChain.PendingBlockAddTx(msg.ToCoreMessage()) + err = testChain.PendingBlockAddTx(msg.ToCoreMessage(), nil) if err != nil { - return err, nil + return nil, err } // Commit the pending block to the chain, so it becomes the new head. err = testChain.PendingBlockCommit() if err != nil { - return err, nil + return nil, err } // Ensure our transaction succeeded and, if it did not, attach an execution trace to it and re-run it. @@ -419,20 +417,20 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (err Block: block, TransactionIndex: len(block.Messages) - 1, } - - // Replay the execution trace for the failed contract deployment tx - err = cse.AttachExecutionTrace(testChain, fuzzer.contractDefinitions) - - // Throw an error if execution tracing threw an error or the trace is nil + // Revert to genesis and re-run the failed contract deployment tx. + // We should be able to attach an execution trace; however, if it fails, we provide the ExecutionResult at a minimum. + err = testChain.RevertToBlockNumber(0) if err != nil { - return fmt.Errorf("failed to attach execution trace to failed contract deployment tx: %v", err), nil - } - if cse.ExecutionTrace == nil { - return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err), nil + return nil, fmt.Errorf("failed to reset to genesis block: %v", err) + } else { + _, err = calls.ExecuteCallSequenceWithExecutionTracer(testChain, fuzzer.contractDefinitions, []*calls.CallSequenceElement{cse}, true) + if err != nil { + return nil, fmt.Errorf("deploying %s returned a failed status: %v", contractName, block.MessageResults[0].ExecutionResult.Err) + } } - // Return the execution error and the execution trace - return fmt.Errorf("contract deployment tx returned a failed status: %v", block.MessageResults[0].ExecutionResult.Err), cse.ExecutionTrace + // Return the execution error and the execution trace, if possible. + return cse.ExecutionTrace, fmt.Errorf("deploying %s returned a failed status: %v", contractName, block.MessageResults[0].ExecutionResult.Err) } // Record our deployed contract so the next config-specified constructor args can reference this @@ -448,7 +446,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (err // If we did not find a contract corresponding to this item in the deployment order, we throw an error. if !found { - return fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName), nil + return nil, fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName) } } return nil, nil @@ -668,7 +666,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") - err, trace := f.Hooks.ChainSetupFunc(f, baseTestChain) + trace, err := f.Hooks.ChainSetupFunc(f, baseTestChain) if err != nil { if trace != nil { f.logger.Error("Failed to initialize the test chain", err, errors.New(trace.Log().ColorString())) diff --git a/fuzzing/fuzzer_hooks.go b/fuzzing/fuzzer_hooks.go index cf458192..11d0e450 100644 --- a/fuzzing/fuzzer_hooks.go +++ b/fuzzing/fuzzer_hooks.go @@ -1,9 +1,10 @@ package fuzzing import ( - "github.com/crytic/medusa/fuzzing/executiontracer" "math/rand" + "github.com/crytic/medusa/fuzzing/executiontracer" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" @@ -27,7 +28,7 @@ type FuzzerHooks struct { ChainSetupFunc TestChainSetupFunc // CallSequenceTestFuncs describes a list of functions to be called upon by a FuzzerWorker after every call - // in a call sequence. + // in a call sequence. These must not commit to state CallSequenceTestFuncs []CallSequenceTestFunc } @@ -43,7 +44,7 @@ type NewCallSequenceGeneratorConfigFunc func(fuzzer *Fuzzer, valueSet *valuegene // TestChainSetupFunc describes a function which sets up a test chain's initial state prior to fuzzing. // An execution trace can also be returned in case of a deployment error for an improved debugging experience -type TestChainSetupFunc func(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) +type TestChainSetupFunc func(fuzzer *Fuzzer, testChain *chain.TestChain) (*executiontracer.ExecutionTrace, error) // CallSequenceTestFunc defines a method called after a fuzzing.FuzzerWorker sends another call in a types.CallSequence // during a fuzzing campaign. It returns a ShrinkCallSequenceRequest set, which represents a set of requests for @@ -61,7 +62,7 @@ type ShrinkCallSequenceRequest struct { VerifierFunction func(worker *FuzzerWorker, callSequence calls.CallSequence) (bool, error) // FinishedCallback is a method called upon when the shrink request has concluded. It provides the finalized // shrunken call sequence. - FinishedCallback func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) error + FinishedCallback func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence, verboseTracing bool) error // RecordResultInCorpus indicates whether the shrunken call sequence should be recorded in the corpus. If so, when // the shrinking operation is completed, the sequence will be added to the corpus if it doesn't already exist. RecordResultInCorpus bool diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index ce2aa536..f2ae19b6 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -37,7 +37,7 @@ func TestFuzzerHooks(t *testing.T) { return existingSeqGenConfigFunc(fuzzer, valueSet, randomProvider) } existingChainSetupFunc := f.fuzzer.Hooks.ChainSetupFunc - f.fuzzer.Hooks.ChainSetupFunc = func(fuzzer *Fuzzer, testChain *chain.TestChain) (error, *executiontracer.ExecutionTrace) { + f.fuzzer.Hooks.ChainSetupFunc = func(fuzzer *Fuzzer, testChain *chain.TestChain) (*executiontracer.ExecutionTrace, error) { chainSetupOk = true return existingChainSetupFunc(fuzzer, testChain) } @@ -250,7 +250,10 @@ func TestCheatCodes(t *testing.T) { // enable assertion testing only config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false config.Fuzzing.Testing.AssertionTesting.Enabled = true + + config.Fuzzing.TestChainConfig.CheatCodeConfig.CheatCodesEnabled = true config.Fuzzing.TestChainConfig.CheatCodeConfig.EnableFFI = true }, method: func(f *fuzzerTestContext) { diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 344063f8..77f4a945 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -316,7 +316,7 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall } // Execute our call sequence. - testedCallSequence, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) + testedCallSequence, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc, nil) // If we encountered an error, report it. if err != nil { @@ -382,7 +382,7 @@ func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.Ca } // Execute our call sequence. - _, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) + _, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc, nil) if err != nil { return false, err } @@ -508,8 +508,8 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri } } - // We have a finalized call sequence, re-execute it, so our current chain state is representative of post-execution. - _, err := calls.ExecuteCallSequence(fw.chain, optimizedSequence) + // Reset our state before running tracing in FinishedCallback. + err := fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber) if err != nil { return nil, err } @@ -517,15 +517,7 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri // Shrinking is complete. If our config specified we want all result sequences to have execution traces attached, // attach them now to each element in the sequence. Otherwise, call sequences will only have traces that the // test providers choose to attach themselves. - if fw.fuzzer.config.Fuzzing.Testing.TraceAll { - err = optimizedSequence.AttachExecutionTraces(fw.chain, fw.fuzzer.contractDefinitions) - if err != nil { - return nil, err - } - } - - // After we finished shrinking, report our result and return it. - err = shrinkRequest.FinishedCallback(fw, optimizedSequence) + err = shrinkRequest.FinishedCallback(fw, optimizedSequence, fw.fuzzer.config.Fuzzing.Testing.TraceAll) if err != nil { return nil, err } @@ -562,7 +554,7 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) { // If we have coverage-guided fuzzing enabled, create a tracer to collect coverage and connect it to the chain. if fw.fuzzer.config.Fuzzing.CoverageEnabled { fw.coverageTracer = coverage.NewCoverageTracer() - initializedChain.AddTracer(fw.coverageTracer, true, false) + initializedChain.AddTracer(fw.coverageTracer.NativeTracer(), true, false) } return nil }) diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index af1a0b47..233e2ef3 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -1,14 +1,16 @@ package fuzzing import ( + "sync" + "github.com/crytic/medusa/compilation/abiutils" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/config" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/accounts/abi" + "golang.org/x/exp/slices" - "sync" ) // AssertionTestCaseProvider is am AssertionTestCase provider which spawns test cases for every contract method and @@ -222,10 +224,10 @@ func (t *AssertionTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorke // If we encountered assertion failures on the same method, this shrunk sequence is satisfactory. return shrunkSeqTestFailed && *methodId == *shrunkSeqMethodId, nil }, - FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) error { - // When we're finished shrinking, attach an execution trace to the last call + FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence, verboseTracing bool) error { + // When we're finished shrinking, attach an execution trace to the last call. If verboseTracing is true, attach to all calls. if len(shrunkenCallSequence) > 0 { - err = shrunkenCallSequence[len(shrunkenCallSequence)-1].AttachExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions) + _, err = calls.ExecuteCallSequenceWithExecutionTracer(worker.chain, worker.fuzzer.contractDefinitions, shrunkenCallSequence, verboseTracing) if err != nil { return err } diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index 93c7e536..2177e39b 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -2,14 +2,15 @@ package fuzzing import ( "fmt" + "math/big" + "sync" + "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" - "math/big" - "sync" ) const MIN_INT = "-8000000000000000000000000000000000000000000000000000000000000000" @@ -85,9 +86,7 @@ func (t *OptimizationTestCaseProvider) runOptimizationTest(worker *FuzzerWorker, var executionResult *core.ExecutionResult var executionTrace *executiontracer.ExecutionTrace if trace { - executionTracer := executiontracer.NewExecutionTracer(worker.fuzzer.contractDefinitions, worker.chain.CheatCodeContracts()) - executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil, executionTracer) - executionTrace = executionTracer.Trace() + executionResult, executionTrace, err = executiontracer.CallWithExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions, msg.ToCoreMessage(), nil) } else { executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil) } @@ -321,10 +320,10 @@ func (t *OptimizationTestCaseProvider) callSequencePostCallTest(worker *FuzzerWo return shrunkenSequenceNewValue.Cmp(newValue) >= 0, err }, - FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) error { - // When we're finished shrinking, attach an execution trace to the last call + FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence, verboseTracing bool) error { + // When we're finished shrinking, attach an execution trace to the last call. If verboseTracing is true, attach to all calls. if len(shrunkenCallSequence) > 0 { - err = shrunkenCallSequence[len(shrunkenCallSequence)-1].AttachExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions) + _, err = calls.ExecuteCallSequenceWithExecutionTracer(worker.chain, worker.fuzzer.contractDefinitions, shrunkenCallSequence, verboseTracing) if err != nil { return err } diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 6d85db17..db990cbc 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -89,9 +89,7 @@ func (t *PropertyTestCaseProvider) checkPropertyTestFailed(worker *FuzzerWorker, var executionResult *core.ExecutionResult var executionTrace *executiontracer.ExecutionTrace if trace { - executionTracer := executiontracer.NewExecutionTracer(worker.fuzzer.contractDefinitions, worker.chain.CheatCodeContracts()) - executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil, executionTracer) - executionTrace = executionTracer.Trace() + executionResult, executionTrace, err = executiontracer.CallWithExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions, msg.ToCoreMessage(), nil) } else { executionResult, err = worker.Chain().CallContract(msg.ToCoreMessage(), nil) } @@ -318,10 +316,10 @@ func (t *PropertyTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorker shrunkenSequenceFailedTest, _, err := t.checkPropertyTestFailed(worker, &workerPropertyTestMethod, false) return shrunkenSequenceFailedTest, err }, - FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence) error { - // When we're finished shrinking, attach an execution trace to the last call + FinishedCallback: func(worker *FuzzerWorker, shrunkenCallSequence calls.CallSequence, verboseTracing bool) error { + // When we're finished shrinking, attach an execution trace to the last call. If verboseTracing is true, attach to all calls. if len(shrunkenCallSequence) > 0 { - err = shrunkenCallSequence[len(shrunkenCallSequence)-1].AttachExecutionTrace(worker.chain, worker.fuzzer.contractDefinitions) + _, err = calls.ExecuteCallSequenceWithExecutionTracer(worker.chain, worker.fuzzer.contractDefinitions, shrunkenCallSequence, verboseTracing) if err != nil { return err } diff --git a/go.mod b/go.mod index 16fa2f1b..05b14e8a 100644 --- a/go.mod +++ b/go.mod @@ -1,73 +1,90 @@ module github.com/crytic/medusa -go 1.18 +go 1.22 require ( github.com/Masterminds/semver v1.5.0 - github.com/ethereum/go-ethereum v1.12.0 + github.com/ethereum/go-ethereum v1.14.6 github.com/fxamacker/cbor v1.5.1 github.com/google/uuid v1.6.0 + github.com/holiman/uint256 v1.3.0 github.com/pkg/errors v0.9.1 - github.com/rs/zerolog v1.31.0 - github.com/shopspring/decimal v1.3.1 - github.com/spf13/cobra v1.8.0 + github.com/rs/zerolog v1.33.0 + github.com/shopspring/decimal v1.4.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.22.0 - golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/net v0.21.0 - golang.org/x/sys v0.19.0 + golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240707233637-46b078467d37 + golang.org/x/net v0.27.0 + golang.org/x/sys v0.22.0 ) require ( - github.com/DataDog/zstd v1.5.2 // indirect - github.com/VictoriaMetrics/fastcache v1.12.0 // indirect + github.com/DataDog/zstd v1.5.5 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/VictoriaMetrics/fastcache v1.12.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cockroachdb/errors v1.9.1 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect - github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 // indirect - github.com/cockroachdb/redact v1.1.3 // indirect + github.com/cockroachdb/pebble v1.1.1 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect + github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deckarep/golang-set/v2 v2.1.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect - github.com/getsentry/sentry-go v0.18.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-stack/stack v1.8.1 // indirect - github.com/gofrs/flock v0.8.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/ethereum/c-kzg-4844 v1.0.2 // indirect + github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect + github.com/getsentry/sentry-go v0.28.1 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gofrs/flock v0.12.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect - github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.15.15 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.39.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect - github.com/rivo/uniseg v0.4.3 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/status-im/keycard-go v0.3.2 // indirect + github.com/supranational/blst v0.3.12 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect - github.com/tklauser/go-sysconf v0.3.11 // indirect - github.com/tklauser/numcpus v0.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca +replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20240708141007-2f7f9258289f diff --git a/go.sum b/go.sum index ff4eb54b..d20fe464 100644 --- a/go.sum +++ b/go.sum @@ -1,481 +1,288 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= -github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= -github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= -github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= +github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/VictoriaMetrics/fastcache v1.12.0 h1:vnVi/y9yKDcD9akmc4NqAoqgQhJrOwUF+j9LTgn4QDE= -github.com/VictoriaMetrics/fastcache v1.12.0/go.mod h1:tjiYeEfYXCqacuvYw/7UoDIeJaNxq6132xHICNP77w8= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= -github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= +github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= -github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= -github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= -github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a h1:f52TdbU4D5nozMAhO9TvTJ2ZMCXtN4VIAmfrrZ0JXQ4= +github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoGMWEhDvS3zToKcDpRsLuRolQJBVGdozk= -github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811/go.mod h1:Nb5lgvnQ2+oGlE/EyZy4+2/CxRh9KfvCXnag1vtpxVM= -github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= -github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cockroachdb/pebble v1.1.1 h1:XnKU22oiCLy2Xn8vp1re67cXg4SAasg/WDt1NtcRFaw= +github.com/cockroachdb/pebble v1.1.1/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= +github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca h1:oGRWjrs9pStgFbnk1K5aLTQoY/aeO5zkzqADZH/maFw= -github.com/crytic/medusa-geth v0.0.0-20240209160711-dfded09070ca/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= +github.com/crytic/medusa-geth v0.0.0-20240708141007-2f7f9258289f h1:fxAlt4nFXa2WhoGVXbPezydKLFKx0mDRD4voT/xPcF4= +github.com/crytic/medusa-geth v0.0.0-20240708141007-2f7f9258289f/go.mod h1:hglUZo/5pVIYXNyYjWzsAUDpT/zI+WbWo/Nih7ot+G0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= -github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= -github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/ethereum/c-kzg-4844 v1.0.2 h1:8tV84BCEiPeOkiVgW9mpYBeBUir2bkCNVqxPwwVeO+s= +github.com/ethereum/c-kzg-4844 v1.0.2/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0 h1:KrE8I4reeVvf7C1tm8elRjj4BdscTYzz/WAbYyf/JI4= +github.com/ethereum/go-verkle v0.1.1-0.20240306133620-7d920df305f0/go.mod h1:D9AJLVXSyZQXJQVk8oh1EwjISE+sJTn2duYIZC0dy3w= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= -github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= -github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= -github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= -github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= -github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= +github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= -github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= +github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c h1:DZfsyhDK1hnSS5lH8l+JggqzEleHteTYfutAiVlSUM8= -github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.3.0 h1:4wdcm/tnd0xXdu7iS3ruNvxkWwrb4aeBQv19ayYn8F4= +github.com/holiman/uint256 v1.3.0/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= -github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= -github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= -github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= -github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= -github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= -github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= -github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= -github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= -github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/status-im/keycard-go v0.3.2 h1:YusIF/bHx6YZis8UTOJrpZFnTs4IkRBdmJXqdiXkpFE= +github.com/status-im/keycard-go v0.3.2/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.12 h1:Vfas2U2CFHhniv2QkUm2OVa1+pGTdqtpqm9NnhUUbZ8= +github.com/supranational/blst v0.3.12/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= -golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= +golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/logging/logger_test.go b/logging/logger_test.go index 53e0d6a4..f613fcdf 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -68,8 +68,9 @@ func TestDisabledColors(t *testing.T) { colors.DisableColor() logger.Info("foo") - // Ensure that msg doesn't include colors afterwards - prefix := fmt.Sprintf("%s %s", colors.LEFT_ARROW, "foo") + // Ensure that msg doesn't include colors afterwards (it is bolded) + prefix := fmt.Sprintf("%s \033[1m%s\033[0m", colors.LEFT_ARROW, "foo") + _, _, ok := strings.Cut(buf.String(), prefix) assert.True(t, ok) } From 0ce00fd55d9efbb1ab260c408e90b22ec293bb0b Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 18 Jul 2024 11:34:35 -0400 Subject: [PATCH 070/109] feat: Support deterministic deployment of contracts (#353) * initial commit for predeployed addresses * add test * initial working state of deploy contracts * minor tweaks and bug fix * allow for same contract to be in predeploy and target contract lists * fix lint issue * use static allocation for contract address overrides * bug fix for contract receipts * remove omitempty * update documentation and use omitempty * run prettier to fix linting --- chain/config/config.go | 16 ++++-- chain/test_chain.go | 3 +- chain/vendored/apply_transaction.go | 18 +++++-- .../project_configuration/fuzzing_config.md | 7 +++ fuzzing/config/config.go | 11 ++++ fuzzing/config/config_defaults.go | 1 + fuzzing/fuzzer.go | 52 +++++++++++++++++-- fuzzing/fuzzer_test.go | 23 ++++++++ .../deployments/predeploy_contract.sol | 13 +++++ 9 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 fuzzing/testdata/contracts/deployments/predeploy_contract.sol diff --git a/chain/config/config.go b/chain/config/config.go index dddc38c3..b50fc2f6 100644 --- a/chain/config/config.go +++ b/chain/config/config.go @@ -13,6 +13,9 @@ type TestChainConfig struct { // CheatCodeConfig indicates the configuration for EVM cheat codes to use. CheatCodeConfig CheatCodeConfig `json:"cheatCodes"` + + // ContractAddressOverrides describes contracts that are going to be deployed at deterministic addresses + ContractAddressOverrides map[common.Hash]common.Address `json:"contractAddressOverrides,omitempty"` } // CheatCodeConfig describes any configuration options related to the use of vm extensions (a.k.a. cheat codes) @@ -27,9 +30,16 @@ type CheatCodeConfig struct { // GetVMConfigExtensions derives a vm.ConfigExtensions from the provided TestChainConfig. func (t *TestChainConfig) GetVMConfigExtensions() *vm.ConfigExtensions { - // Obtain our cheat code precompiled contracts. + // Create a copy of the contract address overrides that can be ephemerally updated by medusa-geth + contractAddressOverrides := make(map[common.Hash]common.Address) + for hash, addr := range t.ContractAddressOverrides { + contractAddressOverrides[hash] = addr + } + + // Obtain our vm config extensions data structure return &vm.ConfigExtensions{ - OverrideCodeSizeCheck: t.CodeSizeCheckDisabled, - AdditionalPrecompiles: make(map[common.Address]vm.PrecompiledContract), + OverrideCodeSizeCheck: t.CodeSizeCheckDisabled, + AdditionalPrecompiles: make(map[common.Address]vm.PrecompiledContract), + ContractAddressOverrides: contractAddressOverrides, } } diff --git a/chain/test_chain.go b/chain/test_chain.go index 66e7cc5d..c1400311 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -775,8 +775,7 @@ func (t *TestChain) PendingBlockAddTx(message *core.Message, getTracerFn func(tx // Apply our transaction var usedGas uint64 - receipt, executionResult, err := vendored.EVMApplyTransaction(message, t.chainConfig, &t.pendingBlock.Header.Coinbase, gasPool, t.state, t.pendingBlock.Header.Number, t.pendingBlock.Hash, tx, &usedGas, evm) - + receipt, executionResult, err := vendored.EVMApplyTransaction(message, t.chainConfig, t.testChainConfig, &t.pendingBlock.Header.Coinbase, gasPool, t.state, t.pendingBlock.Header.Number, t.pendingBlock.Hash, tx, &usedGas, evm) if err != nil { return fmt.Errorf("test chain state write error when adding tx to pending block: %v", err) } diff --git a/chain/vendored/apply_transaction.go b/chain/vendored/apply_transaction.go index 6b1366bd..1472025b 100644 --- a/chain/vendored/apply_transaction.go +++ b/chain/vendored/apply_transaction.go @@ -17,6 +17,7 @@ package vendored import ( + "github.com/crytic/medusa/chain/config" "github.com/ethereum/go-ethereum/common" . "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" @@ -35,7 +36,7 @@ import ( // This executes on an underlying EVM and returns a transaction receipt, or an error if one occurs. // Additional changes: // - Exposed core.ExecutionResult as a return value. -func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (receipt *types.Receipt, result *ExecutionResult, err error) { +func EVMApplyTransaction(msg *Message, config *params.ChainConfig, testChainConfig *config.TestChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (receipt *types.Receipt, result *ExecutionResult, err error) { // Apply the OnTxStart and OnTxEnd hooks if evm.Config.Tracer != nil && evm.Config.Tracer.OnTxStart != nil { evm.Config.Tracer.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -45,7 +46,6 @@ func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *commo }() } } - // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) @@ -78,7 +78,19 @@ func EVMApplyTransaction(msg *Message, config *params.ChainConfig, author *commo // If the transaction created a contract, store the creation address in the receipt. if msg.To == nil { - receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) + // If the contract creation was a predeployed contract, we need to set the receipt's contract address to the + // override address + // Otherwise, we use the traditional method based on tx.origin and nonce + if len(testChainConfig.ContractAddressOverrides) > 0 { + initBytecodeHash := crypto.Keccak256Hash(msg.Data) + if overrideAddr, ok := testChainConfig.ContractAddressOverrides[initBytecodeHash]; ok { + receipt.ContractAddress = overrideAddr + } else { + receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) + } + } else { + receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) + } } // Set the receipt logs and create the bloom filter. diff --git a/docs/src/project_configuration/fuzzing_config.md b/docs/src/project_configuration/fuzzing_config.md index 0fa1f1e5..b2bf537d 100644 --- a/docs/src/project_configuration/fuzzing_config.md +++ b/docs/src/project_configuration/fuzzing_config.md @@ -65,6 +65,13 @@ The fuzzing configuration defines the parameters for the fuzzing campaign. > longer work since the contract addresses of the target contracts will change. This may render the entire corpus useless. - **Default**: `[]` +### `predeployedContracts` + +- **Type**: `{"contractName": "contractAddress"}` (e.g.`{"TestContract": "0x1234"}`) +- **Description**: This configuration parameter allows you to deterministically deploy contracts at predefined addresses. + > 🚩 Predeployed contracts do not accept constructor arguments. This may be added in the future. +- **Default**: `{}` + ### `targetContractBalances` - **Type**: [Base-16 Strings] (e.g. `[0x123, 0x456, 0x789]`) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index 8cbab7dd..f6782292 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -63,6 +63,10 @@ type FuzzingConfig struct { // TargetContracts are the target contracts for fuzz testing TargetContracts []string `json:"targetContracts"` + // PredeployedContracts are contracts that can be deterministically deployed at a specific address. It maps the + // contract name to the deployment address + PredeployedContracts map[string]string `json:"predeployedContracts"` + // TargetContractsBalances holds the amount of wei that should be sent during deployment for one or more contracts in // TargetContracts TargetContractsBalances []*big.Int `json:"targetContractsBalances"` @@ -333,6 +337,13 @@ func (p *ProjectConfig) Validate() error { return errors.New("project configuration must specify only a well-formed deployer address") } + // Verify that addresses of predeployed contracts are well-formed + for _, addr := range p.Fuzzing.PredeployedContracts { + if _, err := utils.HexStringToAddress(addr); err != nil { + return errors.New("project configuration must specify only well-formed predeployed contract address(es)") + } + } + // Ensure that the log level is a valid one level, err := zerolog.ParseLevel(p.Logging.Level.String()) if err != nil || level == zerolog.FatalLevel { diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 1fed1120..b77b2143 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -41,6 +41,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { CallSequenceLength: 100, TargetContracts: []string{}, TargetContractsBalances: []*big.Int{}, + PredeployedContracts: map[string]string{}, ConstructorArgs: map[string]map[string]any{}, CorpusDirectory: "", CoverageEnabled: true, diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 337e4e64..3fcd95e3 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/ethereum/go-ethereum/crypto" "math/big" "math/rand" "os" @@ -327,6 +328,35 @@ func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { Balance: initBalance, } + // Identify which contracts need to be predeployed to a deterministic address by iterating across the mapping + contractAddressOverrides := make(map[common.Hash]common.Address, len(f.config.Fuzzing.PredeployedContracts)) + for contractName, addrStr := range f.config.Fuzzing.PredeployedContracts { + found := false + // Try to find the associated compilation artifact + for _, contract := range f.contractDefinitions { + if contract.Name() == contractName { + // Hash the init bytecode (so that it can be easily identified in the EVM) and map it to the + // requested address + initBytecodeHash := crypto.Keccak256Hash(contract.CompiledContract().InitBytecode) + contractAddr, err := utils.HexStringToAddress(addrStr) + if err != nil { + return nil, fmt.Errorf("invalid address provided for a predeployed contract: %v", contract.Name()) + } + contractAddressOverrides[initBytecodeHash] = contractAddr + found = true + break + } + } + + // Throw an error if the contract specified in the config is not found + if !found { + return nil, fmt.Errorf("%v was specified in the predeployed contracts but was not found in the compilation artifacts", contractName) + } + } + + // Update the test chain config with the contract address overrides + f.config.Fuzzing.TestChainConfig.ContractAddressOverrides = contractAddressOverrides + // Create our test chain with our basic allocations and passed medusa's chain configuration testChain, err := chain.NewTestChain(genesisAlloc, &f.config.Fuzzing.TestChainConfig) @@ -346,21 +376,35 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex if len(fuzzer.contractDefinitions) == 1 { fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { - return nil, fmt.Errorf("missing target contracts (update fuzzing.targetContracts in the project config " + - "or use the --target-contracts CLI flag)") + return nil, fmt.Errorf("missing target contracts") } } - // Loop for all contracts to deploy + // Concatenate the predeployed contracts and target contracts + // Ordering is important here (predeploys _then_ targets) so that you can have the same contract in both lists + // while still being able to use the contract address overrides + contractsToDeploy := make([]string, 0) + for contractName := range fuzzer.config.Fuzzing.PredeployedContracts { + contractsToDeploy = append(contractsToDeploy, contractName) + } + contractsToDeploy = append(contractsToDeploy, fuzzer.config.Fuzzing.TargetContracts...) + deployedContractAddr := make(map[string]common.Address) - for i, contractName := range fuzzer.config.Fuzzing.TargetContracts { + // Loop for all contracts to deploy + for i, contractName := range contractsToDeploy { // Look for a contract in our compiled contract definitions that matches this one found := false for _, contract := range fuzzer.contractDefinitions { // If we found a contract definition that matches this definition by name, try to deploy it if contract.Name() == contractName { + // Concatenate constructor arguments, if necessary args := make([]any, 0) if len(contract.CompiledContract().Abi.Constructor.Inputs) > 0 { + // If the contract is a predeployed contract, throw an error because they do not accept constructor + // args. + if _, ok := fuzzer.config.Fuzzing.PredeployedContracts[contractName]; ok { + return nil, fmt.Errorf("predeployed contracts cannot accept constructor arguments") + } jsonArgs, ok := fuzzer.config.Fuzzing.ConstructorArgs[contractName] if !ok { return nil, fmt.Errorf("constructor arguments for contract %s not provided", contractName) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index f2ae19b6..1898e578 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -396,6 +396,29 @@ func TestDeploymentsInternalLibrary(t *testing.T) { }) } +// TestDeploymentsWithPredeploy runs a test to ensure that predeployed contracts are instantiated correctly. +func TestDeploymentsWithPredeploy(t *testing.T) { + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/deployments/predeploy_contract.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.TestLimit = 1000 // this test should expose a failure immediately + config.Fuzzing.Testing.PropertyTesting.Enabled = false + config.Fuzzing.Testing.OptimizationTesting.Enabled = false + config.Fuzzing.PredeployedContracts = map[string]string{"PredeployContract": "0x1234"} + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + // Check for any failed tests and verify coverage was captured + assertFailedTestsExpected(f, true) + assertCorpusCallSequencesCollected(f, true) + }, + }) +} + // TestDeploymentsWithPayableConstructor runs a test to ensure that we can send ether to payable constructors func TestDeploymentsWithPayableConstructors(t *testing.T) { runFuzzerTest(t, &fuzzerSolcFileTest{ diff --git a/fuzzing/testdata/contracts/deployments/predeploy_contract.sol b/fuzzing/testdata/contracts/deployments/predeploy_contract.sol new file mode 100644 index 00000000..12ceef8f --- /dev/null +++ b/fuzzing/testdata/contracts/deployments/predeploy_contract.sol @@ -0,0 +1,13 @@ +contract PredeployContract { + function triggerFailure() public { + assert(false); + } +} + +contract TestContract { + PredeployContract predeploy = PredeployContract(address(0x1234)); + + function testPredeploy() public { + predeploy.triggerFailure(); + } +} From ff96116bd3b875776136dc2626234e51be1d7ead Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 18 Jul 2024 15:27:00 -0400 Subject: [PATCH 071/109] add alpharush as code owner (#402) --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index a0108738..85f87d99 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @Xenomega @anishnaik +* @Xenomega @anishnaik @0xalpharush From 9a2d0d9f895c74b168047b37f825e7d2b4079d41 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 18 Jul 2024 14:49:12 -0500 Subject: [PATCH 072/109] feat: add config to target/exclude func sig. by contract (#400) * feat: add config to target/exclude func sig. by contract * call property and optimization tests even if they aren't targeted * prettier * nits * minor updates to fix API-level capabilities and some comment fixes --------- Co-authored-by: Anish Naik --- .../project_configuration/testing_config.md | 14 ++++++ fuzzing/config/config.go | 49 ++++++++++++++++++- fuzzing/config/config_defaults.go | 5 +- fuzzing/contracts/contract.go | 41 ++++++++++++++++ fuzzing/fuzzer.go | 24 ++++++++- fuzzing/fuzzer_test.go | 43 ++++++++++++++++ fuzzing/fuzzer_worker.go | 5 +- fuzzing/test_case_assertion_provider.go | 26 +--------- fuzzing/test_case_optimization_provider.go | 7 +-- fuzzing/test_case_property_provider.go | 8 +-- .../deploy_payable_constructors.sol | 7 +++ .../filtering/target_and_exclude.sol | 35 +++++++++++++ fuzzing/utils/fuzz_method_utils.go | 26 ++++++++-- 13 files changed, 243 insertions(+), 47 deletions(-) create mode 100644 fuzzing/testdata/contracts/filtering/target_and_exclude.sol diff --git a/docs/src/project_configuration/testing_config.md b/docs/src/project_configuration/testing_config.md index 98047e72..fd2f03f0 100644 --- a/docs/src/project_configuration/testing_config.md +++ b/docs/src/project_configuration/testing_config.md @@ -63,6 +63,20 @@ contract MyContract { that triggered a test failure. - **Default**: `false` +### `targetFunctionSignatures`: + +- **Type**: [String] +- **Description**: A list of function signatures that the fuzzer should exclusively target by omitting calls to other signatures. The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`. + > **Note**: Property and optimization tests will always be called even if they are not explicitly specified in this list. +- **Default**: `[]` + +### `excludeFunctionSignatures`: + +- **Type**: [String] +- **Description**: A list of function signatures that the fuzzer should exclude from the fuzzing campaign. The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`. + > **Note**: Property and optimization tests will always be called and cannot be excluded. +- **Default**: `[]` + ## Assertion Testing Configuration ### `enabled` diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index f6782292..e488238c 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -141,6 +141,48 @@ type TestingConfig struct { // OptimizationTesting describes the configuration used for optimization testing. OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"` + + // TargetFunctionSignatures is a list function signatures call the fuzzer should exclusively target by omitting calls to other signatures. + // The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`. + TargetFunctionSignatures []string `json:"targetFunctionSignatures"` + + // ExcludeFunctionSignatures is a list of function signatures that will be excluded from call sequences. + // The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`. + ExcludeFunctionSignatures []string `json:"excludeFunctionSignatures"` +} + +// Validate validates that the TestingConfig meets certain requirements. +func (testCfg *TestingConfig) Validate() error { + // Verify that target and exclude function signatures are used mutually exclusive. + if (len(testCfg.TargetFunctionSignatures) != 0) && (len(testCfg.ExcludeFunctionSignatures) != 0) { + return errors.New("project configuration must specify only one of blacklist or whitelist at a time") + } + + // Verify property testing fields. + if testCfg.PropertyTesting.Enabled { + // Test prefixes must be supplied if property testing is enabled. + if len(testCfg.PropertyTesting.TestPrefixes) == 0 { + return errors.New("project configuration must specify test name prefixes if property testing is enabled") + } + } + + if testCfg.OptimizationTesting.Enabled { + // Test prefixes must be supplied if optimization testing is enabled. + if len(testCfg.OptimizationTesting.TestPrefixes) == 0 { + return errors.New("project configuration must specify test name prefixes if optimization testing is enabled") + } + } + + // Validate that prefixes do not overlap + for _, prefix := range testCfg.PropertyTesting.TestPrefixes { + for _, prefix2 := range testCfg.OptimizationTesting.TestPrefixes { + if prefix == prefix2 { + return errors.New("project configuration must specify unique test name prefixes for property and optimization testing") + } + } + } + + return nil } // AssertionTestingConfig describes the configuration options used for assertion testing @@ -216,7 +258,7 @@ type LoggingConfig struct { // equivalent to enabling file logging. LogDirectory string `json:"logDirectory"` - // NoColor indicates whether or not log messages should be displayed with colored formatting. + // NoColor indicates whether log messages should be displayed with colored formatting. NoColor bool `json:"noColor"` } @@ -286,6 +328,11 @@ func (p *ProjectConfig) Validate() error { logger = logging.GlobalLogger.NewSubLogger("module", "fuzzer config") } + // Validate testing config + if err := p.Fuzzing.Testing.Validate(); err != nil { + return err + } + // Verify the worker count is a positive number. if p.Fuzzing.Workers <= 0 { return errors.New("project configuration must specify a positive number for the worker count") diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index b77b2143..10f45dc1 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -1,10 +1,11 @@ package config import ( + "math/big" + testChainConfig "github.com/crytic/medusa/chain/config" "github.com/crytic/medusa/compilation" "github.com/rs/zerolog" - "math/big" ) // GetDefaultProjectConfig obtains a default configuration for a project. It populates a default compilation config @@ -61,6 +62,8 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { StopOnNoTests: true, TestAllContracts: false, TraceAll: false, + TargetFunctionSignatures: []string{}, + ExcludeFunctionSignatures: []string{}, AssertionTesting: AssertionTestingConfig{ Enabled: true, TestViewMethods: false, diff --git a/fuzzing/contracts/contract.go b/fuzzing/contracts/contract.go index 17ec9f66..30ad094a 100644 --- a/fuzzing/contracts/contract.go +++ b/fuzzing/contracts/contract.go @@ -1,7 +1,11 @@ package contracts import ( + "golang.org/x/exp/slices" + "strings" + "github.com/crytic/medusa/compilation/types" + "github.com/ethereum/go-ethereum/accounts/abi" ) // Contracts describes an array of contracts @@ -35,6 +39,17 @@ type Contract struct { // compilation describes the compilation which contains the compiledContract. compilation *types.Compilation + + // PropertyTestMethods are the methods that are property tests. + PropertyTestMethods []abi.Method + + // OptimizationTestMethods are the methods that are optimization tests. + OptimizationTestMethods []abi.Method + + // AssertionTestMethods are ALL other methods that are not property or optimization tests by default. + // If configured, the methods will be targeted or excluded based on the targetFunctionSignatures + // and excludedFunctionSignatures, respectively. + AssertionTestMethods []abi.Method } // NewContract returns a new Contract instance with the provided information. @@ -47,6 +62,32 @@ func NewContract(name string, sourcePath string, compiledContract *types.Compile } } +// WithTargetedAssertionMethods filters the assertion test methods to those in the target list. +func (c *Contract) WithTargetedAssertionMethods(target []string) *Contract { + var candidateMethods []abi.Method + for _, method := range c.AssertionTestMethods { + canonicalSig := strings.Join([]string{c.name, method.Sig}, ".") + if slices.Contains(target, canonicalSig) { + candidateMethods = append(candidateMethods, method) + } + } + c.AssertionTestMethods = candidateMethods + return c +} + +// WithExcludedAssertionMethods filters the assertion test methods to all methods not in excluded list. +func (c *Contract) WithExcludedAssertionMethods(excludedMethods []string) *Contract { + var candidateMethods []abi.Method + for _, method := range c.AssertionTestMethods { + canonicalSig := strings.Join([]string{c.name, method.Sig}, ".") + if !slices.Contains(excludedMethods, canonicalSig) { + candidateMethods = append(candidateMethods, method) + } + } + c.AssertionTestMethods = candidateMethods + return c +} + // Name returns the name of the contract. func (c *Contract) Name() string { return c.name diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 3fcd95e3..6f1d6e8b 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -32,6 +32,7 @@ import ( "github.com/crytic/medusa/fuzzing/config" fuzzerTypes "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/corpus" + fuzzingutils "github.com/crytic/medusa/fuzzing/utils" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/accounts/abi" @@ -282,7 +283,7 @@ func (f *Fuzzer) ReportTestCaseFinished(testCase TestCase) { // AddCompilationTargets takes a compilation and updates the Fuzzer state with additional Fuzzer.ContractDefinitions // definitions and Fuzzer.BaseValueSet values. func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilation) { - // Loop for each contract in each compilation and deploy it to the test node. + // Loop for each contract in each compilation and deploy it to the test chain for i := 0; i < len(compilations); i++ { // Add our compilation to the list and get a reference to it. f.compilations = append(f.compilations, compilations[i]) @@ -297,6 +298,26 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati for contractName := range source.Contracts { contract := source.Contracts[contractName] contractDefinition := fuzzerTypes.NewContract(contractName, sourcePath, &contract, compilation) + + // Sort available methods by type + assertionTestMethods, propertyTestMethods, optimizationTestMethods := fuzzingutils.BinTestByType(&contract, + f.config.Fuzzing.Testing.PropertyTesting.TestPrefixes, + f.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes, + f.config.Fuzzing.Testing.AssertionTesting.TestViewMethods) + contractDefinition.AssertionTestMethods = assertionTestMethods + contractDefinition.PropertyTestMethods = propertyTestMethods + contractDefinition.OptimizationTestMethods = optimizationTestMethods + + // Filter and record methods available for assertion testing. Property and optimization tests are always run. + if len(f.config.Fuzzing.Testing.TargetFunctionSignatures) > 0 { + // Only consider methods that are in the target methods list + contractDefinition = contractDefinition.WithTargetedAssertionMethods(f.config.Fuzzing.Testing.TargetFunctionSignatures) + } + if len(f.config.Fuzzing.Testing.ExcludeFunctionSignatures) > 0 { + // Consider all methods except those in the exclude methods list + contractDefinition = contractDefinition.WithExcludedAssertionMethods(f.config.Fuzzing.Testing.ExcludeFunctionSignatures) + } + f.contractDefinitions = append(f.contractDefinitions, contractDefinition) } } @@ -373,6 +394,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, // we can infer the target contracts. Otherwise, we report an error. if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { + // TODO filter libraries if len(fuzzer.contractDefinitions) == 1 { fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} } else { diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 1898e578..0af56dd9 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "math/big" "math/rand" + "reflect" "testing" "github.com/crytic/medusa/fuzzing/executiontracer" @@ -893,3 +894,45 @@ func TestDeploymentOrderWithCoverage(t *testing.T) { }, }) } + +// TestTargetingFuncSignatures tests whether functions will be correctly whitelisted for testing +func TestTargetingFuncSignatures(t *testing.T) { + targets := []string{"TestContract.f(), TestContract.g()"} + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/filtering/target_and_exclude.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.Testing.TargetFunctionSignatures = targets + }, + method: func(f *fuzzerTestContext) { + for _, contract := range f.fuzzer.ContractDefinitions() { + // The targets should be the only functions tested, excluding h and i + reflect.DeepEqual(contract.AssertionTestMethods, targets) + + // ALL properties and optimizations should be tested + reflect.DeepEqual(contract.PropertyTestMethods, []string{"TestContract.property_a()"}) + reflect.DeepEqual(contract.OptimizationTestMethods, []string{"TestContract.optimize_b()"}) + } + }}) +} + +// TestExcludeFunctionSignatures tests whether functions will be blacklisted/excluded for testing +func TestExcludeFunctionSignatures(t *testing.T) { + excluded := []string{"TestContract.f(), TestContract.g()"} + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/filtering/target_and_exclude.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.Testing.ExcludeFunctionSignatures = excluded + }, + method: func(f *fuzzerTestContext) { + for _, contract := range f.fuzzer.ContractDefinitions() { + // Only h and i should be test since f and g are excluded + reflect.DeepEqual(contract.AssertionTestMethods, []string{"TestContract.h()", "TestContract.i()"}) + + // ALL properties and optimizations should be tested + reflect.DeepEqual(contract.PropertyTestMethods, []string{"TestContract.property_a()"}) + reflect.DeepEqual(contract.OptimizationTestMethods, []string{"TestContract.optimize_b()"}) + } + }}) +} diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 77f4a945..5f651325 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -90,6 +90,7 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) fuzzer: fuzzer, deployedContracts: make(map[common.Address]*fuzzerTypes.Contract), stateChangingMethods: make([]fuzzerTypes.DeployedContractMethod, 0), + pureMethods: make([]fuzzerTypes.DeployedContractMethod, 0), coverageTracer: nil, randomProvider: randomProvider, valueSet: valueSet, @@ -239,9 +240,9 @@ func (fw *FuzzerWorker) updateMethods() { // Loop through each deployed contract for contractAddress, contractDefinition := range fw.deployedContracts { // If we deployed the contract, also enumerate property tests and state changing methods. - for _, method := range contractDefinition.CompiledContract().Abi.Methods { + for _, method := range contractDefinition.AssertionTestMethods { // Any non-constant method should be tracked as a state changing method. - // We favor calling state changing methods over view methods. + // We favor calling state changing methods over view/pure methods. if method.IsConstant() { fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(1))) diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 233e2ef3..8ab4a5bd 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -7,8 +7,6 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/config" "github.com/crytic/medusa/fuzzing/contracts" - "github.com/crytic/medusa/fuzzing/utils" - "github.com/ethereum/go-ethereum/accounts/abi" "golang.org/x/exp/slices" ) @@ -44,23 +42,6 @@ func attachAssertionTestCaseProvider(fuzzer *Fuzzer) *AssertionTestCaseProvider return t } -// isTestableMethod checks whether the method is configured by the attached fuzzer to be a target of assertion testing. -// Returns true if this target should be tested, false otherwise. -func (t *AssertionTestCaseProvider) isTestableMethod(method abi.Method) bool { - // Do not test optimization tests - if utils.IsOptimizationTest(method, t.fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) { - return false - } - - // Do not test property tests - if utils.IsPropertyTest(method, t.fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) { - return false - } - - // Only test constant methods (pure/view) if we are configured to. - return !method.IsConstant() || t.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods -} - // checkAssertionFailures checks the results of the last call for assertion failures. // Returns the method ID, a boolean indicating if an assertion test failed, or an error if one occurs. func (t *AssertionTestCaseProvider) checkAssertionFailures(callSequence calls.CallSequence) (*contracts.ContractMethodID, bool, error) { @@ -105,12 +86,7 @@ func (t *AssertionTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) continue } - for _, method := range contract.CompiledContract().Abi.Methods { - // Verify this method is an assertion testable method - if !t.isTestableMethod(method) { - continue - } - + for _, method := range contract.AssertionTestMethods { // Create local variables to avoid pointer types in the loop being overridden. contract := contract method := method diff --git a/fuzzing/test_case_optimization_provider.go b/fuzzing/test_case_optimization_provider.go index 2177e39b..e782c646 100644 --- a/fuzzing/test_case_optimization_provider.go +++ b/fuzzing/test_case_optimization_provider.go @@ -8,7 +8,6 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" - "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" ) @@ -135,11 +134,7 @@ func (t *OptimizationTestCaseProvider) onFuzzerStarting(event FuzzerStartingEven continue } - for _, method := range contract.CompiledContract().Abi.Methods { - // Verify this method is an optimization test method - if !utils.IsOptimizationTest(method, t.fuzzer.config.Fuzzing.Testing.OptimizationTesting.TestPrefixes) { - continue - } + for _, method := range contract.OptimizationTestMethods { // Create local variables to avoid pointer types in the loop being overridden. contract := contract method := method diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index db990cbc..3681d218 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -8,7 +8,6 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" - "github.com/crytic/medusa/fuzzing/utils" "github.com/ethereum/go-ethereum/core" "golang.org/x/exp/slices" ) @@ -137,12 +136,7 @@ func (t *PropertyTestCaseProvider) onFuzzerStarting(event FuzzerStartingEvent) e continue } - for _, method := range contract.CompiledContract().Abi.Methods { - // Verify this method is a property test method - if !utils.IsPropertyTest(method, t.fuzzer.config.Fuzzing.Testing.PropertyTesting.TestPrefixes) { - continue - } - + for _, method := range contract.PropertyTestMethods { // Create local variables to avoid pointer types in the loop being overridden. contract := contract method := method diff --git a/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol b/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol index 223d85c4..f976e48f 100644 --- a/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol +++ b/fuzzing/testdata/contracts/deployments/deploy_payable_constructors.sol @@ -6,6 +6,9 @@ contract FirstContract { function property_contract_has_no_balance() public returns(bool) { return address(this).balance == 0; } + + // This exists so the fuzzer knows there are state changing methods to target, instead of quitting early. + function dummy() public {} } @@ -15,4 +18,8 @@ contract SecondContract { function property_contract_has_balance() public returns(bool) { return address(this).balance == 1 ether; } + + // This exists so the fuzzer knows there are state changing methods to target, instead of quitting early. + function dummy() public {} + } diff --git a/fuzzing/testdata/contracts/filtering/target_and_exclude.sol b/fuzzing/testdata/contracts/filtering/target_and_exclude.sol new file mode 100644 index 00000000..65c76dce --- /dev/null +++ b/fuzzing/testdata/contracts/filtering/target_and_exclude.sol @@ -0,0 +1,35 @@ +// This contract ensures that we can target or exclude functions +contract TestContract { + uint odd_counter = 1; + uint even_counter = 2; + event Counter(uint256 value); + function f() public { + odd_counter += 1; + emit Counter(odd_counter); + } + + function g() public { + even_counter += 2; + emit Counter(even_counter); + + } + + function h() public { + odd_counter += 3; + emit Counter(odd_counter); + + } + + function i() public { + even_counter += 4; + emit Counter(even_counter); + } + + function property_a() public view returns (bool) { + return (odd_counter != 100); + } + + function optimize_b() public view returns (int256) { + return -1; + } +} diff --git a/fuzzing/utils/fuzz_method_utils.go b/fuzzing/utils/fuzz_method_utils.go index 1174246b..70b77a12 100644 --- a/fuzzing/utils/fuzz_method_utils.go +++ b/fuzzing/utils/fuzz_method_utils.go @@ -1,8 +1,10 @@ package utils import ( - "github.com/ethereum/go-ethereum/accounts/abi" "strings" + + compilationTypes "github.com/crytic/medusa/compilation/types" + "github.com/ethereum/go-ethereum/accounts/abi" ) // IsOptimizationTest checks whether the method is an optimization test given potential naming prefixes it must conform to @@ -25,10 +27,26 @@ func IsOptimizationTest(method abi.Method, prefixes []string) bool { func IsPropertyTest(method abi.Method, prefixes []string) bool { // Loop through all enabled prefixes to find a match for _, prefix := range prefixes { - // The property test must simply have the right prefix and take no inputs - if strings.HasPrefix(method.Name, prefix) && len(method.Inputs) == 0 { - return true + // The property test must simply have the right prefix and take no inputs and return a boolean + if strings.HasPrefix(method.Name, prefix) { + if len(method.Inputs) == 0 && len(method.Outputs) == 1 && method.Outputs[0].Type.T == abi.BoolTy { + return true + } } } return false } + +// BinTestByType sorts a contract's methods by whether they are assertion, property, or optimization tests. +func BinTestByType(contract *compilationTypes.CompiledContract, propertyTestPrefixes, optimizationTestPrefixes []string, testViewMethods bool) (assertionTests, propertyTests, optimizationTests []abi.Method) { + for _, method := range contract.Abi.Methods { + if IsPropertyTest(method, propertyTestPrefixes) { + propertyTests = append(propertyTests, method) + } else if IsOptimizationTest(method, optimizationTestPrefixes) { + optimizationTests = append(optimizationTests, method) + } else if !method.IsConstant() || testViewMethods { + assertionTests = append(assertionTests, method) + } + } + return assertionTests, propertyTests, optimizationTests +} From 4577a6946daae7e84ea5286aa4e4ca8fc19aad8c Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Fri, 19 Jul 2024 17:27:12 +0200 Subject: [PATCH 073/109] Fix wrong links in the doc + update command in contributing guidelines (#404) --- CONTRIBUTING.md | 2 +- docs/src/project_configuration/overview.md | 2 +- docs/src/project_configuration/testing_config.md | 2 +- docs/src/testing/writing-tests.md | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index deceb304..dcae4472 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ To install To run - `prettier '**.json' '**/*.md' '**/*.yml' '!(pkg)'` -- `markdown-link-check --config .github/workflows/resources/markdown_link_check.json ./*.md` +- `find . -name '*.md' -print0 | xargs -0 -n1 markdown-link-check --config .github/workflows/resources/markdown_link_check.json` To format (overwrite files) diff --git a/docs/src/project_configuration/overview.md b/docs/src/project_configuration/overview.md index af7d3a46..898c6ee1 100644 --- a/docs/src/project_configuration/overview.md +++ b/docs/src/project_configuration/overview.md @@ -43,7 +43,7 @@ value to `10_000` or `100_000` depending on the use case. You can learn more abo Updating the corpus directory is optional but recommended. The corpus directory determines where corpus items should be stored on disk. A corpus item is a sequence of transactions that increased `medusa`'s coverage of the system. Thus, these corpus items are valuable to store so that they can be re-used for the next fuzzing campaign. Additionally, the directory -will also hold [coverage reports](TODO) which is a valuable tool for debugging and validation. For most cases, you may set +will also hold [coverage reports](../coverage_reports.md) which is a valuable tool for debugging and validation. For most cases, you may set `corpusDirectory`'s value to "corpus". This will create a `corpus/` directory in the same directory as the `medusa.json` file. You can learn more about this option [here](./fuzzing_config.md#corpusdirectory). diff --git a/docs/src/project_configuration/testing_config.md b/docs/src/project_configuration/testing_config.md index fd2f03f0..a9e2f073 100644 --- a/docs/src/project_configuration/testing_config.md +++ b/docs/src/project_configuration/testing_config.md @@ -59,7 +59,7 @@ contract MyContract { ### `traceAll`: - **Type**: Boolean -- **Description**: Determines whether an [execution trace](TODO) should be attached to each element of a call sequence +- **Description**: Determines whether an `execution trace` should be attached to each element of a call sequence that triggered a test failure. - **Default**: `false` diff --git a/docs/src/testing/writing-tests.md b/docs/src/testing/writing-tests.md index 86da3ec6..f8aacca2 100644 --- a/docs/src/testing/writing-tests.md +++ b/docs/src/testing/writing-tests.md @@ -38,7 +38,7 @@ contract TestXY { To begin a fuzzing campaign in property-mode, you can run `medusa fuzz` or `medusa fuzz --config [config_path]`. -> **Note**: Learn more about running `medusa` with its CLI [here](./Command-Line-Interface.md). +> **Note**: Learn more about running `medusa` with its CLI [here](../cli/overview.md). Invoking this fuzzing campaign, `medusa` will: @@ -59,7 +59,7 @@ Test "TestXY.fuzz_never_specific_values()" failed after the following call seque ## Writing assertion tests -Although both property-mode and assertion-mode try to validate / invalidate invariants of the system, they do so in different ways. In property-mode, `medusa` will look for functions with a specific test prefix (e.g. `fuzz_`) and test those. In assertion-mode, `medusa` will test to see if a given call sequence can cause the Ethereum Virtual Machine (EVM) to "panic". The EVM has a variety of panic codes for different scenarios. For example, there is a unique panic code when an `assert(x)` statement returns `false` or when a division by zero is encountered. In assertion mode, which panics should or should not be treated as "failing test cases" can be toggled by updating the [Project Configuration](./Project-Configuration.md#fuzzing-configuration). By default, only `FailOnAssertion` is enabled. Check out the [Example Project Configuration File](https://github.com/crytic/medusa/wiki/Example-Project-Configuration-File) for a visualization of the various panic codes that can be enabled. An explanation of the various panic codes can be found in the [Solidity documentation](https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require). +Although both property-mode and assertion-mode try to validate / invalidate invariants of the system, they do so in different ways. In property-mode, `medusa` will look for functions with a specific test prefix (e.g. `fuzz_`) and test those. In assertion-mode, `medusa` will test to see if a given call sequence can cause the Ethereum Virtual Machine (EVM) to "panic". The EVM has a variety of panic codes for different scenarios. For example, there is a unique panic code when an `assert(x)` statement returns `false` or when a division by zero is encountered. In assertion mode, which panics should or should not be treated as "failing test cases" can be toggled by updating the [Project Configuration](../project_configuration/fuzzing_config.md#fuzzing-configuration). By default, only `FailOnAssertion` is enabled. Check out the [Example Project Configuration File](https://github.com/crytic/medusa/wiki/Example-Project-Configuration-File) for a visualization of the various panic codes that can be enabled. An explanation of the various panic codes can be found in the [Solidity documentation](https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require). Please note that the behavior of assertion mode is different between `medusa` and Echidna. Echidna will only test for `assert(x)` statements while `medusa` provides additional flexibility. @@ -83,7 +83,7 @@ During a call sequence, if `setX` is called with a `value` that breaks the asser To begin a fuzzing campaign in assertion-mode, you can run `medusa fuzz --assertion-mode` or `medusa fuzz --config [config_path] --assertion-mode`. -> **Note**: Learn more about running `medusa` with its CLI [here](./Command-Line-Interface.md). +> **Note**: Learn more about running `medusa` with its CLI [here](../cli/overview.md). Invoking this fuzzing campaign, `medusa` will: @@ -127,7 +127,7 @@ contract TestContract { To begin a fuzzing campaign in optimization-mode, you can run `medusa fuzz --optimization-mode` or `medusa fuzz --config [config_path] --optimization-mode`. -> **Note**: Learn more about running `medusa` with its CLI [here](./Command-Line-Interface.md). +> **Note**: Learn more about running `medusa` with its CLI [here](../cli/overview.md). Invoking this fuzzing campaign, `medusa` will: @@ -149,7 +149,7 @@ Optimization test "TestContract.optimize_opt_linear()" resulted in the maximum v ## Testing with multiple modes -Note that we can run `medusa` with one, many, or no modes enabled. Running `medusa fuzz --assertion-mode --optimization-mode` will run all three modes at the same time, since property-mode is enabled by default. If a project configuration file is used, any combination of the three modes can be toggled. In fact, all three modes can be disabled and `medusa` will still run. Please review the [Project Configuration](./Project-Configuration.md) wiki page and the [Project Configuration Example](/Example-Project-Configuration-File.md) for more information. +Note that we can run `medusa` with one, many, or no modes enabled. Running `medusa fuzz --assertion-mode --optimization-mode` will run all three modes at the same time, since property-mode is enabled by default. If a project configuration file is used, any combination of the three modes can be toggled. In fact, all three modes can be disabled and `medusa` will still run. Please review the [Project Configuration](https://github.com/crytic/medusa/wiki/Project-Configuration) wiki page and the [Project Configuration Example](https://github.com/crytic/medusa/wiki/Example-Project-Configuration-File) for more information. ```solidity contract TestContract { From a724090e2ab6d848ec3dc93aad4822597c01e18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ezequiel=20P=C3=A9rez?= <67927465+EperezOk@users.noreply.github.com> Date: Fri, 19 Jul 2024 13:03:53 -0300 Subject: [PATCH 074/109] Update version (#403) Co-authored-by: anishnaik --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 6dd04a3d..099a0571 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.3" +const version = "0.1.4" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From 7b67cebd5b36bdc8005f935c0b9c1522c3a514d3 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Fri, 19 Jul 2024 13:18:36 -0400 Subject: [PATCH 075/109] upgrade to v0.1.5 (#405) --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 099a0571..7f7ec9ad 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.4" +const version = "0.1.5" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From 1ec85c784bfef97a86bf50da98163de73a74875a Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 23 Jul 2024 09:00:01 -0500 Subject: [PATCH 076/109] fix: refresh generated code to include new configs (#409) * fix: refresh generated code to include new configs * add check in CI to ensure gen. code is up-to-date --- .github/workflows/ci.yml | 7 +++++++ fuzzing/config/gen_fuzzing_config.go | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 861b85ac..98641bb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,6 +156,13 @@ jobs: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest golangci-lint run --timeout 5m0s + - name: Gencodec + run: | + go get github.com/fjl/gencodec + pushd fuzzing/config + go run github.com/fjl/gencodec -type FuzzingConfig -field-override fuzzingConfigMarshaling -out gen_fuzzing_config.go + git diff --exit-code -- . + popd test: strategy: matrix: diff --git a/fuzzing/config/gen_fuzzing_config.go b/fuzzing/config/gen_fuzzing_config.go index 5f1de304..6a2784b7 100644 --- a/fuzzing/config/gen_fuzzing_config.go +++ b/fuzzing/config/gen_fuzzing_config.go @@ -19,10 +19,12 @@ func (f FuzzingConfig) MarshalJSON() ([]byte, error) { WorkerResetLimit int `json:"workerResetLimit"` Timeout int `json:"timeout"` TestLimit uint64 `json:"testLimit"` + ShrinkLimit uint64 `json:"shrinkLimit"` CallSequenceLength int `json:"callSequenceLength"` CorpusDirectory string `json:"corpusDirectory"` CoverageEnabled bool `json:"coverageEnabled"` TargetContracts []string `json:"targetContracts"` + PredeployedContracts map[string]string `json:"predeployedContracts"` TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` ConstructorArgs map[string]map[string]any `json:"constructorArgs"` DeployerAddress string `json:"deployerAddress"` @@ -39,10 +41,12 @@ func (f FuzzingConfig) MarshalJSON() ([]byte, error) { enc.WorkerResetLimit = f.WorkerResetLimit enc.Timeout = f.Timeout enc.TestLimit = f.TestLimit + enc.ShrinkLimit = f.ShrinkLimit enc.CallSequenceLength = f.CallSequenceLength enc.CorpusDirectory = f.CorpusDirectory enc.CoverageEnabled = f.CoverageEnabled enc.TargetContracts = f.TargetContracts + enc.PredeployedContracts = f.PredeployedContracts if f.TargetContractsBalances != nil { enc.TargetContractsBalances = make([]*hexutil.Big, len(f.TargetContractsBalances)) for k, v := range f.TargetContractsBalances { @@ -68,10 +72,12 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { WorkerResetLimit *int `json:"workerResetLimit"` Timeout *int `json:"timeout"` TestLimit *uint64 `json:"testLimit"` + ShrinkLimit *uint64 `json:"shrinkLimit"` CallSequenceLength *int `json:"callSequenceLength"` CorpusDirectory *string `json:"corpusDirectory"` CoverageEnabled *bool `json:"coverageEnabled"` TargetContracts []string `json:"targetContracts"` + PredeployedContracts map[string]string `json:"predeployedContracts"` TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` ConstructorArgs map[string]map[string]any `json:"constructorArgs"` DeployerAddress *string `json:"deployerAddress"` @@ -99,6 +105,9 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { if dec.TestLimit != nil { f.TestLimit = *dec.TestLimit } + if dec.ShrinkLimit != nil { + f.ShrinkLimit = *dec.ShrinkLimit + } if dec.CallSequenceLength != nil { f.CallSequenceLength = *dec.CallSequenceLength } @@ -111,6 +120,9 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { if dec.TargetContracts != nil { f.TargetContracts = dec.TargetContracts } + if dec.PredeployedContracts != nil { + f.PredeployedContracts = dec.PredeployedContracts + } if dec.TargetContractsBalances != nil { f.TargetContractsBalances = make([]*big.Int, len(dec.TargetContractsBalances)) for k, v := range dec.TargetContractsBalances { From b4d4c56abea9291d8fa851ca57b7521c5ca407c4 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 24 Jul 2024 14:13:26 -0500 Subject: [PATCH 077/109] fix: get code from scope to report constructor coverage correctly (#412) --- fuzzing/coverage/coverage_tracer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fuzzing/coverage/coverage_tracer.go b/fuzzing/coverage/coverage_tracer.go index fd32309b..a495c20a 100644 --- a/fuzzing/coverage/coverage_tracer.go +++ b/fuzzing/coverage/coverage_tracer.go @@ -155,7 +155,9 @@ func (t *CoverageTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tr // If there is code we're executing, collect coverage. address := scope.Address() - code := t.evmContext.StateDB.GetCode(address) + // We can cast OpContext to ScopeContext because that is the type passed to OnOpcode. + scopeContext := scope.(*vm.ScopeContext) + code := scopeContext.Contract.Code codeSize := len(code) if codeSize > 0 { From 924247c5e862cc367f58d1f6c061bde919076cba Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 24 Jul 2024 15:48:08 -0400 Subject: [PATCH 078/109] Fix panic when execution tracing cheatcode contracts (#411) * fix panic when execution tracing cheatcode contracts. * Update chain/test_chain.go Co-authored-by: alpharush <0xalpharush@protonmail.com> --------- Co-authored-by: alpharush <0xalpharush@protonmail.com> --- chain/test_chain.go | 29 ++++++++++----------- chain/test_chain_test.go | 10 +++---- fuzzing/calls/call_sequence_execution.go | 15 ++++------- fuzzing/corpus/corpus.go | 2 +- fuzzing/executiontracer/execution_tracer.go | 2 ++ fuzzing/fuzzer.go | 2 +- fuzzing/fuzzer_worker.go | 4 +-- 7 files changed, 30 insertions(+), 34 deletions(-) diff --git a/chain/test_chain.go b/chain/test_chain.go index c1400311..0137c6c3 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -9,7 +9,6 @@ import ( "github.com/crytic/medusa/chain/config" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/tracing" - "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/hashdb" "github.com/holiman/uint256" @@ -256,7 +255,7 @@ func (t *TestChain) Clone(onCreateFunc func(chain *TestChain) error) (*TestChain // Now add each transaction/message to it. messages := t.blocks[i].Messages for j := 0; j < len(messages); j++ { - err = targetChain.PendingBlockAddTx(messages[j], nil) + err = targetChain.PendingBlockAddTx(messages[j]) if err != nil { return nil, err } @@ -728,15 +727,7 @@ func (t *TestChain) PendingBlockCreateWithParameters(blockNumber uint64, blockTi // PendingBlockAddTx takes a message (internal txs) and adds it to the current pending block, updating the header // with relevant execution information. If a pending block was not created, an error is returned. // Returns an error if one occurred. -func (t *TestChain) PendingBlockAddTx(message *core.Message, getTracerFn func(txIndex int, txHash common.Hash) *tracers.Tracer) error { - // Caller can specify any tracer they wish to use for this transaction - if getTracerFn == nil { - // TODO: Figure out whether it is possible to identify _which_ transaction you want to trace (versus all) - getTracerFn = func(txIndex int, txHash common.Hash) *tracers.Tracer { - return t.transactionTracerRouter.NativeTracer().Tracer - } - } - +func (t *TestChain) PendingBlockAddTx(message *core.Message, additionalTracers ...*TestChainTracer) error { // If we don't have a pending block, return an error if t.pendingBlock == nil { return errors.New("could not add tx to the chain's pending block because no pending block was created") @@ -757,12 +748,20 @@ func (t *TestChain) PendingBlockAddTx(message *core.Message, getTracerFn func(tx ConfigExtensions: t.vmConfigExtensions, } - // Set the tracer to be used in the vm config - tracer := getTracerFn(len(t.pendingBlock.Messages), tx.Hash()) - if tracer != nil { - vmConfig.Tracer = tracer.Hooks + // Figure out whether we need to attach any more tracers + var extendedTracerRouter *TestChainTracerRouter + if len(additionalTracers) > 0 { + // If we have more tracers, extend the transaction tracer router's tracers with additional ones + extendedTracerRouter = NewTestChainTracerRouter() + extendedTracerRouter.AddTracer(t.transactionTracerRouter.NativeTracer()) + extendedTracerRouter.AddTracers(additionalTracers...) + } else { + extendedTracerRouter = t.transactionTracerRouter } + // Update the VM's tracer + vmConfig.Tracer = extendedTracerRouter.NativeTracer().Tracer.Hooks + // Set tx context t.state.SetTxContext(tx.Hash(), len(t.pendingBlock.Messages)) diff --git a/chain/test_chain_test.go b/chain/test_chain_test.go index 693260fc..5d33ceea 100644 --- a/chain/test_chain_test.go +++ b/chain/test_chain_test.go @@ -260,7 +260,7 @@ func TestChainDynamicDeployments(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg, nil) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -385,7 +385,7 @@ func TestChainDeploymentWithArgs(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg, nil) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -494,7 +494,7 @@ func TestChainCloning(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg, nil) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -588,7 +588,7 @@ func TestChainCallSequenceReplayMatchSimple(t *testing.T) { assert.NoError(t, err) // Add our transaction to the block - err = chain.PendingBlockAddTx(&msg, nil) + err = chain.PendingBlockAddTx(&msg) assert.NoError(t, err) // Commit the pending block to the chain, so it becomes the new head. @@ -627,7 +627,7 @@ func TestChainCallSequenceReplayMatchSimple(t *testing.T) { _, err := recreatedChain.PendingBlockCreate() assert.NoError(t, err) for _, message := range chain.blocks[i].Messages { - err = recreatedChain.PendingBlockAddTx(message, nil) + err = recreatedChain.PendingBlockAddTx(message) assert.NoError(t, err) } err = recreatedChain.PendingBlockCommit() diff --git a/fuzzing/calls/call_sequence_execution.go b/fuzzing/calls/call_sequence_execution.go index 14fc1ecc..ca983f0d 100644 --- a/fuzzing/calls/call_sequence_execution.go +++ b/fuzzing/calls/call_sequence_execution.go @@ -7,8 +7,6 @@ import ( "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/tracers" ) // ExecuteCallSequenceFetchElementFunc describes a function that is called to obtain the next call sequence element to @@ -28,7 +26,7 @@ type ExecuteCallSequenceExecutionCheckFunc func(currentExecutedSequence CallSequ // A "post element executed check" function is provided to check whether execution should stop after each element is // executed. // Returns the call sequence which was executed and an error if one occurs. -func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc, getTracerFn func(txIndex int, txHash common.Hash) *tracers.Tracer) (CallSequence, error) { +func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc, additionalTracers ...*chain.TestChainTracer) (CallSequence, error) { // If there is no fetch element function provided, throw an error if fetchElementFunc == nil { return nil, fmt.Errorf("could not execute call sequence on chain as the 'fetch element function' provided was nil") @@ -90,7 +88,7 @@ func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc Exe } // Try to add our transaction to this block. - err = chain.PendingBlockAddTx(callSequenceElement.Call.ToCoreMessage(), getTracerFn) + err = chain.PendingBlockAddTx(callSequenceElement.Call.ToCoreMessage(), additionalTracers...) if err != nil { // If we encountered a block gas limit error, this tx is too expensive to fit in this block. @@ -168,7 +166,7 @@ func ExecuteCallSequence(chain *chain.TestChain, callSequence CallSequence) (Cal return nil, nil } - return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil, nil) + return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil) } // ExecuteCallSequenceWithTracer attaches an executiontracer.ExecutionTracer to ExecuteCallSequenceIteratively and attaches execution traces to the call sequence elements. @@ -176,9 +174,6 @@ func ExecuteCallSequenceWithExecutionTracer(testChain *chain.TestChain, contract // Create a new execution tracer executionTracer := executiontracer.NewExecutionTracer(contractDefinitions, testChain.CheatCodeContracts()) defer executionTracer.Close() - getTracerFunc := func(txIndex int, txHash common.Hash) *tracers.Tracer { - return executionTracer.NativeTracer().Tracer - } // Execute our sequence with a simple fetch operation provided to obtain each element. fetchElementFunc := func(currentIndex int) (*CallSequenceElement, error) { @@ -188,8 +183,8 @@ func ExecuteCallSequenceWithExecutionTracer(testChain *chain.TestChain, contract return nil, nil } - // Execute the call sequence - executedCallSeq, err := ExecuteCallSequenceIteratively(testChain, fetchElementFunc, nil, getTracerFunc) + // Execute the call sequence and attach the execution tracer + executedCallSeq, err := ExecuteCallSequenceIteratively(testChain, fetchElementFunc, nil, executionTracer.NativeTracer()) // By default, we only trace the last element in the call sequence. traceFrom := len(callSequence) - 1 diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 011b414d..8426f156 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -210,7 +210,7 @@ func (c *Corpus) initializeSequences(sequenceFiles *corpusDirectory[calls.CallSe } // Execute each call sequence, populating runtime data and collecting coverage data along the way. - _, err = calls.ExecuteCallSequenceIteratively(testChain, fetchElementFunc, executionCheckFunc, nil) + _, err = calls.ExecuteCallSequenceIteratively(testChain, fetchElementFunc, executionCheckFunc) // If we failed to replay a sequence and measure coverage due to an unexpected error, report it. if err != nil { diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index 651f9379..46bfb01a 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -119,6 +119,8 @@ func (t *ExecutionTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transac t.trace = newExecutionTrace(t.contractDefinitions) t.currentCallFrame = nil t.onNextCaptureState = nil + t.traceMap = make(map[common.Hash]*ExecutionTrace) + // Store our evm reference t.evmContext = vm } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 6f1d6e8b..960ebfe2 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -463,7 +463,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex } // Add our transaction to the block - err = testChain.PendingBlockAddTx(msg.ToCoreMessage(), nil) + err = testChain.PendingBlockAddTx(msg.ToCoreMessage()) if err != nil { return nil, err } diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 5f651325..7ac958b2 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -317,7 +317,7 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall } // Execute our call sequence. - testedCallSequence, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc, nil) + testedCallSequence, err := calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) // If we encountered an error, report it. if err != nil { @@ -383,7 +383,7 @@ func (fw *FuzzerWorker) testShrunkenCallSequence(possibleShrunkSequence calls.Ca } // Execute our call sequence. - _, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc, nil) + _, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc) if err != nil { return false, err } From 275191f789284882c931aed55214c359972e2759 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 25 Jul 2024 15:09:11 -0500 Subject: [PATCH 079/109] fix: weight methods correctly to avoid skipping some --- fuzzing/fuzzer_worker.go | 7 ------ fuzzing/fuzzer_worker_sequence_generator.go | 25 ++++++++++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 7ac958b2..57437227 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -11,7 +11,6 @@ import ( "github.com/crytic/medusa/fuzzing/coverage" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" - "github.com/crytic/medusa/utils/randomutils" "github.com/ethereum/go-ethereum/common" "golang.org/x/exp/maps" ) @@ -44,9 +43,6 @@ type FuzzerWorker struct { // pureMethods is a list of contract functions which are side-effect free with respect to the EVM (view and/or pure in terms of Solidity mutability). pureMethods []fuzzerTypes.DeployedContractMethod - // methodChooser uses a weighted selection algorithm to choose a method to call, prioritizing state changing methods over pure ones. - methodChooser *randomutils.WeightedRandomChooser[fuzzerTypes.DeployedContractMethod] - // randomProvider provides random data as inputs to decisions throughout the worker. randomProvider *rand.Rand // sequenceGenerator creates entirely new or mutated call sequences based on corpus call sequences, for use in @@ -94,7 +90,6 @@ func newFuzzerWorker(fuzzer *Fuzzer, workerIndex int, randomProvider *rand.Rand) coverageTracer: nil, randomProvider: randomProvider, valueSet: valueSet, - methodChooser: randomutils.NewWeightedRandomChooser[fuzzerTypes.DeployedContractMethod](), } worker.sequenceGenerator = NewCallSequenceGenerator(worker, callSequenceGenConfig) worker.shrinkingValueMutator = shrinkingValueMutator @@ -245,10 +240,8 @@ func (fw *FuzzerWorker) updateMethods() { // We favor calling state changing methods over view/pure methods. if method.IsConstant() { fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) - fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(1))) } else { fw.stateChangingMethods = append(fw.stateChangingMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) - fw.methodChooser.AddChoices(randomutils.NewWeightedRandomChoice(fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}, big.NewInt(100))) } } } diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index edc3b224..c0267ab3 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -5,6 +5,7 @@ import ( "math/big" "github.com/crytic/medusa/fuzzing/calls" + "github.com/crytic/medusa/fuzzing/contracts" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" "github.com/crytic/medusa/utils/randomutils" @@ -274,14 +275,26 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement // deployed to the CallSequenceGenerator's parent FuzzerWorker chain, with fuzzed call data. // Returns the call sequence element, or an error if one was encountered. func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement, error) { - // Verify we have state changing methods to call if we are not testing view/pure methods. - if len(g.worker.stateChangingMethods) == 0 && !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { - return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call") + // Verify we have state changing methods to call. + onlyPure := false + if len(g.worker.stateChangingMethods) == 0 { + if !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { + return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call") + } else if len(g.worker.pureMethods) == 0 { + return nil, fmt.Errorf("cannot generate fuzzed call as there are no methods to call") + } else { + // TestViewMethods && len(g.worker.pureMethods) > 0 + onlyPure = true + } } // Select a random method and sender - selectedMethod, err := g.worker.methodChooser.Choose() - if err != nil { - return nil, err + + // If available, 1 out 100 calls will be pure/view method calls. + var selectedMethod *contracts.DeployedContractMethod + if len(g.worker.pureMethods) > 0 && g.worker.randomProvider.Intn(100) == 0 || onlyPure { + selectedMethod = &g.worker.pureMethods[g.worker.randomProvider.Intn(len(g.worker.pureMethods))] + } else { + selectedMethod = &g.worker.stateChangingMethods[g.worker.randomProvider.Intn(len(g.worker.stateChangingMethods))] } selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] From 7a1d236be771760231eb2950c3ade8c4b37d44b8 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Thu, 25 Jul 2024 16:54:37 -0400 Subject: [PATCH 080/109] fix commenting --- fuzzing/fuzzer_worker_sequence_generator.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index c0267ab3..ae91a267 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -275,21 +275,28 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement // deployed to the CallSequenceGenerator's parent FuzzerWorker chain, with fuzzed call data. // Returns the call sequence element, or an error if one was encountered. func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement, error) { - // Verify we have state changing methods to call. + // This will track if the fuzzer worker can only invoke pure functions onlyPure := false + + // Verify we have any method (state-changing or pure) to call if len(g.worker.stateChangingMethods) == 0 { if !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { + // There are no state-changing methods and we are not testing view/pure methods return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call") } else if len(g.worker.pureMethods) == 0 { + // There are no pure functions to call either return nil, fmt.Errorf("cannot generate fuzzed call as there are no methods to call") - } else { - // TestViewMethods && len(g.worker.pureMethods) > 0 - onlyPure = true } + // Now we know that there are no state-changing functions, there are pure functions, and we can call the + // pure functions + onlyPure = true } - // Select a random method and sender - // If available, 1 out 100 calls will be pure/view method calls. + // Select a random sender + selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] + + // Select a random method + // There is a 1/100 chance that a pure method will be invoked (or there are onl pure functions) var selectedMethod *contracts.DeployedContractMethod if len(g.worker.pureMethods) > 0 && g.worker.randomProvider.Intn(100) == 0 || onlyPure { selectedMethod = &g.worker.pureMethods[g.worker.randomProvider.Intn(len(g.worker.pureMethods))] @@ -297,8 +304,6 @@ func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement selectedMethod = &g.worker.stateChangingMethods[g.worker.randomProvider.Intn(len(g.worker.stateChangingMethods))] } - selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] - // Generate fuzzed parameters for the function call args := make([]any, len(selectedMethod.Method.Inputs)) for i := 0; i < len(args); i++ { From 3f1b0a976812917c2280b9f7f2c99e8adb20efc3 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 25 Jul 2024 16:38:11 -0500 Subject: [PATCH 081/109] add debugging scripts --- DEV.md | 104 ++++++++++++++++++++ fuzzing/fuzzer_worker_sequence_generator.go | 6 +- scripts/corpus_diff.py | 63 ++++++++++++ scripts/corpus_stats.py | 57 +++++++++++ 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 DEV.md create mode 100644 scripts/corpus_diff.py create mode 100644 scripts/corpus_stats.py diff --git a/DEV.md b/DEV.md new file mode 100644 index 00000000..7543fe56 --- /dev/null +++ b/DEV.md @@ -0,0 +1,104 @@ +# Debugging and Development + +## Debugging + +The following scripts are available for Medusa developers for debugging changes to the fuzzer. + +### Corpus diff + +The corpus diff script is used to compare two corpora and identify the methods that are present in one but not the other. This is useful for identifying methods that are missing from a corpus that should be present. + +```shell +python3 scripts/corpus_diff.py corpus1 corpus2 +``` + +```shell +Methods only in ~/corpus1: +- clampSplitWeight(uint32,uint32) + +Methods only in ~/corpus2: + +``` + +### Corpus stats + +The corpus stats script is used to generate statistics about a corpus. This includes the number of sequences, the average length of sequences, and the frequency of methods called. + +```shell +python3 scripts/corpus_stats.py corpus +``` + +```shell +Number of Sequences in ~/corpus: 130 + +Average Length of Transactions List: 43 + +Frequency of Methods Called: +- testReceiversReceivedSplit(uint8): 280 +- setMaxEndHints(uint32,uint32): 174 +- setStreamBalanceWithdrawAll(uint8): 139 +- giveClampedAmount(uint8,uint8,uint128): 136 +- receiveStreamsSplitAndCollectToSelf(uint8): 133 +- testSqueezeViewVsActual(uint8,uint8): 128 +- testSqueeze(uint8,uint8): 128 +- testSetStreamBalance(uint8,int128): 128 +- addStreamWithClamping(uint8,uint8,uint160,uint32,uint32,int128): 125 +- removeAllSplits(uint8): 118 +- testSplittableAfterSplit(uint8): 113 +- testSqueezableVsReceived(uint8): 111 +- testBalanceAtInFuture(uint8,uint8,uint160): 108 +- testRemoveStreamShouldNotRevert(uint8,uint256): 103 +- invariantWithdrawAllTokensShouldNotRevert(): 103 +- collect(uint8,uint8): 101 +- invariantAmtPerSecVsMinAmtPerSec(uint8,uint256): 98 +- testSqueezableAmountCantBeWithdrawn(uint8,uint8): 97 +- split(uint8): 97 +- invariantWithdrawAllTokens(): 95 +- testReceiveStreams(uint8,uint32): 93 +- invariantAccountingVsTokenBalance(): 92 +- testSqueezeWithFuzzedHistoryShouldNotRevert(uint8,uint8,uint256,bytes32): 91 +- testSqueezableAmountCantBeUndone(uint8,uint8,uint160,uint32,uint32,int128): 87 +- testCollect(uint8,uint8): 86 +- testSetStreamBalanceWithdrawAllShouldNotRevert(uint8): 86 +- testAddStreamShouldNotRevert(uint8,uint8,uint160,uint32,uint32,int128): 85 +- testReceiveStreamsShouldNotRevert(uint8): 84 +- addSplitsReceiver(uint8,uint8,uint32): 84 +- setStreamBalanceWithClamping(uint8,int128): 82 +- addSplitsReceiverWithClamping(uint8,uint8,uint32): 80 +- testSetStreamBalanceShouldNotRevert(uint8,int128): 80 +- testSplitShouldNotRevert(uint8): 80 +- squeezeAllAndReceiveAndSplitAndCollectToSelf(uint8): 79 +- addStreamImmediatelySqueezable(uint8,uint8,uint160): 79 +- testSetSplitsShouldNotRevert(uint8,uint8,uint32): 78 +- invariantSumAmtDeltaIsZero(uint8): 78 +- testReceiveStreamsViewConsistency(uint8,uint32): 76 +- squeezeToSelf(uint8): 74 +- collectToSelf(uint8): 72 +- setStreams(uint8,uint8,uint160,uint32,uint32,int128): 70 +- receiveStreamsAllCycles(uint8): 69 +- invariantWithdrawShouldAlwaysFail(uint256): 68 +- addStream(uint8,uint8,uint160,uint32,uint32,int128): 68 +- squeezeWithFuzzedHistory(uint8,uint8,uint256,bytes32): 67 +- setStreamsWithClamping(uint8,uint8,uint160,uint32,uint32,int128): 67 +- splitAndCollectToSelf(uint8): 67 +- testSqueezeWithFullyHashedHistory(uint8,uint8): 65 +- give(uint8,uint8,uint128): 65 +- setSplits(uint8,uint8,uint32): 65 +- testSqueezeTwice(uint8,uint8,uint256,bytes32): 65 +- testSetStreamsShouldNotRevert(uint8,uint8,uint160,uint32,uint32,int128): 64 +- squeezeAllSenders(uint8): 63 +- removeStream(uint8,uint256): 62 +- testCollectableAfterSplit(uint8): 58 +- testCollectShouldNotRevert(uint8,uint8): 56 +- testReceiveStreamsViewVsActual(uint8,uint32): 55 +- receiveStreams(uint8,uint32): 55 +- setSplitsWithClamping(uint8,uint8,uint32): 55 +- testGiveShouldNotRevert(uint8,uint8,uint128): 47 +- setStreamBalance(uint8,int128): 47 +- squeezeWithDefaultHistory(uint8,uint8): 45 +- testSplitViewVsActual(uint8): 45 +- testAddSplitsShouldNotRevert(uint8,uint8,uint32): 30 +- testSqueezeWithDefaultHistoryShouldNotRevert(uint8,uint8): 23 + +Number of Unique Methods: 65 +``` diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index ae91a267..b3f66caf 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -287,8 +287,8 @@ func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement // There are no pure functions to call either return nil, fmt.Errorf("cannot generate fuzzed call as there are no methods to call") } - // Now we know that there are no state-changing functions, there are pure functions, and we can call the - // pure functions + // Since there are no state-changing functions, there are pure functions, and TestViewMethods is enabled, we can call + // exclusively pure functions. onlyPure = true } @@ -296,7 +296,7 @@ func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] // Select a random method - // There is a 1/100 chance that a pure method will be invoked (or there are onl pure functions) + // There is a 1/100 chance that a pure method will be invoked (or there are only pure functions) var selectedMethod *contracts.DeployedContractMethod if len(g.worker.pureMethods) > 0 && g.worker.randomProvider.Intn(100) == 0 || onlyPure { selectedMethod = &g.worker.pureMethods[g.worker.randomProvider.Intn(len(g.worker.pureMethods))] diff --git a/scripts/corpus_diff.py b/scripts/corpus_diff.py new file mode 100644 index 00000000..b622f212 --- /dev/null +++ b/scripts/corpus_diff.py @@ -0,0 +1,63 @@ +import os +import json +import sys + +def load_json_files_from_subdirectory(subdirectory): + json_data = [] + for root, _, files in os.walk(subdirectory): + for file in files: + if file.endswith('.json'): + with open(os.path.join(root, file), 'r') as f: + data = json.load(f) + json_data.extend(data) + return json_data + +def extract_unique_methods(transactions): + unique_methods = set() + for tx in transactions: + call_data = tx.get('call', {}) + data_abi_values = call_data.get('dataAbiValues', {}) + method_signature = data_abi_values.get('methodSignature', '') + if method_signature: + unique_methods.add(method_signature) + return unique_methods + +def compare_methods(subdirectory1, subdirectory2): + transactions1 = load_json_files_from_subdirectory(subdirectory1) + transactions2 = load_json_files_from_subdirectory(subdirectory2) + + unique_methods1 = extract_unique_methods(transactions1) + unique_methods2 = extract_unique_methods(transactions2) + + only_in_subdir1 = unique_methods1 - unique_methods2 + only_in_subdir2 = unique_methods2 - unique_methods1 + + return only_in_subdir1, only_in_subdir2 + +def main(subdirectory1, subdirectory2): + + only_in_subdir1, only_in_subdir2 = compare_methods(subdirectory1, subdirectory2) + + print(f"Methods only in {subdirectory1}:") + if len(only_in_subdir1) == 0: + print(" ") + else: + for method in only_in_subdir1: + print(f"- {method}") + print("\n") + + + print(f"Methods only in {subdirectory2}:") + if len(only_in_subdir2) == 0: + print(" ") + else: + for method in only_in_subdir2: + print(f"- {method}") + print("\n") + +if __name__ == '__main__': + if len(sys.argv) != 3: + print("Usage: python3 unique.py ") + print("Compares the unique methods in the two given corpora.") + sys.exit(1) + main(sys.argv[1], sys.argv[2]) diff --git a/scripts/corpus_stats.py b/scripts/corpus_stats.py new file mode 100644 index 00000000..a5c818f8 --- /dev/null +++ b/scripts/corpus_stats.py @@ -0,0 +1,57 @@ +import os +import json +from collections import Counter +import sys + +def load_json_files_from_subdirectory(subdirectory): + json_data = [] + for root, _, files in os.walk(subdirectory): + for file in files: + if file.endswith('.json'): + with open(os.path.join(root, file), 'r') as f: + data = json.load(f) + json_data.append(data) + return json_data + + +def analyze_transactions(transactions, method_counter): + + for tx in transactions: + call_data = tx.get('call', {}) + data_abi_values = call_data.get('dataAbiValues', {}) + method_signature = data_abi_values.get('methodSignature', '') + + method_counter[method_signature] += 1 + + + +def main(subdirectory): + transaction_seqs = load_json_files_from_subdirectory(subdirectory) + + method_counter = Counter() + total_length = 0 + + for seq in transaction_seqs: + analyze_transactions(seq, method_counter) + total_length += len(seq) + + average_length = total_length // len(transaction_seqs) + + print(f"Number of Sequences in {subdirectory}: {len(transaction_seqs)}") + print("\n") + + print(f"Average Length of Transactions List: {average_length}") + print("\n") + print("Frequency of Methods Called:") + for method, count in method_counter.most_common(): + print(f"- {method}: {count}") + print("\n") + print(f"Number of Unique Methods: {len(method_counter)}") + print("\n") + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("Usage: python3 corpus_stats.py ") + print("Computes statistics on the transactions in the given corpus.") + sys.exit(1) + main(sys.argv[1]) From ca07f6c96354336d81f93444c0b05f06afe4fd13 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Thu, 25 Jul 2024 18:14:09 -0400 Subject: [PATCH 082/109] zero clue if i optimized anything at all... --- fuzzing/fuzzer_worker.go | 6 ++-- fuzzing/fuzzer_worker_sequence_generator.go | 32 +++++++++------------ 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index 57437227..b5f2db80 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -237,9 +237,11 @@ func (fw *FuzzerWorker) updateMethods() { // If we deployed the contract, also enumerate property tests and state changing methods. for _, method := range contractDefinition.AssertionTestMethods { // Any non-constant method should be tracked as a state changing method. - // We favor calling state changing methods over view/pure methods. if method.IsConstant() { - fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) + // Only track the pure/view method if testing view methods is enabled + if fw.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { + fw.pureMethods = append(fw.pureMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) + } } else { fw.stateChangingMethods = append(fw.stateChangingMethods, fuzzerTypes.DeployedContractMethod{Address: contractAddress, Contract: contractDefinition, Method: method}) } diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index ae91a267..666efb23 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -275,35 +275,29 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement // deployed to the CallSequenceGenerator's parent FuzzerWorker chain, with fuzzed call data. // Returns the call sequence element, or an error if one was encountered. func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement, error) { - // This will track if the fuzzer worker can only invoke pure functions - onlyPure := false - - // Verify we have any method (state-changing or pure) to call - if len(g.worker.stateChangingMethods) == 0 { - if !g.worker.fuzzer.config.Fuzzing.Testing.AssertionTesting.TestViewMethods { - // There are no state-changing methods and we are not testing view/pure methods - return nil, fmt.Errorf("cannot generate fuzzed tx as there are no state changing methods to call") - } else if len(g.worker.pureMethods) == 0 { - // There are no pure functions to call either - return nil, fmt.Errorf("cannot generate fuzzed call as there are no methods to call") - } - // Now we know that there are no state-changing functions, there are pure functions, and we can call the - // pure functions - onlyPure = true + // Check to make sure that we have any functions to call + if len(g.worker.stateChangingMethods) == 0 && len(g.worker.pureMethods) == 0 { + return nil, fmt.Errorf("cannot generate fuzzed call as there are no methods to call") } - // Select a random sender - selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] + // Only call view functions if there are no state-changing methods + var callOnlyPureFunctions bool + if len(g.worker.stateChangingMethods) == 0 && len(g.worker.pureMethods) > 0 { + callOnlyPureFunctions = true + } // Select a random method - // There is a 1/100 chance that a pure method will be invoked (or there are onl pure functions) + // There is a 1/100 chance that a pure method will be invoked or if there are only pure functions that are callable var selectedMethod *contracts.DeployedContractMethod - if len(g.worker.pureMethods) > 0 && g.worker.randomProvider.Intn(100) == 0 || onlyPure { + if (len(g.worker.pureMethods) > 0 && g.worker.randomProvider.Intn(100) == 0) || callOnlyPureFunctions { selectedMethod = &g.worker.pureMethods[g.worker.randomProvider.Intn(len(g.worker.pureMethods))] } else { selectedMethod = &g.worker.stateChangingMethods[g.worker.randomProvider.Intn(len(g.worker.stateChangingMethods))] } + // Select a random sender + selectedSender := g.worker.fuzzer.senders[g.worker.randomProvider.Intn(len(g.worker.fuzzer.senders))] + // Generate fuzzed parameters for the function call args := make([]any, len(selectedMethod.Method.Inputs)) for i := 0; i < len(args); i++ { From d1d6344cc3ccb31062bd46017d31c2cd43fbb322 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 25 Jul 2024 22:32:52 -0500 Subject: [PATCH 083/109] upload artifact on every PR --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98641bb0..36aade25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,6 @@ jobs: inputs: ./medusa-*.tar.gz - name: Upload artifact - if: github.ref == 'refs/heads/master' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) uses: actions/upload-artifact@v4 with: name: medusa-${{ runner.os }}-${{ runner.arch }} From f54d046ccaae92c3fd68f765aa8830ffe46a17be Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Fri, 26 Jul 2024 15:50:44 -0500 Subject: [PATCH 084/109] fix: log number of workers shrinking (#8) * fix: log number of workers shrinking * report total # failed sequences/ total sequences tested --- fuzzing/fuzzer.go | 7 +++++-- fuzzing/fuzzer_metrics.go | 12 ++++++++++++ fuzzing/test_case_assertion_provider.go | 2 ++ fuzzing/test_case_property_provider.go | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 960ebfe2..f1171cc2 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/ethereum/go-ethereum/crypto" "math/big" "math/rand" "os" @@ -16,6 +15,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/fuzzing/coverage" @@ -846,6 +847,7 @@ func (f *Fuzzer) printMetricsLoop() { // Obtain our metrics callsTested := f.metrics.CallsTested() sequencesTested := f.metrics.SequencesTested() + failedSequences := f.metrics.FailedSequences() workerStartupCount := f.metrics.WorkerStartupCount() workersShrinking := f.metrics.WorkersShrinkingCount() @@ -865,8 +867,9 @@ func (f *Fuzzer) printMetricsLoop() { 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(", failures: ", colors.Bold, fmt.Sprintf("%d/%d", failedSequences, sequencesTested), 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) } diff --git a/fuzzing/fuzzer_metrics.go b/fuzzing/fuzzer_metrics.go index 70fc3788..b0984ab0 100644 --- a/fuzzing/fuzzer_metrics.go +++ b/fuzzing/fuzzer_metrics.go @@ -14,6 +14,9 @@ type fuzzerWorkerMetrics struct { // sequencesTested describes the amount of sequences of transactions which tests were run against. sequencesTested *big.Int + //failedSequences describes the amount of sequences of transactions which tests failed. + failedSequences *big.Int + // callsTested describes the amount of transactions/calls the fuzzer executed and ran tests against. callsTested *big.Int @@ -33,12 +36,21 @@ func newFuzzerMetrics(workerCount int) *FuzzerMetrics { } for i := 0; i < len(metrics.workerMetrics); i++ { metrics.workerMetrics[i].sequencesTested = big.NewInt(0) + metrics.workerMetrics[i].failedSequences = big.NewInt(0) metrics.workerMetrics[i].callsTested = big.NewInt(0) metrics.workerMetrics[i].workerStartupCount = big.NewInt(0) } return &metrics } +func (m *FuzzerMetrics) FailedSequences() *big.Int { + failedSequences := big.NewInt(0) + for _, workerMetrics := range m.workerMetrics { + failedSequences.Add(failedSequences, workerMetrics.failedSequences) + } + return failedSequences +} + // SequencesTested returns the amount of sequences of transactions the fuzzer executed and ran tests against. func (m *FuzzerMetrics) SequencesTested() *big.Int { sequencesTested := big.NewInt(0) diff --git a/fuzzing/test_case_assertion_provider.go b/fuzzing/test_case_assertion_provider.go index 8ab4a5bd..f9b9978a 100644 --- a/fuzzing/test_case_assertion_provider.go +++ b/fuzzing/test_case_assertion_provider.go @@ -1,6 +1,7 @@ package fuzzing import ( + "math/big" "sync" "github.com/crytic/medusa/compilation/abiutils" @@ -212,6 +213,7 @@ func (t *AssertionTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorke // Update our test state and report it finalized. testCase.status = TestCaseStatusFailed testCase.callSequence = &shrunkenCallSequence + worker.workerMetrics().failedSequences.Add(worker.workerMetrics().failedSequences, big.NewInt(1)) worker.Fuzzer().ReportTestCaseFinished(testCase) return nil }, diff --git a/fuzzing/test_case_property_provider.go b/fuzzing/test_case_property_provider.go index 3681d218..6bb6d419 100644 --- a/fuzzing/test_case_property_provider.go +++ b/fuzzing/test_case_property_provider.go @@ -332,6 +332,7 @@ func (t *PropertyTestCaseProvider) callSequencePostCallTest(worker *FuzzerWorker testCase.status = TestCaseStatusFailed testCase.callSequence = &shrunkenCallSequence testCase.propertyTestTrace = executionTrace + worker.workerMetrics().failedSequences.Add(worker.workerMetrics().failedSequences, big.NewInt(1)) worker.Fuzzer().ReportTestCaseFinished(testCase) return nil }, From 21022fd4749ab8dabc7a45891b6d78661ac62b89 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Sat, 27 Jul 2024 23:19:32 -0500 Subject: [PATCH 085/109] fix: lookup by source unit id instead of reyling on sourceList ordering --- chain/test_chain_test.go | 12 +-- compilation/platforms/crytic_compile.go | 60 +++++++++--- compilation/platforms/crytic_compile_test.go | 43 ++++----- compilation/platforms/solc.go | 47 ++++++---- compilation/types/ast.go | 98 ++++++++++++++++++++ compilation/types/compilation.go | 26 ++---- compilation/types/compiled_contract.go | 3 + compilation/types/compiled_source.go | 7 +- compilation/types/source_maps.go | 11 ++- fuzzing/coverage/source_analysis.go | 30 +++--- 10 files changed, 242 insertions(+), 95 deletions(-) create mode 100644 compilation/types/ast.go diff --git a/chain/test_chain_test.go b/chain/test_chain_test.go index 5d33ceea..ff0ca589 100644 --- a/chain/test_chain_test.go +++ b/chain/test_chain_test.go @@ -215,7 +215,7 @@ func TestChainDynamicDeployments(t *testing.T) { compilations, _, err := cryticCompile.Compile() assert.NoError(t, err) assert.EqualValues(t, 1, len(compilations)) - assert.EqualValues(t, 1, len(compilations[0].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) // Obtain our chain and senders chain, senders := createChain(t) @@ -223,7 +223,7 @@ func TestChainDynamicDeployments(t *testing.T) { // Deploy each contract that has no construct arguments. deployCount := 0 for _, compilation := range compilations { - for _, source := range compilation.Sources { + for _, source := range compilation.SourcePathToArtifact { for _, contract := range source.Contracts { contract := contract if len(contract.Abi.Constructor.Inputs) == 0 { @@ -329,7 +329,7 @@ func TestChainDeploymentWithArgs(t *testing.T) { compilations, _, err := cryticCompile.Compile() assert.NoError(t, err) assert.EqualValues(t, 1, len(compilations)) - assert.EqualValues(t, 1, len(compilations[0].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) // Obtain our chain and senders chain, senders := createChain(t) @@ -346,7 +346,7 @@ func TestChainDeploymentWithArgs(t *testing.T) { // Deploy each contract deployCount := 0 for _, compilation := range compilations { - for _, source := range compilation.Sources { + for _, source := range compilation.SourcePathToArtifact { for contractName, contract := range source.Contracts { contract := contract @@ -467,7 +467,7 @@ func TestChainCloning(t *testing.T) { // Deploy each contract that has no construct arguments 10 times. for _, compilation := range compilations { - for _, source := range compilation.Sources { + for _, source := range compilation.SourcePathToArtifact { for _, contract := range source.Contracts { contract := contract if len(contract.Abi.Constructor.Inputs) == 0 { @@ -563,7 +563,7 @@ func TestChainCallSequenceReplayMatchSimple(t *testing.T) { // Deploy each contract that has no construct arguments 10 times. for _, compilation := range compilations { - for _, source := range compilation.Sources { + for _, source := range compilation.SourcePathToArtifact { for _, contract := range source.Contracts { contract := contract if len(contract.Abi.Constructor.Inputs) == 0 { diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index 377d1a6d..1b74da1c 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -8,6 +8,8 @@ import ( "os" "os/exec" "path/filepath" + "regexp" + "strconv" "strings" "github.com/crytic/medusa/compilation/types" @@ -87,6 +89,19 @@ func (c *CryticCompilationConfig) getArgs() ([]string, error) { return args, nil } +func getSourceUnitID(src string) int { + re := regexp.MustCompile(`[0-9]*:[0-9]*:([0-9]*)`) + sourceUnitCandidates := re.FindStringSubmatch(src) + + if len(sourceUnitCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element + sourceUnit, err := strconv.Atoi(sourceUnitCandidates[1]) + if err == nil { + return sourceUnit + } + } + return -1 +} + // Compile uses the CryticCompilationConfig provided to compile a given target, parse the artifacts, and then // create a list of types.Compilation. func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) { @@ -143,7 +158,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) var compilationList []types.Compilation // Define the structure of our crytic-compile export data. - type solcExportSource struct { + type solcSourceUnit struct { AST any `json:"AST"` } type solcExportContract struct { @@ -154,9 +169,8 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) BinRuntime string `json:"bin-runtime"` } type solcExportData struct { - Sources map[string]solcExportSource `json:"sources"` - Contracts map[string]solcExportContract `json:"contracts"` - SourceList []string `json:"sourceList"` + Sources map[string]solcSourceUnit `json:"sources"` + Contracts map[string]solcExportContract `json:"contracts"` } // Loop through each .json file for compilation units. @@ -176,14 +190,35 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) // Create a compilation object that will store the contracts and source information. compilation := types.NewCompilation() - compilation.SourceList = solcExport.SourceList + + // Create a map of contract names to their kinds + contractKinds := make(map[string]types.ContractKind) // Loop through all sources and parse them into our types. for sourcePath, source := range solcExport.Sources { - compilation.Sources[sourcePath] = types.CompiledSource{ - Ast: source.AST, - Contracts: make(map[string]types.CompiledContract), + + var ast types.AST + b, _ := json.Marshal(source.AST) + err := json.Unmarshal(b, &ast) + if err != nil { + return nil, "", fmt.Errorf("could not parse AST from sources, error: %v", err) + } + // From the AST, extract the contract kinds. + for _, node := range ast.Nodes { + if node.GetNodeType() == "ContractDefinition" { + cdef := node.(types.ContractDefinition) + contractKinds[cdef.CanonicalName] = cdef.ContractKind + } + } + + sourceUnitId := getSourceUnitID(ast.Src) + compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ + // TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any" + Ast: source.AST, + Contracts: make(map[string]types.CompiledContract), + SourceUnitId: sourceUnitId, } + compilation.SourceIdToPath[sourceUnitId] = sourcePath } // Loop through all contracts and parse them into our types. @@ -198,12 +233,12 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) // Ensure a source exists for this, or create one if our path somehow differed from any // path not existing in the "sources" key at the root of the export. - if _, ok := compilation.Sources[sourcePath]; !ok { - parentSource := types.CompiledSource{ + if _, ok := compilation.SourcePathToArtifact[sourcePath]; !ok { + parentSource := types.SourceArtifact{ Ast: nil, Contracts: make(map[string]types.CompiledContract), } - compilation.Sources[sourcePath] = parentSource + compilation.SourcePathToArtifact[sourcePath] = parentSource } // Parse the ABI @@ -223,12 +258,13 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) } // Add contract details - compilation.Sources[sourcePath].Contracts[contractName] = types.CompiledContract{ + compilation.SourcePathToArtifact[sourcePath].Contracts[contractName] = types.CompiledContract{ Abi: *contractAbi, InitBytecode: initBytecode, RuntimeBytecode: runtimeBytecode, SrcMapsInit: contract.SrcMap, SrcMapsRuntime: contract.SrcMapRuntime, + Kind: contractKinds[contractName], } } diff --git a/compilation/platforms/crytic_compile_test.go b/compilation/platforms/crytic_compile_test.go index 3c4ac420..5f0f6e70 100644 --- a/compilation/platforms/crytic_compile_test.go +++ b/compilation/platforms/crytic_compile_test.go @@ -1,22 +1,23 @@ package platforms import ( - "github.com/crytic/medusa/compilation/types" - "github.com/crytic/medusa/utils" - "github.com/crytic/medusa/utils/testutils" - "github.com/stretchr/testify/assert" "os" "os/exec" "path/filepath" "strings" "testing" + + "github.com/crytic/medusa/compilation/types" + "github.com/crytic/medusa/utils" + "github.com/crytic/medusa/utils/testutils" + "github.com/stretchr/testify/assert" ) // testCryticGetCompiledSourceByBaseName checks if a given source file exists in a given compilation's map of sources. // The source file is the file name of a specific file. This function simply checks one of the paths ends with // this name. Avoid including any directories in case the path separators differ per system. // Returns the types.CompiledSource (mapping value) associated to the path if it is found. Returns nil otherwise. -func testCryticGetCompiledSourceByBaseName(sources map[string]types.CompiledSource, name string) *types.CompiledSource { +func testCryticGetCompiledSourceByBaseName(sources map[string]types.SourceArtifact, name string) *types.SourceArtifact { // Obtain a lower case version of our name to search for lowerName := strings.ToLower(name) @@ -53,10 +54,10 @@ func TestCryticSingleFileAbsolutePath(t *testing.T) { // One compilation object assert.EqualValues(t, 1, len(compilations)) // One source because we specified one file - assert.EqualValues(t, 1, len(compilations[0].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) // Two contracts in SimpleContract.sol contractCount := 0 - for _, source := range compilations[0].Sources { + for _, source := range compilations[0].SourcePathToArtifact { contractCount += len(source.Contracts) } assert.EqualValues(t, 2, contractCount) @@ -82,10 +83,10 @@ func TestCryticSingleFileRelativePathSameDirectory(t *testing.T) { // One compilation object assert.EqualValues(t, 1, len(compilations)) // One source because we specified one file - assert.EqualValues(t, 1, len(compilations[0].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) // Two contracts in SimpleContract.sol contractCount := 0 - for _, source := range compilations[0].Sources { + for _, source := range compilations[0].SourcePathToArtifact { contractCount += len(source.Contracts) } assert.EqualValues(t, 2, contractCount) @@ -118,10 +119,10 @@ func TestCryticSingleFileRelativePathChildDirectory(t *testing.T) { // One compilation object assert.EqualValues(t, 1, len(compilations)) // One source because we specified one file - assert.EqualValues(t, 1, len(compilations[0].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) // Two contracts in SimpleContract.sol contractCount := 0 - for _, source := range compilations[0].Sources { + for _, source := range compilations[0].SourcePathToArtifact { contractCount += len(source.Contracts) } assert.EqualValues(t, 2, contractCount) @@ -160,9 +161,9 @@ func TestCryticSingleFileBuildDirectoryArgRelativePath(t *testing.T) { // One compilation object assert.EqualValues(t, 1, len(compilations)) // One source because we specified one file - assert.EqualValues(t, 1, len(compilations[0].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) // Two contracts in SimpleContract.sol. - compiledSource := testCryticGetCompiledSourceByBaseName(compilations[0].Sources, contractName) + compiledSource := testCryticGetCompiledSourceByBaseName(compilations[0].SourcePathToArtifact, contractName) assert.NotNil(t, compiledSource, "source file could not be resolved in compilation sources") assert.EqualValues(t, 2, len(compiledSource.Contracts)) }) @@ -215,11 +216,11 @@ func TestCryticMultipleFiles(t *testing.T) { // Verify there is one compilation object assert.EqualValues(t, 1, len(compilations)) // Verify there are two sources - assert.EqualValues(t, 2, len(compilations[0].Sources)) + assert.EqualValues(t, 2, len(compilations[0].SourcePathToArtifact)) // Verify there are three contracts contractCount := 0 - for _, source := range compilations[0].Sources { + for _, source := range compilations[0].SourcePathToArtifact { contractCount += len(source.Contracts) } assert.EqualValues(t, 3, contractCount) @@ -247,16 +248,16 @@ func TestCryticDirectoryNoArgs(t *testing.T) { // Two compilation objects assert.EqualValues(t, 2, len(compilations)) // One source per compilation unit - assert.EqualValues(t, 1, len(compilations[0].Sources)) - assert.EqualValues(t, 1, len(compilations[1].Sources)) + assert.EqualValues(t, 1, len(compilations[0].SourcePathToArtifact)) + assert.EqualValues(t, 1, len(compilations[1].SourcePathToArtifact)) // Obtain the compiled source from both compilation units firstContractName := "FirstContract.sol" secondContractName := "SecondContract.sol" - firstUnitFirstContractSource := testCryticGetCompiledSourceByBaseName(compilations[0].Sources, firstContractName) - firstUnitSecondContractSource := testCryticGetCompiledSourceByBaseName(compilations[0].Sources, secondContractName) - secondUnitFirstContractSource := testCryticGetCompiledSourceByBaseName(compilations[1].Sources, firstContractName) - secondUnitSecondContractSource := testCryticGetCompiledSourceByBaseName(compilations[1].Sources, secondContractName) + firstUnitFirstContractSource := testCryticGetCompiledSourceByBaseName(compilations[0].SourcePathToArtifact, firstContractName) + firstUnitSecondContractSource := testCryticGetCompiledSourceByBaseName(compilations[0].SourcePathToArtifact, secondContractName) + secondUnitFirstContractSource := testCryticGetCompiledSourceByBaseName(compilations[1].SourcePathToArtifact, firstContractName) + secondUnitSecondContractSource := testCryticGetCompiledSourceByBaseName(compilations[1].SourcePathToArtifact, secondContractName) // Assert that each compilation unit should have two contracts in it. // Compilation unit ordering is non-deterministic in JSON output diff --git a/compilation/platforms/solc.go b/compilation/platforms/solc.go index 283d8611..572553f0 100644 --- a/compilation/platforms/solc.go +++ b/compilation/platforms/solc.go @@ -104,23 +104,14 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { // Create a compilation unit out of this. compilation := types.NewCompilation() - if sourceList, ok := results["sourceList"]; ok { - if sourceListCasted, ok := sourceList.([]any); ok { - compilation.SourceList = make([]string, len(sourceListCasted)) - for i := 0; i < len(sourceListCasted); i++ { - compilation.SourceList[i] = sourceListCasted[i].(string) - } - } else { - return nil, "", fmt.Errorf("could not parse compiled source artifact because 'sourcesList' was not a []string type") - } - } else { - return nil, "", fmt.Errorf("could not parse compiled source artifact because 'sourcesList' did not exist") - } + + // Create a map of contract names to their kinds + contractKinds := make(map[string]types.ContractKind) // Parse our sources from solc output if sources, ok := results["sources"]; ok { if sourcesMap, ok := sources.(map[string]any); ok { - for name, source := range sourcesMap { + for sourcePath, source := range sourcesMap { // Treat our source as a key-value lookup sourceDict, sourceCorrectType := source.(map[string]any) if !sourceCorrectType { @@ -128,16 +119,35 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { } // Try to obtain our AST key - ast, hasAST := sourceDict["AST"] + origAST, hasAST := sourceDict["AST"] if !hasAST { return nil, "", fmt.Errorf("could not parse AST from sources, AST field could not be found") } + var ast types.AST + b, _ := json.Marshal(sourceDict["AST"]) + err := json.Unmarshal(b, &ast) + if err != nil { + return nil, "", fmt.Errorf("could not parse AST from sources, error: %v", err) + } + // From the AST, extract the contract kinds. + for _, node := range ast.Nodes { + if node.GetNodeType() == "ContractDefinition" { + cdef := node.(types.ContractDefinition) + contractKinds[cdef.CanonicalName] = cdef.ContractKind + } + } + + sourceUnitId := getSourceUnitID(ast.Src) // Construct our compiled source object - compilation.Sources[name] = types.CompiledSource{ - Ast: ast, - Contracts: make(map[string]types.CompiledContract), + compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ + // TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any" + Ast: origAST, + Contracts: make(map[string]types.CompiledContract), + SourceUnitId: sourceUnitId, } + compilation.SourceIdToPath[sourceUnitId] = sourcePath + } } } @@ -171,12 +181,13 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { } // Construct our compiled contract - compilation.Sources[sourcePath].Contracts[contractName] = types.CompiledContract{ + compilation.SourcePathToArtifact[sourcePath].Contracts[contractName] = types.CompiledContract{ Abi: *contractAbi, InitBytecode: initBytecode, RuntimeBytecode: runtimeBytecode, SrcMapsInit: contract.Info.SrcMap.(string), SrcMapsRuntime: contract.Info.SrcMapRuntime, + Kind: contractKinds[contractName], } } diff --git a/compilation/types/ast.go b/compilation/types/ast.go new file mode 100644 index 00000000..3f0d2866 --- /dev/null +++ b/compilation/types/ast.go @@ -0,0 +1,98 @@ +package types + +import ( + "encoding/json" + "fmt" +) + +// ContractKind represents the kind of contract +type ContractKind string + +const ( + ContractKindContract ContractKind = "contract" + ContractKindLibrary ContractKind = "library" + ContractKindInterface ContractKind = "interface" +) + +// ContractKindFromString converts a string to a ContractKind +func ContractKindFromString(s string) ContractKind { + switch s { + case "contract": + return ContractKindContract + case "library": + return ContractKindLibrary + case "interface": + return ContractKindInterface + default: + panic(fmt.Sprintf("unknown contract kind: %s", s)) + } +} + +// Node interface represents a generic AST node +type Node interface { + GetNodeType() string +} + +// ContractDefinition is the contract definition node +type ContractDefinition struct { + NodeType string `json:"nodeType"` + CanonicalName string `json:"canonicalName,omitempty"` + ContractKind ContractKind `json:"contractKind,omitempty"` +} + +func (s ContractDefinition) GetNodeType() string { + return s.NodeType +} + +// AST is the abstract syntax tree +type AST struct { + NodeType string `json:"nodeType"` + Nodes []Node `json:"nodes"` + Src string `json:"src"` +} + +// UnmarshalJSON custom unmarshaller for AST +func (a *AST) UnmarshalJSON(data []byte) error { + type Alias AST + aux := &struct { + Nodes []json.RawMessage `json:"nodes"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Check if nodeType is "SourceUnit" + if aux.NodeType != "SourceUnit" { + return nil + } + + for _, nodeData := range aux.Nodes { + var nodeType struct { + NodeType string `json:"nodeType"` + } + + if err := json.Unmarshal(nodeData, &nodeType); err != nil { + return err + } + + var node Node + switch nodeType.NodeType { + case "ContractDefinition": + var cdef ContractDefinition + if err := json.Unmarshal(nodeData, &cdef); err != nil { + return err + } + node = cdef + // Add cases for other node types as needed + default: + continue + } + + a.Nodes = append(a.Nodes, node) + } + + return nil +} diff --git a/compilation/types/compilation.go b/compilation/types/compilation.go index 9958930b..94539f54 100644 --- a/compilation/types/compilation.go +++ b/compilation/types/compilation.go @@ -3,19 +3,16 @@ package types import ( "errors" "fmt" - "golang.org/x/exp/slices" "os" ) // Compilation represents the artifacts of a smart contract compilation. type Compilation struct { - // Sources describes the CompiledSource objects provided in a compilation, housing information regarding source - // files, mappings, ASTs, and contracts. - Sources map[string]CompiledSource + // SourcePathToArtifact maps source file paths to their corresponding SourceArtifact. + SourcePathToArtifact map[string]SourceArtifact - // SourceList describes the CompiledSource keys in Sources, in order. The file identifier used for a SourceMap - // corresponds to an index in this list. - SourceList []string + // SourceIdToPath is a mapping of source unit IDs to source file paths. + SourceIdToPath map[int]string // SourceCode is a lookup of a source file path from SourceList to source code. This is populated by // CacheSourceCode. @@ -26,29 +23,22 @@ type Compilation struct { func NewCompilation() *Compilation { // Create our compilation compilation := &Compilation{ - Sources: make(map[string]CompiledSource), - SourceList: make([]string, 0), - SourceCode: make(map[string][]byte), + SourcePathToArtifact: make(map[string]SourceArtifact), + SourceCode: make(map[string][]byte), + SourceIdToPath: make(map[int]string), } // Return the compilation. return compilation } -// GetSourceFileId obtains the file identifier for a given source file path. This simply checks the index of the -// source file path in SourceList. -// Returns the identifier of the source file, or -1 if it could not be found. -func (c *Compilation) GetSourceFileId(sourcePath string) int { - return slices.Index(c.SourceList, sourcePath) -} - // CacheSourceCode caches source code for each CompiledSource in the compilation in the CompiledSource.SourceCode field. // This method will attempt to populate each CompiledSource.SourceCode which has not yet been populated (is nil) before // returning an error, if one occurs. func (c *Compilation) CacheSourceCode() error { // Loop through each source file, try to read it, and collect errors in an aggregated string if we encounter any. var errStr string - for sourcePath := range c.Sources { + for sourcePath := range c.SourcePathToArtifact { if _, ok := c.SourceCode[sourcePath]; !ok { sourceCodeBytes, sourceReadErr := os.ReadFile(sourcePath) if sourceReadErr != nil { diff --git a/compilation/types/compiled_contract.go b/compilation/types/compiled_contract.go index 8cb9f57a..33693ec8 100644 --- a/compilation/types/compiled_contract.go +++ b/compilation/types/compiled_contract.go @@ -29,6 +29,9 @@ type CompiledContract struct { // SrcMapsRuntime describes the source mappings to associate source file and bytecode segments in RuntimeBytecode. SrcMapsRuntime string + + // Kind describes the kind of contract, i.e. contract, library, interface. + Kind ContractKind } // IsMatch returns a boolean indicating whether provided contract bytecode is a match to this compiled contract diff --git a/compilation/types/compiled_source.go b/compilation/types/compiled_source.go index 9adbd283..2950a74b 100644 --- a/compilation/types/compiled_source.go +++ b/compilation/types/compiled_source.go @@ -1,8 +1,8 @@ package types -// CompiledSource represents a source descriptor for a smart contract compilation, including AST and contained +// SourceArtifact represents a source descriptor for a smart contract compilation, including AST and contained // CompiledContract instances. -type CompiledSource struct { +type SourceArtifact struct { // Ast describes the abstract syntax tree artifact of a source file compilation, providing tokenization of the // source file components. Ast any @@ -10,4 +10,7 @@ type CompiledSource struct { // Contracts describes a mapping of contract names to contract definition structures which are contained within // the source. Contracts map[string]CompiledContract + + // SourceUnitId refers to the identifier of the source unit within the compilation. + SourceUnitId int } diff --git a/compilation/types/source_maps.go b/compilation/types/source_maps.go index c5a92ad0..57da3fb2 100644 --- a/compilation/types/source_maps.go +++ b/compilation/types/source_maps.go @@ -2,9 +2,10 @@ package types import ( "fmt" - "github.com/ethereum/go-ethereum/core/vm" "strconv" "strings" + + "github.com/ethereum/go-ethereum/core/vm" ) // Reference: Source mapping is performed according to the rules specified in solidity documentation: @@ -47,8 +48,8 @@ type SourceMapElement struct { // Length refers to the byte length of the source range the instruction maps to. Length int - // FileID refers to an identifier for the CompiledSource file which houses the relevant source code. - FileID int + // SourceUnitID refers to an identifier for the CompiledSource file which houses the relevant source code. + SourceUnitID int // JumpType refers to the SourceMapJumpType which provides information about any type of jump that occurred. JumpType SourceMapJumpType @@ -83,7 +84,7 @@ func ParseSourceMap(sourceMapStr string) (SourceMap, error) { Index: -1, Offset: -1, Length: -1, - FileID: -1, + SourceUnitID: -1, JumpType: "", ModifierDepth: 0, } @@ -120,7 +121,7 @@ func ParseSourceMap(sourceMapStr string) (SourceMap, error) { // If the source file identifier exists, update our current element data. if len(fields) > 2 && fields[2] != "" { - current.FileID, err = strconv.Atoi(fields[2]) + current.SourceUnitID, err = strconv.Atoi(fields[2]) if err != nil { return nil, err } diff --git a/fuzzing/coverage/source_analysis.go b/fuzzing/coverage/source_analysis.go index baed6505..49b8aafa 100644 --- a/fuzzing/coverage/source_analysis.go +++ b/fuzzing/coverage/source_analysis.go @@ -3,9 +3,10 @@ package coverage import ( "bytes" "fmt" + "sort" + "github.com/crytic/medusa/compilation/types" "golang.org/x/exp/maps" - "sort" ) // SourceAnalysis describes source code coverage across a list of compilations, after analyzing associated CoverageMaps. @@ -117,7 +118,7 @@ func AnalyzeSourceCoverage(compilations []types.Compilation, coverageMaps *Cover // Loop through all sources in all compilations to add them to our source file analysis container. for _, compilation := range compilations { - for sourcePath := range compilation.Sources { + for sourcePath := range compilation.SourcePathToArtifact { // If we have no source code loaded for this source, skip it. if _, ok := compilation.SourceCode[sourcePath]; !ok { return nil, fmt.Errorf("could not perform source code analysis, code was not cached for '%v'", sourcePath) @@ -135,9 +136,13 @@ func AnalyzeSourceCoverage(compilations []types.Compilation, coverageMaps *Cover // Loop through all sources in all compilations to process coverage information. for _, compilation := range compilations { - for _, source := range compilation.Sources { + for _, source := range compilation.SourcePathToArtifact { // Loop for each contract in this source for _, contract := range source.Contracts { + // Skip interfaces. + if contract.Kind == types.ContractKindInterface { + continue + } // Obtain coverage map data for this contract. initCoverageMapData, err := coverageMaps.GetContractCoverageMap(contract.InitBytecode, true) if err != nil { @@ -196,20 +201,19 @@ func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis for _, sourceMapElement := range sourceMap { // If this source map element doesn't map to any file (compiler generated inline code), it will have no // relevance to the coverage map, so we skip it. - if sourceMapElement.FileID == -1 { + if sourceMapElement.SourceUnitID == -1 { continue } - // Verify this file ID is not out of bounds for a source file index - if sourceMapElement.FileID < 0 || sourceMapElement.FileID >= len(compilation.SourceList) { - // TODO: We may also go out of bounds because this maps to a "generated source" which we do not have. - // For now, we silently skip these cases. + // Obtain our source for this file ID + sourcePath, idExists := compilation.SourceIdToPath[sourceMapElement.SourceUnitID] + + // TODO: We may also go out of bounds because this maps to a "generated source" which we do not have. + // For now, we silently skip these cases. + if !idExists { continue } - // Obtain our source for this file ID - sourcePath := compilation.SourceList[sourceMapElement.FileID] - // Check if the source map element was executed. sourceMapElementCovered := false sourceMapElementCoveredReverted := false @@ -258,7 +262,7 @@ func filterSourceMaps(compilation types.Compilation, sourceMap types.SourceMap) // Loop for each source map entry and determine if it should be included. for i, sourceMapElement := range sourceMap { // Verify this file ID is not out of bounds for a source file index - if sourceMapElement.FileID < 0 || sourceMapElement.FileID >= len(compilation.SourceList) { + if _, exists := compilation.SourceIdToPath[sourceMapElement.SourceUnitID]; !exists { // TODO: We may also go out of bounds because this maps to a "generated source" which we do not have. // For now, we silently skip these cases. continue @@ -267,7 +271,7 @@ func filterSourceMaps(compilation types.Compilation, sourceMap types.SourceMap) // Verify this source map does not overlap another encapsulatesOtherMapping := false for x, sourceMapElement2 := range sourceMap { - if i != x && sourceMapElement.FileID == sourceMapElement2.FileID && + if i != x && sourceMapElement.SourceUnitID == sourceMapElement2.SourceUnitID && !(sourceMapElement.Offset == sourceMapElement2.Offset && sourceMapElement.Length == sourceMapElement2.Length) { if sourceMapElement2.Offset >= sourceMapElement.Offset && sourceMapElement2.Offset+sourceMapElement2.Length <= sourceMapElement.Offset+sourceMapElement.Length { From 3e315a14c337001350c7f2f26fcc8e353cdf3b37 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Sat, 27 Jul 2024 23:20:15 -0500 Subject: [PATCH 086/109] skip library and interface in target contract lookup --- fuzzing/fuzzer.go | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index f1171cc2..c28ac9d8 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -291,13 +291,19 @@ func (f *Fuzzer) AddCompilationTargets(compilations []compilationTypes.Compilati compilation := &f.compilations[len(f.compilations)-1] // Loop for each source - for sourcePath, source := range compilation.Sources { + for sourcePath, source := range compilation.SourcePathToArtifact { // Seed our base value set from every source's AST f.baseValueSet.SeedFromAst(source.Ast) // Loop for every contract and register it in our contract definitions for contractName := range source.Contracts { contract := source.Contracts[contractName] + + // Skip interfaces. + if contract.Kind == compilationTypes.ContractKindInterface { + continue + } + contractDefinition := fuzzerTypes.NewContract(contractName, sourcePath, &contract, compilation) // Sort available methods by type @@ -395,11 +401,17 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex // Verify that target contracts is not empty. If it's empty, but we only have one contract definition, // we can infer the target contracts. Otherwise, we report an error. if len(fuzzer.config.Fuzzing.TargetContracts) == 0 { - // TODO filter libraries - if len(fuzzer.contractDefinitions) == 1 { - fuzzer.config.Fuzzing.TargetContracts = []string{fuzzer.contractDefinitions[0].Name()} - } else { - return nil, fmt.Errorf("missing target contracts") + var found bool + for _, contract := range fuzzer.contractDefinitions { + // If only one contract is defined, we can infer the target contract by filtering interfaces/libraries. + if contract.CompiledContract().Kind == compilationTypes.ContractKindContract { + if !found { + fuzzer.config.Fuzzing.TargetContracts = []string{contract.Name()} + found = true + } else { + return nil, fmt.Errorf("specify target contract(s)") + } + } } } @@ -816,7 +828,11 @@ func (f *Fuzzer) Start() error { if err == nil && f.config.Fuzzing.CorpusDirectory != "" { coverageReportPath := filepath.Join(f.config.Fuzzing.CorpusDirectory, "coverage_report.html") err = coverage.GenerateReport(f.compilations, f.corpus.CoverageMaps(), coverageReportPath) - f.logger.Info("Coverage report saved to file: ", colors.Bold, coverageReportPath, colors.Reset) + if err != nil { + f.logger.Error("Failed to generate coverage report", err) + } else { + f.logger.Info("Coverage report saved to file: ", colors.Bold, coverageReportPath, colors.Reset) + } } // Return any encountered error. From 2dcc0f9fe9eac2eb4ada34a812e9ab067605d45d Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Wed, 31 Jul 2024 14:36:29 -0400 Subject: [PATCH 087/109] improve commenting and error handling --- compilation/platforms/crytic_compile.go | 14 ++++++++++---- compilation/types/ast.go | 24 ++++++------------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index 1b74da1c..d43a6d12 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -196,13 +196,19 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) // Loop through all sources and parse them into our types. for sourcePath, source := range solcExport.Sources { - + // Convert the AST into our version of the AST (types.AST) var ast types.AST - b, _ := json.Marshal(source.AST) - err := json.Unmarshal(b, &ast) + b, err = json.Marshal(source.AST) + if err != nil { + + return nil, "", fmt.Errorf("could not encode AST from sources: %v", err) + } + err = json.Unmarshal(b, &ast) if err != nil { - return nil, "", fmt.Errorf("could not parse AST from sources, error: %v", err) + + return nil, "", fmt.Errorf("could not parse AST from sources: %v", err) } + // From the AST, extract the contract kinds. for _, node := range ast.Nodes { if node.GetNodeType() == "ContractDefinition" { diff --git a/compilation/types/ast.go b/compilation/types/ast.go index 3f0d2866..fac97f51 100644 --- a/compilation/types/ast.go +++ b/compilation/types/ast.go @@ -2,32 +2,20 @@ package types import ( "encoding/json" - "fmt" ) -// ContractKind represents the kind of contract +// ContractKind represents the kind of contract represented by an AST node type ContractKind string const ( - ContractKindContract ContractKind = "contract" - ContractKindLibrary ContractKind = "library" + // ContractKindContract represents a contract node + ContractKindContract ContractKind = "contract" + // ContractKindLibrary represents a library node + ContractKindLibrary ContractKind = "library" + // ContractKindInterface represents an interface node ContractKindInterface ContractKind = "interface" ) -// ContractKindFromString converts a string to a ContractKind -func ContractKindFromString(s string) ContractKind { - switch s { - case "contract": - return ContractKindContract - case "library": - return ContractKindLibrary - case "interface": - return ContractKindInterface - default: - panic(fmt.Sprintf("unknown contract kind: %s", s)) - } -} - // Node interface represents a generic AST node type Node interface { GetNodeType() string From 2328a683c98d3e0b35d186d4629a64eadf398fb8 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Wed, 31 Jul 2024 15:24:45 -0400 Subject: [PATCH 088/109] added more comments --- compilation/platforms/crytic_compile.go | 9 +++---- compilation/types/ast.go | 31 ++++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index d43a6d12..4ee08d1a 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -209,17 +209,18 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) return nil, "", fmt.Errorf("could not parse AST from sources: %v", err) } - // From the AST, extract the contract kinds. + // From the AST, extract the contract kinds where the contract definition could be for a contract, library, + // or interface for _, node := range ast.Nodes { if node.GetNodeType() == "ContractDefinition" { - cdef := node.(types.ContractDefinition) - contractKinds[cdef.CanonicalName] = cdef.ContractKind + contractDefinition := node.(types.ContractDefinition) + contractKinds[contractDefinition.CanonicalName] = contractDefinition.Kind } } sourceUnitId := getSourceUnitID(ast.Src) compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ - // TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any" + // TODO: Our types.AST is not the same as the original AST but we could parse it and avoid using "any" Ast: source.AST, Contracts: make(map[string]types.CompiledContract), SourceUnitId: sourceUnitId, diff --git a/compilation/types/ast.go b/compilation/types/ast.go index fac97f51..6b495312 100644 --- a/compilation/types/ast.go +++ b/compilation/types/ast.go @@ -4,7 +4,7 @@ import ( "encoding/json" ) -// ContractKind represents the kind of contract represented by an AST node +// ContractKind represents the kind of contract definition represented by an AST node type ContractKind string const ( @@ -23,11 +23,15 @@ type Node interface { // ContractDefinition is the contract definition node type ContractDefinition struct { - NodeType string `json:"nodeType"` - CanonicalName string `json:"canonicalName,omitempty"` - ContractKind ContractKind `json:"contractKind,omitempty"` + // NodeType represents the AST node type (note that it will always be a contract definition) + NodeType string `json:"nodeType"` + // CanonicalName is the name of the contract definition + CanonicalName string `json:"canonicalName,omitempty"` + // Kind is a ContractKind that represents what type of contract definition this is (contract, interface, or library) + Kind ContractKind `json:"contractKind,omitempty"` } +// GetNodeType implements the Node interface and returns the node type for the contract definition func (s ContractDefinition) GetNodeType() string { return s.NodeType } @@ -39,8 +43,9 @@ type AST struct { Src string `json:"src"` } -// UnmarshalJSON custom unmarshaller for AST +// UnmarshalJSON unmarshals from JSON func (a *AST) UnmarshalJSON(data []byte) error { + // Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later type Alias AST aux := &struct { Nodes []json.RawMessage `json:"nodes"` @@ -52,33 +57,37 @@ func (a *AST) UnmarshalJSON(data []byte) error { return err } - // Check if nodeType is "SourceUnit" + // Check if nodeType is "SourceUnit". Return early otherwise if aux.NodeType != "SourceUnit" { return nil } + // Iterate through all the nodes of the source unit for _, nodeData := range aux.Nodes { + // Unmarshal the node data to retrieve the node type var nodeType struct { NodeType string `json:"nodeType"` } - if err := json.Unmarshal(nodeData, &nodeType); err != nil { return err } + // Unmarshal the contents of the node based on the node type var node Node switch nodeType.NodeType { case "ContractDefinition": - var cdef ContractDefinition - if err := json.Unmarshal(nodeData, &cdef); err != nil { + // If this is a contract definition, unmarshal it + var contractDefinition ContractDefinition + if err := json.Unmarshal(nodeData, &contractDefinition); err != nil { return err } - node = cdef - // Add cases for other node types as needed + node = contractDefinition + // TODO: Add cases for other node types as needed default: continue } + // Append the node a.Nodes = append(a.Nodes, node) } From 4eb55b896723973c30080f7bc934d5c33076ea06 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Wed, 31 Jul 2024 15:42:03 -0400 Subject: [PATCH 089/109] migrate getSourceUnitID function to AST class --- compilation/platforms/crytic_compile.go | 20 ++------------------ compilation/platforms/solc.go | 19 +++++++++++++------ compilation/types/ast.go | 23 +++++++++++++++++++++-- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index 4ee08d1a..a9e07fae 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -8,8 +8,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" - "strconv" "strings" "github.com/crytic/medusa/compilation/types" @@ -89,19 +87,6 @@ func (c *CryticCompilationConfig) getArgs() ([]string, error) { return args, nil } -func getSourceUnitID(src string) int { - re := regexp.MustCompile(`[0-9]*:[0-9]*:([0-9]*)`) - sourceUnitCandidates := re.FindStringSubmatch(src) - - if len(sourceUnitCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element - sourceUnit, err := strconv.Atoi(sourceUnitCandidates[1]) - if err == nil { - return sourceUnit - } - } - return -1 -} - // Compile uses the CryticCompilationConfig provided to compile a given target, parse the artifacts, and then // create a list of types.Compilation. func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) { @@ -200,12 +185,10 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) var ast types.AST b, err = json.Marshal(source.AST) if err != nil { - return nil, "", fmt.Errorf("could not encode AST from sources: %v", err) } err = json.Unmarshal(b, &ast) if err != nil { - return nil, "", fmt.Errorf("could not parse AST from sources: %v", err) } @@ -218,7 +201,8 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) } } - sourceUnitId := getSourceUnitID(ast.Src) + // Retrieve the source unit ID + sourceUnitId := ast.GetSourceUnitID() compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ // TODO: Our types.AST is not the same as the original AST but we could parse it and avoid using "any" Ast: source.AST, diff --git a/compilation/platforms/solc.go b/compilation/platforms/solc.go index 572553f0..4ceef747 100644 --- a/compilation/platforms/solc.go +++ b/compilation/platforms/solc.go @@ -124,21 +124,28 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { return nil, "", fmt.Errorf("could not parse AST from sources, AST field could not be found") } + // Convert the AST into our version of the AST (types.AST) var ast types.AST - b, _ := json.Marshal(sourceDict["AST"]) - err := json.Unmarshal(b, &ast) + b, err := json.Marshal(origAST) + if err != nil { + return nil, "", fmt.Errorf("could not encode AST from sources: %v", err) + } + err = json.Unmarshal(b, &ast) if err != nil { return nil, "", fmt.Errorf("could not parse AST from sources, error: %v", err) } - // From the AST, extract the contract kinds. + + // From the AST, extract the contract kinds where the contract definition could be for a contract, library, + // or interface for _, node := range ast.Nodes { if node.GetNodeType() == "ContractDefinition" { - cdef := node.(types.ContractDefinition) - contractKinds[cdef.CanonicalName] = cdef.ContractKind + contractDefinition := node.(types.ContractDefinition) + contractKinds[contractDefinition.CanonicalName] = contractDefinition.Kind } } - sourceUnitId := getSourceUnitID(ast.Src) + // Get the source unit ID + sourceUnitId := ast.GetSourceUnitID() // Construct our compiled source object compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ // TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any" diff --git a/compilation/types/ast.go b/compilation/types/ast.go index 6b495312..f6b21612 100644 --- a/compilation/types/ast.go +++ b/compilation/types/ast.go @@ -2,6 +2,8 @@ package types import ( "encoding/json" + "regexp" + "strconv" ) // ContractKind represents the kind of contract definition represented by an AST node @@ -38,9 +40,12 @@ func (s ContractDefinition) GetNodeType() string { // AST is the abstract syntax tree type AST struct { + // NodeType represents the node type (currently we only evaluate source unit node types) NodeType string `json:"nodeType"` - Nodes []Node `json:"nodes"` - Src string `json:"src"` + // Nodes is a list of Nodes within the AST + Nodes []Node `json:"nodes"` + // Src is the source file for this AST + Src string `json:"src"` } // UnmarshalJSON unmarshals from JSON @@ -93,3 +98,17 @@ func (a *AST) UnmarshalJSON(data []byte) error { return nil } + +// GetSourceUnitID returns the source unit ID based on the source of the AST +func (a *AST) GetSourceUnitID() int { + re := regexp.MustCompile(`[0-9]*:[0-9]*:([0-9]*)`) + sourceUnitCandidates := re.FindStringSubmatch(a.Src) + + if len(sourceUnitCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element + sourceUnit, err := strconv.Atoi(sourceUnitCandidates[1]) + if err == nil { + return sourceUnit + } + } + return -1 +} From f4d710f254581f9898bf2659b7980364163eb914 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Fri, 2 Aug 2024 12:31:20 -0400 Subject: [PATCH 090/109] fix comments --- fuzzing/fuzzer_metrics.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fuzzing/fuzzer_metrics.go b/fuzzing/fuzzer_metrics.go index b0984ab0..ef495a80 100644 --- a/fuzzing/fuzzer_metrics.go +++ b/fuzzing/fuzzer_metrics.go @@ -14,7 +14,7 @@ type fuzzerWorkerMetrics struct { // sequencesTested describes the amount of sequences of transactions which tests were run against. sequencesTested *big.Int - //failedSequences describes the amount of sequences of transactions which tests failed. + // failedSequences describes the amount of sequences of transactions which tests failed. failedSequences *big.Int // callsTested describes the amount of transactions/calls the fuzzer executed and ran tests against. @@ -43,6 +43,7 @@ func newFuzzerMetrics(workerCount int) *FuzzerMetrics { return &metrics } +// FailedSequences returns the number of sequences that led to failures across all workers func (m *FuzzerMetrics) FailedSequences() *big.Int { failedSequences := big.NewInt(0) for _, workerMetrics := range m.workerMetrics { From 7e41898a3eb4b91e8ca274dc38f961e3f66f9e76 Mon Sep 17 00:00:00 2001 From: Anish Naik Date: Fri, 2 Aug 2024 13:48:32 -0400 Subject: [PATCH 091/109] update version --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 7f7ec9ad..c08518b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.5" +const version = "0.1.6" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From b81a9bc7a09baa8f699241fa959e466849b9b453 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 15 Aug 2024 12:34:42 -0500 Subject: [PATCH 092/109] feat: prompt user for overwrite if file already exists (#444) * feat: prompt user for overwrite if file already exists Fixes #375 --------- Co-authored-by: to4to --- cmd/init.go | 20 +++++++++++++++++++- go.mod | 1 + go.sum | 4 ++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index 4f845517..35cb918b 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -2,11 +2,12 @@ package cmd import ( "fmt" - "github.com/crytic/medusa/logging/colors" "os" "path/filepath" "strings" + "github.com/crytic/medusa/logging/colors" + "github.com/crytic/medusa/compilation" "github.com/crytic/medusa/fuzzing/config" "github.com/spf13/cobra" @@ -136,6 +137,23 @@ func cmdRunInit(cmd *cobra.Command, args []string) error { return err } + if _, err = os.Stat(outputPath); err == nil { + // Prompt user for overwrite confirmation + fmt.Print("The file already exists. Overwrite? (y/n): ") + var response string + if _, err := fmt.Scan(&response); err != nil { + // Handle the error (e.g., log it, return an error) + cmdLogger.Error("Failed to scan input", err) + return err + } + + if response != "y" && response != "Y" { + fmt.Println("Operation canceled.") + return nil + } + + } + // Write our project configuration err = projectConfig.WriteToFile(outputPath) if err != nil { diff --git a/go.mod b/go.mod index 05b14e8a..f5a85d40 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/onsi/gomega v1.33.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/go.sum b/go.sum index d20fe464..d01119b9 100644 --- a/go.sum +++ b/go.sum @@ -150,8 +150,9 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -262,7 +263,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= From e3458cb8f79ca4c2926c32acfbe93deb1b4c3d7a Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 22 Aug 2024 12:49:21 -0500 Subject: [PATCH 093/109] feat: display success/revert hit count in coverage report (#364) * feat: display success/revert hit count in coverage report * fix: do not add to corpus if already hit * update test to disregard hit count * fix: check test limit in each worker * Revert "fix: check test limit in each worker" This reverts commit c93b5579567b8f4882dca113110f95f8003d4a70. * remove copy function * add minor todo * update coverage maps API * simplify equality check * fix coverage map API and bug in map updates * fix hit count in report bug --------- Co-authored-by: Anish Naik --- fuzzing/coverage/coverage_maps.go | 58 ++++++++++++++----------- fuzzing/coverage/coverage_tracer.go | 2 +- fuzzing/coverage/report_template.gohtml | 4 +- fuzzing/coverage/source_analysis.go | 24 ++++++---- fuzzing/fuzzer_test.go | 12 ++++- 5 files changed, 62 insertions(+), 38 deletions(-) diff --git a/fuzzing/coverage/coverage_maps.go b/fuzzing/coverage/coverage_maps.go index 1e8a48d6..06059ab7 100644 --- a/fuzzing/coverage/coverage_maps.go +++ b/fuzzing/coverage/coverage_maps.go @@ -1,12 +1,13 @@ package coverage import ( - "bytes" + "golang.org/x/exp/slices" + "sync" + compilationTypes "github.com/crytic/medusa/compilation/types" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - "sync" ) // CoverageMaps represents a data structure used to identify instruction execution coverage of various smart contracts @@ -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 @@ -210,7 +211,7 @@ 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 } @@ -285,18 +286,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. @@ -310,27 +311,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 @@ -347,28 +350,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 } diff --git a/fuzzing/coverage/coverage_tracer.go b/fuzzing/coverage/coverage_tracer.go index a495c20a..2f1a47e9 100644 --- a/fuzzing/coverage/coverage_tracer.go +++ b/fuzzing/coverage/coverage_tracer.go @@ -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) } diff --git a/fuzzing/coverage/report_template.gohtml b/fuzzing/coverage/report_template.gohtml index d7993e81..edcd4986 100644 --- a/fuzzing/coverage/report_template.gohtml +++ b/fuzzing/coverage/report_template.gohtml @@ -190,12 +190,12 @@ {{/* Output two cells for the reverted/non-reverted execution status */}} {{if $line.IsCovered}} -

+
√ {{$line.SuccessHitCount}}
{{end}} {{if $line.IsCoveredReverted}} -
+
⟳ {{$line.RevertHitCount}}
{{end}} diff --git a/fuzzing/coverage/source_analysis.go b/fuzzing/coverage/source_analysis.go index 49b8aafa..7705d2c1 100644 --- a/fuzzing/coverage/source_analysis.go +++ b/fuzzing/coverage/source_analysis.go @@ -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 } @@ -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. @@ -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. diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 0af56dd9..5f56ca4c 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -839,8 +839,16 @@ func TestCorpusReplayability(t *testing.T) { assertCorpusCallSequencesCollected(f, true) newCoverage := f.fuzzer.corpus.CoverageMaps() - // Check to see if original and new coverage are the same. - assert.True(t, originalCoverage.Equal(newCoverage)) + // Check to see if original and new coverage are the same (disregarding hit count) + successCovIncreased, revertCovIncreased, err := originalCoverage.Update(newCoverage) + assert.False(t, successCovIncreased) + assert.False(t, revertCovIncreased) + assert.NoError(t, err) + + successCovIncreased, revertCovIncreased, err = newCoverage.Update(originalCoverage) + assert.False(t, successCovIncreased) + assert.False(t, revertCovIncreased) + assert.NoError(t, err) // Verify that the fuzzer finished after fewer sequences than there are in the corpus assert.LessOrEqual(t, f.fuzzer.metrics.SequencesTested().Uint64(), uint64(originalCorpusSequenceCount)) From ff587c3d49001f0399a32fdba36b131d5a59a334 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 22 Aug 2024 17:01:08 -0500 Subject: [PATCH 094/109] Log/unique pcs (#453) * chore: add and rearrange how/what is logged * log unique pc's * inline comment improvements * minor changes * improve inline documentation * improve rounding * use setUint64 * fix error --------- Co-authored-by: Anish Naik --- compilation/platforms/crytic_compile.go | 2 ++ fuzzing/coverage/coverage_maps.go | 36 +++++++++++++++++++++++-- fuzzing/fuzzer.go | 24 ++++++++++++----- fuzzing/fuzzer_metrics.go | 20 +++++++++++--- fuzzing/fuzzer_worker.go | 4 +++ 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index a9e07fae..6039df26 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/crytic/medusa/compilation/types" + "github.com/crytic/medusa/logging" "github.com/crytic/medusa/utils" ) @@ -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 != "" { diff --git a/fuzzing/coverage/coverage_maps.go b/fuzzing/coverage/coverage_maps.go index 06059ab7..59237669 100644 --- a/fuzzing/coverage/coverage_maps.go +++ b/fuzzing/coverage/coverage_maps.go @@ -2,12 +2,12 @@ package coverage import ( "golang.org/x/exp/slices" - "sync" compilationTypes "github.com/crytic/medusa/compilation/types" "github.com/crytic/medusa/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "sync" ) // CoverageMaps represents a data structure used to identify instruction execution coverage of various smart contracts @@ -212,6 +212,7 @@ func (cm *CoverageMaps) UpdateAt(codeAddress common.Address, codeLookupHash comm // Set our coverage in the map and return our change state changedInMap, err = coverageMap.updateCoveredAt(codeSize, pc) + return addedNewMap || changedInMap, err } @@ -243,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 @@ -268,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 diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index c28ac9d8..4fcc00d5 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -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) @@ -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 @@ -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 { @@ -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 @@ -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() @@ -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) } @@ -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 diff --git a/fuzzing/fuzzer_metrics.go b/fuzzing/fuzzer_metrics.go index ef495a80..a1ef8f11 100644 --- a/fuzzing/fuzzer_metrics.go +++ b/fuzzing/fuzzer_metrics.go @@ -11,16 +11,19 @@ type FuzzerMetrics struct { // fuzzerWorkerMetrics represents metrics for a single FuzzerWorker instance. type fuzzerWorkerMetrics struct { - // sequencesTested describes the amount of sequences of transactions which tests were run against. + // sequencesTested is the amount of sequences of transactions which tests were run against. sequencesTested *big.Int - // failedSequences describes the amount of sequences of transactions which tests failed. + // failedSequences is the amount of sequences of transactions which tests failed. failedSequences *big.Int - // callsTested describes the amount of transactions/calls the fuzzer executed and ran tests against. + // callsTested is the amount of transactions/calls the fuzzer executed and ran tests against. callsTested *big.Int - // workerStartupCount describes the amount of times the worker was generated, or re-generated for this index. + // gasUsed is the amount of gas the fuzzer executed and ran tests against. + gasUsed *big.Int + + // workerStartupCount is the amount of times the worker was generated, or re-generated for this index. workerStartupCount *big.Int // shrinking indicates whether the fuzzer worker is currently shrinking. @@ -39,6 +42,7 @@ func newFuzzerMetrics(workerCount int) *FuzzerMetrics { metrics.workerMetrics[i].failedSequences = big.NewInt(0) metrics.workerMetrics[i].callsTested = big.NewInt(0) metrics.workerMetrics[i].workerStartupCount = big.NewInt(0) + metrics.workerMetrics[i].gasUsed = big.NewInt(0) } return &metrics } @@ -70,6 +74,14 @@ func (m *FuzzerMetrics) CallsTested() *big.Int { return transactionsTested } +func (m *FuzzerMetrics) GasUsed() *big.Int { + gasUsed := big.NewInt(0) + for _, workerMetrics := range m.workerMetrics { + gasUsed.Add(gasUsed, workerMetrics.gasUsed) + } + return gasUsed +} + // WorkerStartupCount describes the amount of times the worker was spawned for this index. Workers are periodically // reset. func (m *FuzzerMetrics) WorkerStartupCount() *big.Int { diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index b5f2db80..dcac1d0f 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -301,6 +301,8 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall // Update our metrics fw.workerMetrics().callsTested.Add(fw.workerMetrics().callsTested, big.NewInt(1)) + lastCallSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1] + fw.workerMetrics().gasUsed.Add(fw.workerMetrics().gasUsed, new(big.Int).SetUint64(lastCallSequenceElement.ChainReference.Block.MessageResults[lastCallSequenceElement.ChainReference.TransactionIndex].Receipt.GasUsed)) // If our fuzzer context is done, exit out immediately without results. if utils.CheckContextDone(fw.fuzzer.ctx) { @@ -425,6 +427,8 @@ func (fw *FuzzerWorker) shrinkCallSequence(callSequence calls.CallSequence, shri // 2) Add block/time delay to previous call (retain original block/time, possibly exceed max delays) // At worst, this costs `2 * len(callSequence)` shrink iterations. fw.workerMetrics().shrinking = true + fw.fuzzer.logger.Info(fmt.Sprintf("[Worker %d] Shrinking call sequence with %d call(s)", fw.workerIndex, len(callSequence))) + for removalStrategy := 0; removalStrategy < 2 && !shrinkingEnded(); removalStrategy++ { for i := len(optimizedSequence) - 1; i >= 0 && !shrinkingEnded(); i-- { // Recreate our current optimized sequence without the item at this index From 3beda5cc80b2b79ceeeafe2583899fa2d72a6613 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 27 Aug 2024 15:42:58 -0400 Subject: [PATCH 095/109] fix: check that receipt is non-nil in `OnTxEnd` hook (#457) * throw panic if execution tracing fails * do not store trace in case of error --- fuzzing/executiontracer/execution_tracer.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fuzzing/executiontracer/execution_tracer.go b/fuzzing/executiontracer/execution_tracer.go index 46bfb01a..0b876c0c 100644 --- a/fuzzing/executiontracer/execution_tracer.go +++ b/fuzzing/executiontracer/execution_tracer.go @@ -110,6 +110,12 @@ func (t *ExecutionTracer) GetTrace(txHash common.Hash) *ExecutionTrace { // OnTxEnd is called upon the end of transaction execution, as defined by tracers.Tracer. func (t *ExecutionTracer) OnTxEnd(receipt *coretypes.Receipt, err error) { + // We avoid storing the trace for this transaction. An error should realistically only occur if we hit a block gas + // limit error. In this case, the transaction will be retried in the next block and we can retrieve the trace at + // that time. + if err != nil || receipt == nil { + return + } t.traceMap[receipt.TxHash] = t.trace } From 9bb8cb2e657b288fb67eb24f2082aadbeb1f8d97 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 27 Aug 2024 14:57:05 -0500 Subject: [PATCH 096/109] fix: incorrect target contract balance when used with predeploys (#461) --- fuzzing/fuzzer.go | 8 ++++++-- fuzzing/fuzzer_test.go | 1 + .../testdata/contracts/deployments/predeploy_contract.sol | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 4fcc00d5..5234ac1d 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -419,10 +419,14 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex // Ordering is important here (predeploys _then_ targets) so that you can have the same contract in both lists // while still being able to use the contract address overrides contractsToDeploy := make([]string, 0) + balances := make([]*big.Int, 0) for contractName := range fuzzer.config.Fuzzing.PredeployedContracts { contractsToDeploy = append(contractsToDeploy, contractName) + // Preserve index of target contract balances + balances = append(balances, big.NewInt(0)) } contractsToDeploy = append(contractsToDeploy, fuzzer.config.Fuzzing.TargetContracts...) + balances = append(balances, fuzzer.config.Fuzzing.TargetContractsBalances...) deployedContractAddr := make(map[string]common.Address) // Loop for all contracts to deploy @@ -460,8 +464,8 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex // If our project config has a non-zero balance for this target contract, retrieve it contractBalance := big.NewInt(0) - if len(fuzzer.config.Fuzzing.TargetContractsBalances) > i { - contractBalance = new(big.Int).Set(fuzzer.config.Fuzzing.TargetContractsBalances[i]) + if len(balances) > i { + contractBalance = new(big.Int).Set(balances[i]) } // Create a message to represent our contract deployment (we let deployments consume the whole block diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 5f56ca4c..47450b63 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -403,6 +403,7 @@ func TestDeploymentsWithPredeploy(t *testing.T) { filePath: "testdata/contracts/deployments/predeploy_contract.sol", configUpdates: func(config *config.ProjectConfig) { config.Fuzzing.TargetContracts = []string{"TestContract"} + config.Fuzzing.TargetContractsBalances = []*big.Int{big.NewInt(1)} config.Fuzzing.TestLimit = 1000 // this test should expose a failure immediately config.Fuzzing.Testing.PropertyTesting.Enabled = false config.Fuzzing.Testing.OptimizationTesting.Enabled = false diff --git a/fuzzing/testdata/contracts/deployments/predeploy_contract.sol b/fuzzing/testdata/contracts/deployments/predeploy_contract.sol index 12ceef8f..bca678e4 100644 --- a/fuzzing/testdata/contracts/deployments/predeploy_contract.sol +++ b/fuzzing/testdata/contracts/deployments/predeploy_contract.sol @@ -7,6 +7,8 @@ contract PredeployContract { contract TestContract { PredeployContract predeploy = PredeployContract(address(0x1234)); + constructor() payable {} + function testPredeploy() public { predeploy.triggerFailure(); } From a6d14ca773f265fe22cf262b94d1e5ee007af59f Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 27 Aug 2024 16:42:41 -0400 Subject: [PATCH 097/109] feat: combine mutable and immutable call sequences folder (#456) * remove immutable call sequences folder and references to it * support for legacy corpus migration * fix error handling --- fuzzing/corpus/corpus.go | 155 ++++++++++++++++++---------- fuzzing/corpus/corpus_test.go | 14 +-- fuzzing/fuzzer.go | 4 +- fuzzing/fuzzer_test.go | 3 +- fuzzing/fuzzer_test_methods_test.go | 2 +- utils/fs_utils.go | 29 ++++++ 6 files changed, 140 insertions(+), 67 deletions(-) diff --git a/fuzzing/corpus/corpus.go b/fuzzing/corpus/corpus.go index 8426f156..8a640298 100644 --- a/fuzzing/corpus/corpus.go +++ b/fuzzing/corpus/corpus.go @@ -4,10 +4,13 @@ import ( "bytes" "fmt" "math/big" + "os" "path/filepath" "sync" "time" + "github.com/crytic/medusa/utils" + "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/coverage" @@ -30,13 +33,8 @@ type Corpus struct { // coverageMaps describes the total code coverage known to be achieved across all corpus call sequences. coverageMaps *coverage.CoverageMaps - // mutableSequenceFiles represents a corpus directory with files which describe call sequences that should - // be used for mutations. - mutableSequenceFiles *corpusDirectory[calls.CallSequence] - - // immutableSequenceFiles represents a corpus directory with files which describe call sequences that should not be - // used for mutations. - immutableSequenceFiles *corpusDirectory[calls.CallSequence] + // callSequenceFiles represents a corpus directory with files that should be used for mutations. + callSequenceFiles *corpusDirectory[calls.CallSequence] // testResultSequenceFiles represents a corpus directory with files which describe call sequences that were flagged // to be saved by a test case provider. These are not used in mutations. @@ -66,8 +64,7 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) { corpus := &Corpus{ storageDirectory: corpusDirectory, coverageMaps: coverage.NewCoverageMaps(), - mutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""), - immutableSequenceFiles: newCorpusDirectory[calls.CallSequence](""), + callSequenceFiles: newCorpusDirectory[calls.CallSequence](""), testResultSequenceFiles: newCorpusDirectory[calls.CallSequence](""), unexecutedCallSequences: make([]calls.CallSequence, 0), logger: logging.GlobalLogger.NewSubLogger("module", "corpus"), @@ -75,16 +72,17 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) { // If we have a corpus directory set, parse our call sequences. if corpus.storageDirectory != "" { - // Read mutable call sequences. - corpus.mutableSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences", "mutable") - err = corpus.mutableSequenceFiles.readFiles("*.json") + // Migrate the legacy corpus structure + // Note that it is important to call this first since we want to move all the call sequence files before reading + // them into the corpus + err = corpus.migrateLegacyCorpus() if err != nil { return nil, err } - // Read immutable call sequences. - corpus.immutableSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences", "immutable") - err = corpus.immutableSequenceFiles.readFiles("*.json") + // Read call sequences. + corpus.callSequenceFiles.path = filepath.Join(corpus.storageDirectory, "call_sequences") + err = corpus.callSequenceFiles.readFiles("*.json") if err != nil { return nil, err } @@ -100,26 +98,90 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) { return corpus, nil } +// migrateLegacyCorpus is used to read in the legacy corpus standard where call sequences were stored in two separate +// directories (mutable/immutable). +func (c *Corpus) migrateLegacyCorpus() error { + // Check to see if the mutable and/or the immutable directories exist + callSequencePath := filepath.Join(c.storageDirectory, "call_sequences") + mutablePath := filepath.Join(c.storageDirectory, "call_sequences", "mutable") + immutablePath := filepath.Join(c.storageDirectory, "call_sequences", "immutable") + + // Only return an error if the error is something other than "filepath does not exist" + mutableDirInfo, err := os.Stat(mutablePath) + if err != nil && !os.IsNotExist(err) { + return err + } + immutableDirInfo, err := os.Stat(immutablePath) + if err != nil && !os.IsNotExist(err) { + return err + } + + // Return early if these directories do not exist + if mutableDirInfo == nil && immutableDirInfo == nil { + return nil + } + + // Now, we need to notify the user that we have detected a legacy structure + c.logger.Info("Migrating legacy corpus") + + // If the mutable directory exists, read in all the files and add them to the call sequence files + if mutableDirInfo != nil { + // Discover all corpus files in the given directory. + filePaths, err := filepath.Glob(filepath.Join(mutablePath, "*.json")) + if err != nil { + return err + } + + // Move each file from the mutable directory to the parent call_sequences directory + for _, filePath := range filePaths { + err = utils.MoveFile(filePath, filepath.Join(callSequencePath, filepath.Base(filePath))) + if err != nil { + return err + } + } + + // Delete the mutable directory + err = utils.DeleteDirectory(mutablePath) + if err != nil { + return err + } + } + + // If the immutable directory exists, read in all the files and add them to the call sequence files + if immutableDirInfo != nil { + // Discover all corpus files in the given directory. + filePaths, err := filepath.Glob(filepath.Join(immutablePath, "*.json")) + if err != nil { + return err + } + + // Move each file from the immutable directory to the parent call_sequences directory + for _, filePath := range filePaths { + err = utils.MoveFile(filePath, filepath.Join(callSequencePath, filepath.Base(filePath))) + if err != nil { + return err + } + } + + // Delete the immutable directory + err = utils.DeleteDirectory(immutablePath) + if err != nil { + return err + } + } + + return nil +} + // CoverageMaps exposes coverage details for all call sequences known to the corpus. func (c *Corpus) CoverageMaps() *coverage.CoverageMaps { return c.coverageMaps } -// CallSequenceEntryCount returns the total number of call sequences entries in the corpus, based on the provided filter -// flags. Some call sequences may not be valid for use if they fail validation when initializing the corpus. -// Returns the count of the requested call sequence entries. -func (c *Corpus) CallSequenceEntryCount(mutable bool, immutable bool, testResults bool) int { - count := 0 - if mutable { - count += len(c.mutableSequenceFiles.files) - } - if immutable { - count += len(c.immutableSequenceFiles.files) - } - if testResults { - count += len(c.testResultSequenceFiles.files) - } - return count +// CallSequenceEntryCount returns the total number of call sequences that increased coverage and also any test results +// that led to a failure. +func (c *Corpus) CallSequenceEntryCount() (int, int) { + return len(c.callSequenceFiles.files), len(c.testResultSequenceFiles.files) } // ActiveMutableSequenceCount returns the count of call sequences recorded in the corpus which have been validated @@ -302,18 +364,13 @@ func (c *Corpus) Initialize(baseTestChain *chain.TestChain, contractDefinitions return 0, 0, err } - err = c.initializeSequences(c.mutableSequenceFiles, testChain, deployedContracts, true) - if err != nil { - return 0, 0, err - } - - err = c.initializeSequences(c.immutableSequenceFiles, testChain, deployedContracts, false) + err = c.initializeSequences(c.callSequenceFiles, testChain, deployedContracts, true) if err != nil { return 0, 0, err } // Calculate corpus health metrics - corpusSequencesTotal := len(c.mutableSequenceFiles.files) + len(c.immutableSequenceFiles.files) + len(c.testResultSequenceFiles.files) + corpusSequencesTotal := len(c.callSequenceFiles.files) + len(c.testResultSequenceFiles.files) corpusSequencesActive := len(c.unexecutedCallSequences) return corpusSequencesActive, corpusSequencesTotal, nil @@ -411,17 +468,9 @@ func (c *Corpus) CheckSequenceCoverageAndUpdate(callSequence calls.CallSequence, } // If we had an increase in non-reverted or reverted coverage, we save the sequence. - // Note: We only want to save the sequence once. We're most interested if it can be used for mutations first. - if coverageUpdated { - // If we achieved new non-reverting coverage, save this sequence for mutation purposes. - err = c.addCallSequence(c.mutableSequenceFiles, callSequence, true, mutationChooserWeight, flushImmediately) - if err != nil { - return err - } - } else if revertedCoverageUpdated { - // If we did not achieve new successful coverage, but achieved an increase in reverted coverage, save this - // sequence for non-mutation purposes. - err = c.addCallSequence(c.immutableSequenceFiles, callSequence, false, mutationChooserWeight, flushImmediately) + if coverageUpdated || revertedCoverageUpdated { + // If we achieved new coverage, save this sequence for mutation purposes. + err = c.addCallSequence(c.callSequenceFiles, callSequence, true, mutationChooserWeight, flushImmediately) if err != nil { return err } @@ -470,8 +519,8 @@ func (c *Corpus) Flush() error { c.callSequencesLock.Lock() defer c.callSequencesLock.Unlock() - // Write mutation target call sequences. - err := c.mutableSequenceFiles.writeFiles() + // Write all coverage-increasing call sequences. + err := c.callSequenceFiles.writeFiles() if err != nil { return err } @@ -482,11 +531,5 @@ func (c *Corpus) Flush() error { return err } - // Write other call sequences. - err = c.immutableSequenceFiles.writeFiles() - if err != nil { - return err - } - return nil } diff --git a/fuzzing/corpus/corpus_test.go b/fuzzing/corpus/corpus_test.go index c49c904e..1997588f 100644 --- a/fuzzing/corpus/corpus_test.go +++ b/fuzzing/corpus/corpus_test.go @@ -23,7 +23,7 @@ func getMockSimpleCorpus(minSequences int, maxSequences, minBlocks int, maxBlock // Add the requested number of entries. numSequences := minSequences + (rand.Int() % (maxSequences - minSequences)) for i := 0; i < numSequences; i++ { - err := corpus.addCallSequence(corpus.mutableSequenceFiles, getMockCallSequence(minBlocks+(rand.Int()%(maxBlocks-minBlocks))), true, nil, false) + err := corpus.addCallSequence(corpus.callSequenceFiles, getMockCallSequence(minBlocks+(rand.Int()%(maxBlocks-minBlocks))), true, nil, false) if err != nil { return nil, err } @@ -100,9 +100,9 @@ func TestCorpusReadWrite(t *testing.T) { assert.NoError(t, err) // Ensure that there are the correct number of call sequence files - matches, err := filepath.Glob(filepath.Join(corpus.mutableSequenceFiles.path, "*.json")) + matches, err := filepath.Glob(filepath.Join(corpus.callSequenceFiles.path, "*.json")) assert.NoError(t, err) - assert.EqualValues(t, len(corpus.mutableSequenceFiles.files), len(matches)) + assert.EqualValues(t, len(corpus.callSequenceFiles.files), len(matches)) // Wipe corpus clean so that you can now read it in from disk corpus, err = NewCorpus("corpus") @@ -124,7 +124,7 @@ func TestCorpusCallSequenceMarshaling(t *testing.T) { // Run the test in our temporary test directory to avoid artifact pollution. testutils.ExecuteInDirectory(t, t.TempDir(), func() { // For each entry, marshal it and then unmarshal the byte array - for _, entryFile := range corpus.mutableSequenceFiles.files { + for _, entryFile := range corpus.callSequenceFiles.files { // Marshal the entry b, err := json.Marshal(entryFile.data) assert.NoError(t, err) @@ -139,9 +139,9 @@ func TestCorpusCallSequenceMarshaling(t *testing.T) { } // Remove all items - for i := 0; i < len(corpus.mutableSequenceFiles.files); { - corpus.mutableSequenceFiles.removeFile(corpus.mutableSequenceFiles.files[i].fileName) + for i := 0; i < len(corpus.callSequenceFiles.files); { + corpus.callSequenceFiles.removeFile(corpus.callSequenceFiles.files[i].fileName) } - assert.Empty(t, corpus.mutableSequenceFiles.files) + assert.Empty(t, corpus.callSequenceFiles.files) }) } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 5234ac1d..0b9b1500 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -762,8 +762,8 @@ func (f *Fuzzer) Start() error { // Initialize our coverage maps by measuring the coverage we get from the corpus. var corpusActiveSequences, corpusTotalSequences int - if f.corpus.CallSequenceEntryCount(true, true, true) > 0 { - f.logger.Info("Running call sequences in the corpus...") + if totalCallSequences, testResults := f.corpus.CallSequenceEntryCount(); totalCallSequences > 0 || testResults > 0 { + f.logger.Info("Running call sequences in the corpus") } startTime := time.Now() corpusActiveSequences, corpusTotalSequences, err = f.corpus.Initialize(baseTestChain, f.contractDefinitions) diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 47450b63..f4b7bf99 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -826,7 +826,8 @@ func TestCorpusReplayability(t *testing.T) { // Cache current coverage maps originalCoverage := f.fuzzer.corpus.CoverageMaps() - originalCorpusSequenceCount := f.fuzzer.corpus.CallSequenceEntryCount(true, true, true) + originalTotalCallSequences, originalTotalTestResults := f.fuzzer.corpus.CallSequenceEntryCount() + originalCorpusSequenceCount := originalTotalCallSequences + originalTotalTestResults // Next, set the fuzzer worker count to one, this allows us to count the call sequences executed before // solving a problem. We will verify the problem is solved with less or equal sequences tested, than diff --git a/fuzzing/fuzzer_test_methods_test.go b/fuzzing/fuzzer_test_methods_test.go index a022551b..80a7f152 100644 --- a/fuzzing/fuzzer_test_methods_test.go +++ b/fuzzing/fuzzer_test_methods_test.go @@ -81,7 +81,7 @@ func assertFailedTestsExpected(f *fuzzerTestContext, expectFailure bool) { // corpus. It asserts that the actual result matches the provided expected result. func assertCorpusCallSequencesCollected(f *fuzzerTestContext, expectCallSequences bool) { // Obtain our count of mutable (often representing just non-reverted coverage increasing) sequences. - callSequenceCount := f.fuzzer.corpus.CallSequenceEntryCount(true, false, false) + callSequenceCount, _ := f.fuzzer.corpus.CallSequenceEntryCount() // Ensure we captured some coverage-increasing call sequences. if expectCallSequences { diff --git a/utils/fs_utils.go b/utils/fs_utils.go index df656bc6..5a68150e 100644 --- a/utils/fs_utils.go +++ b/utils/fs_utils.go @@ -79,6 +79,35 @@ func CopyFile(sourcePath string, targetPath string) error { return os.Chmod(targetPath, sourceInfo.Mode()) } +// MoveFile will move a given file from the source path to the target path. Returns an error if one occured. +func MoveFile(sourcePath string, targetPath string) error { + // Obtain file info for the source file + sourceInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + + // If the path refers to a directory, return an error + if sourceInfo.IsDir() { + return fmt.Errorf("could not copy file from '%s' to '%s' because the source path refers to a directory", sourcePath, targetPath) + } + + // Ensure the existence of the directory we wish to copy to. + targetDirectory := filepath.Dir(targetPath) + err = os.MkdirAll(targetDirectory, 0777) + if err != nil { + return err + } + + // Move the file from the source path to the target path + err = os.Rename(sourcePath, targetPath) + if err != nil { + return err + } + + return nil +} + // GetFileNameWithoutExtension obtains a filename without the extension. This does not contain any preceding directory // paths. func GetFileNameWithoutExtension(filePath string) string { From 3a9b0fae7888b1c1972c1178ca5022ebd51f5414 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 27 Aug 2024 20:21:03 -0500 Subject: [PATCH 098/109] add quick install and link to installation intrs. (#465) * add quick install and link to installation intrs. --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 18f154b5..162145df 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ `medusa` is a cross-platform [go-ethereum](https://github.com/ethereum/go-ethereum/)-based smart contract fuzzer inspired by [Echidna](https://github.com/crytic/echidna). It provides parallelized fuzz testing of smart contracts through CLI, or its Go API that allows custom user-extended testing methodology. -**Disclaimer**: Please note that `medusa` is an **experimental** smart contract fuzzer. Currently, it should _not_ be adopted into production systems. We intend for `medusa` to reach the same capabilities and maturity that Echidna has. Until then, be careful using `medusa` as your primary smart contract fuzz testing solution. Additionally, please be aware that the Go-level testing API is still **under development** and is subject to breaking changes. +**Disclaimer**: The Go-level testing API is still **under development** and is subject to breaking changes. ## Features @@ -29,6 +29,23 @@ cd docs mdbook serve ``` +## Install + +MacOS users can install the latest release of `medusa` using Homebrew: + +```shell + +brew install medusa +``` + +The master branch can be installed using the following command: + +```shell +brew install --HEAD medusa +``` + +For more information on building from source or obtaining binaries for Windows and Linux, please refer to the [installation guide](./docs/src/getting_started/installation.md). + ## Contributing For information about how to contribute to this project, check out the [CONTRIBUTING](./CONTRIBUTING.md) guidelines. From 4e56d691d298dcb89adf716ec5993137fde520c2 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 5 Sep 2024 13:05:15 -0500 Subject: [PATCH 099/109] feat: allow disabling account checks so contract's can be pranked (#468) * feat: allow disabling account checks so contract's can be pranked * update mdbook * remove medusa.json from version control * run prettier --------- Co-authored-by: Anish Naik --- chain/config/config.go | 3 +++ chain/config/config_defaults.go | 1 + docs/src/project_configuration/chain_config.md | 6 ++++++ docs/src/static/medusa.json | 9 +++++++-- fuzzing/fuzzer_worker_sequence_generator.go | 4 ++++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/chain/config/config.go b/chain/config/config.go index b50fc2f6..6a3b0c4e 100644 --- a/chain/config/config.go +++ b/chain/config/config.go @@ -14,6 +14,9 @@ type TestChainConfig struct { // CheatCodeConfig indicates the configuration for EVM cheat codes to use. CheatCodeConfig CheatCodeConfig `json:"cheatCodes"` + // SkipAccountChecks skips account pre-checks like nonce validation and disallowing non-EOA tx senders (this is done in eth_call, for instance). + SkipAccountChecks bool `json:"skipAccountChecks"` + // ContractAddressOverrides describes contracts that are going to be deployed at deterministic addresses ContractAddressOverrides map[common.Hash]common.Address `json:"contractAddressOverrides,omitempty"` } diff --git a/chain/config/config_defaults.go b/chain/config/config_defaults.go index 72ed8915..5a611c1c 100644 --- a/chain/config/config_defaults.go +++ b/chain/config/config_defaults.go @@ -10,6 +10,7 @@ func DefaultTestChainConfig() (*TestChainConfig, error) { CheatCodesEnabled: true, EnableFFI: false, }, + SkipAccountChecks: true, } // Return the generated configuration. diff --git a/docs/src/project_configuration/chain_config.md b/docs/src/project_configuration/chain_config.md index b101b56d..13bc0685 100644 --- a/docs/src/project_configuration/chain_config.md +++ b/docs/src/project_configuration/chain_config.md @@ -9,6 +9,12 @@ The chain configuration defines the parameters for setting up `medusa`'s underly - > 🚩 Setting `codeSizeCheckDisabled` to `false` is not recommended since it complicates the fuzz testing process. - **Default**: `true` +### `skipAccountChecks` + +- **Type**: Boolean +- **Description**: If `true`, account-related checks (nonce validation, transaction origin must be an EOA) are disabled in `go-ethereum`. +- **Default**: `true` + ## Cheatcode Configuration ### `cheatCodesEnabled` diff --git a/docs/src/static/medusa.json b/docs/src/static/medusa.json index 2e8644b6..8d08a8d0 100644 --- a/docs/src/static/medusa.json +++ b/docs/src/static/medusa.json @@ -4,10 +4,12 @@ "workerResetLimit": 50, "timeout": 0, "testLimit": 0, + "shrinkLimit": 5000, "callSequenceLength": 100, "corpusDirectory": "", "coverageEnabled": true, "targetContracts": [], + "predeployedContracts": {}, "targetContractsBalances": [], "constructorArgs": {}, "deployerAddress": "0x30000", @@ -45,14 +47,17 @@ "optimizationTesting": { "enabled": true, "testPrefixes": ["optimize_"] - } + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] }, "chainConfig": { "codeSizeCheckDisabled": true, "cheatCodes": { "cheatCodesEnabled": true, "enableFFI": false - } + }, + "skipAccountChecks": true } }, "compilation": { diff --git a/fuzzing/fuzzer_worker_sequence_generator.go b/fuzzing/fuzzer_worker_sequence_generator.go index 666efb23..b0bd3557 100644 --- a/fuzzing/fuzzer_worker_sequence_generator.go +++ b/fuzzing/fuzzer_worker_sequence_generator.go @@ -321,6 +321,10 @@ func (g *CallSequenceGenerator) generateNewElement() (*calls.CallSequenceElement InputValues: args, }) + if g.worker.fuzzer.config.Fuzzing.TestChainConfig.SkipAccountChecks { + msg.SkipAccountChecks = true + } + // Determine our delay values for this element blockNumberDelay := uint64(0) blockTimestampDelay := uint64(0) From 7816944848f3076498838b24bf84b8abc705a280 Mon Sep 17 00:00:00 2001 From: highcloudwind Date: Fri, 6 Sep 2024 10:13:17 +0800 Subject: [PATCH 100/109] chore: fix some function names (#430) Signed-off-by: highcloudwind --- chain/test_chain_test.go | 2 +- fuzzing/calls/call_sequence_execution.go | 2 +- fuzzing/corpus/corpus_test.go | 4 ++-- fuzzing/fuzzer_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chain/test_chain_test.go b/chain/test_chain_test.go index ff0ca589..048822a1 100644 --- a/chain/test_chain_test.go +++ b/chain/test_chain_test.go @@ -539,7 +539,7 @@ func TestChainCloning(t *testing.T) { }) } -// TestCallSequenceReplayMatchSimple creates a TestChain, sends some messages to it, then creates another chain which +// TestChainCallSequenceReplayMatchSimple creates a TestChain, sends some messages to it, then creates another chain which // it replays the same sequence on. It ensures that the ending state is the same. // Note: this does not set block timestamps or other data that might be non-deterministic. // This does not test replaying with a previous call sequence with different timestamps, etc. It expects the TestChain diff --git a/fuzzing/calls/call_sequence_execution.go b/fuzzing/calls/call_sequence_execution.go index ca983f0d..593465f4 100644 --- a/fuzzing/calls/call_sequence_execution.go +++ b/fuzzing/calls/call_sequence_execution.go @@ -169,7 +169,7 @@ func ExecuteCallSequence(chain *chain.TestChain, callSequence CallSequence) (Cal return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil) } -// ExecuteCallSequenceWithTracer attaches an executiontracer.ExecutionTracer to ExecuteCallSequenceIteratively and attaches execution traces to the call sequence elements. +// ExecuteCallSequenceWithExecutionTracer attaches an executiontracer.ExecutionTracer to ExecuteCallSequenceIteratively and attaches execution traces to the call sequence elements. func ExecuteCallSequenceWithExecutionTracer(testChain *chain.TestChain, contractDefinitions contracts.Contracts, callSequence CallSequence, verboseTracing bool) (CallSequence, error) { // Create a new execution tracer executionTracer := executiontracer.NewExecutionTracer(contractDefinitions, testChain.CheatCodeContracts()) diff --git a/fuzzing/corpus/corpus_test.go b/fuzzing/corpus/corpus_test.go index 1997588f..5997dbdd 100644 --- a/fuzzing/corpus/corpus_test.go +++ b/fuzzing/corpus/corpus_test.go @@ -31,7 +31,7 @@ func getMockSimpleCorpus(minSequences int, maxSequences, minBlocks int, maxBlock return corpus, nil } -// getMockSimpleCorpusEntry creates a mock CorpusCallSequence with numBlocks blocks for testing +// getMockCallSequence creates a mock CorpusCallSequence with numBlocks blocks for testing func getMockCallSequence(size int) calls.CallSequence { cs := make(calls.CallSequence, size) for i := 0; i < size; i++ { @@ -40,7 +40,7 @@ func getMockCallSequence(size int) calls.CallSequence { return cs } -// getMockSimpleBlockBlock creates a mock CorpusBlock with numTransactions transactions and receipts for testing +// getMockCallSequenceElement creates a mock CorpusBlock with numTransactions transactions and receipts for testing func getMockCallSequenceElement() *calls.CallSequenceElement { return &calls.CallSequenceElement{ Contract: nil, diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index f4b7bf99..06f0992b 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -444,7 +444,7 @@ func TestDeploymentsWithPayableConstructors(t *testing.T) { }) } -// TestDeploymentsInnerDeployments runs a test to ensure dynamically deployed contracts are detected by the Fuzzer and +// TestDeploymentsSelfDestruct runs a test to ensure dynamically deployed contracts are detected by the Fuzzer and // their properties are tested appropriately. func TestDeploymentsSelfDestruct(t *testing.T) { // These contracts provide functions to deploy inner contracts which have properties that will produce a failure. From f5080700438d39b6c4dae4082ee3d47b008c940f Mon Sep 17 00:00:00 2001 From: samalws-tob <129795909+samalws-tob@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:07:55 -0400 Subject: [PATCH 101/109] perf: cache codehash to speedup coverage tracking (#472) --- fuzzing/coverage/coverage_tracer.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/fuzzing/coverage/coverage_tracer.go b/fuzzing/coverage/coverage_tracer.go index 2f1a47e9..0cbe0785 100644 --- a/fuzzing/coverage/coverage_tracer.go +++ b/fuzzing/coverage/coverage_tracer.go @@ -52,6 +52,13 @@ type CoverageTracer struct { // nativeTracer is the underlying tracer used to capture EVM execution. nativeTracer *chain.TestChainTracer + + // codeHashCache is a cache for values returned by getContractCoverageMapHash, + // so that this expensive calculation doesn't need to be done every opcode. + // The [2] array is to differentiate between contract init (0) vs runtime (1), + // since init vs runtime produces different results from getContractCoverageMapHash. + // The Hash key is a contract's codehash, which uniquely identifies it. + codeHashCache [2]map[common.Hash]common.Hash } // coverageTracerCallFrameState tracks state across call frames in the tracer. @@ -71,6 +78,7 @@ func NewCoverageTracer() *CoverageTracer { tracer := &CoverageTracer{ coverageMaps: NewCoverageMaps(), callFrameStates: make([]*coverageTracerCallFrameState, 0), + codeHashCache: [2]map[common.Hash]common.Hash{make(map[common.Hash]common.Hash), make(map[common.Hash]common.Hash)}, } nativeTracer := &tracers.Tracer{ Hooks: &tracing.Hooks{ @@ -159,11 +167,23 @@ func (t *CoverageTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tr scopeContext := scope.(*vm.ScopeContext) code := scopeContext.Contract.Code codeSize := len(code) + isCreate := callFrameState.create + gethCodeHash := scopeContext.Contract.CodeHash + + cacheArrayKey := 1 + if isCreate { + cacheArrayKey = 0 + } + if codeSize > 0 { // Obtain our contract coverage map lookup hash. if callFrameState.lookupHash == nil { - lookupHash := getContractCoverageMapHash(code, callFrameState.create) + lookupHash, cacheHit := t.codeHashCache[cacheArrayKey][gethCodeHash] + if !cacheHit { + lookupHash = getContractCoverageMapHash(code, isCreate) + t.codeHashCache[cacheArrayKey][gethCodeHash] = lookupHash + } callFrameState.lookupHash = &lookupHash } From 61e1a65dd2a9410724bc074ccc7ee5613ec3153c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20L=C3=B3pez?= <2642849+elopez@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:00:51 +0200 Subject: [PATCH 102/109] Update x86 macOS runner to `macos-13` (#480) The `macos-12` x86 runner will be retired by early December: We are beginning the deprecation process for the macOS 12 runner image, which allows us to balance our fleet capacity ahead of our upcoming macOS 15 launch. This image will be fully retired by the December 3rd, 2024. We recommend updating workflows to use `macos-14`, `macos-13`, or `macos-latest`. -- https://github.blog/changelog/2024-08-19-notice-of-upcoming-deprecations-and-breaking-changes-in-github-actions-runners/ --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36aade25..40c9fbe8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: needs: [lint, test] strategy: matrix: - environment: [ubuntu-latest, macos-12, macos-14, windows-latest] + environment: [ubuntu-latest, macos-13, macos-14, windows-latest] permissions: contents: read id-token: write @@ -165,7 +165,7 @@ jobs: test: strategy: matrix: - environment: [ubuntu-latest, macos-12, macos-14, windows-latest] + environment: [ubuntu-latest, macos-13, macos-14, windows-latest] runs-on: ${{ matrix.environment }} timeout-minutes: 20 From fc59d39af0adb201fabc82aec8972dedfec25505 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Thu, 19 Sep 2024 11:06:02 -0400 Subject: [PATCH 103/109] update to latest commit on v1.14.6 medusa-geth branch (#483) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f5a85d40..3bae89ac 100644 --- a/go.mod +++ b/go.mod @@ -88,4 +88,4 @@ require ( rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20240708141007-2f7f9258289f +replace github.com/ethereum/go-ethereum => github.com/crytic/medusa-geth v0.0.0-20240919134035-0fd368c28419 diff --git a/go.sum b/go.sum index d01119b9..6c8f950d 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJ github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crytic/medusa-geth v0.0.0-20240708141007-2f7f9258289f h1:fxAlt4nFXa2WhoGVXbPezydKLFKx0mDRD4voT/xPcF4= -github.com/crytic/medusa-geth v0.0.0-20240708141007-2f7f9258289f/go.mod h1:hglUZo/5pVIYXNyYjWzsAUDpT/zI+WbWo/Nih7ot+G0= +github.com/crytic/medusa-geth v0.0.0-20240919134035-0fd368c28419 h1:MJXzWPObZtF0EMRqX64JkzJDj+GMLPxg3XK5xb12FFU= +github.com/crytic/medusa-geth v0.0.0-20240919134035-0fd368c28419/go.mod h1:ajGCVsk6ctffGwe9TSDQqj4HIUUQ1WdUit5tWFNl8Tw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From e018eb311c31feac24b37d2326990a4d3310566f Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 25 Sep 2024 10:47:55 -0500 Subject: [PATCH 104/109] feat: LCOV report (#442) * feat: produce LCOV-style coverage report * add function metadata to LCOV * consider at least one line hit as function coverage * add coverage report config to select between lcov and html * add default path * lint * fix whitespace in default * rename to coverageFormats --------- Co-authored-by: Anish Naik --- compilation/platforms/crytic_compile.go | 2 +- compilation/platforms/solc.go | 2 +- compilation/types/ast.go | 120 ++++++++++++-- docs/src/coverage_reports.md | 43 ++++- .../project_configuration/fuzzing_config.md | 7 + fuzzing/config/config.go | 13 ++ fuzzing/config/config_defaults.go | 1 + fuzzing/config/gen_fuzzing_config.go | 6 + fuzzing/coverage/report_generation.go | 63 ++++---- fuzzing/coverage/source_analysis.go | 152 +++++++++++++++--- fuzzing/fuzzer.go | 31 +++- 11 files changed, 364 insertions(+), 76 deletions(-) diff --git a/compilation/platforms/crytic_compile.go b/compilation/platforms/crytic_compile.go index 6039df26..77d43245 100644 --- a/compilation/platforms/crytic_compile.go +++ b/compilation/platforms/crytic_compile.go @@ -204,7 +204,7 @@ func (c *CryticCompilationConfig) Compile() ([]types.Compilation, string, error) } // Retrieve the source unit ID - sourceUnitId := ast.GetSourceUnitID() + sourceUnitId := types.GetSrcMapSourceUnitID(ast.Src) compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ // TODO: Our types.AST is not the same as the original AST but we could parse it and avoid using "any" Ast: source.AST, diff --git a/compilation/platforms/solc.go b/compilation/platforms/solc.go index 4ceef747..068cbeb7 100644 --- a/compilation/platforms/solc.go +++ b/compilation/platforms/solc.go @@ -145,7 +145,7 @@ func (s *SolcCompilationConfig) Compile() ([]types.Compilation, string, error) { } // Get the source unit ID - sourceUnitId := ast.GetSourceUnitID() + sourceUnitId := types.GetSrcMapSourceUnitID(ast.Src) // Construct our compiled source object compilation.SourcePathToArtifact[sourcePath] = types.SourceArtifact{ // TODO our types.AST is not the same as the original AST but we could parse it and avoid using "any" diff --git a/compilation/types/ast.go b/compilation/types/ast.go index f6b21612..b1dd251c 100644 --- a/compilation/types/ast.go +++ b/compilation/types/ast.go @@ -20,24 +20,84 @@ const ( // Node interface represents a generic AST node type Node interface { + // GetNodeType returns solc's node type e.g. FunctionDefinition, ContractDefinition. GetNodeType() string } +// FunctionDefinition is the function definition node +type FunctionDefinition struct { + // NodeType represents the node type (currently we only evaluate source unit node types) + NodeType string `json:"nodeType"` + // Src is the source file for this AST + Src string `json:"src"` + Name string `json:"name,omitempty"` +} + +func (s FunctionDefinition) GetNodeType() string { + return s.NodeType +} + // ContractDefinition is the contract definition node type ContractDefinition struct { - // NodeType represents the AST node type (note that it will always be a contract definition) + // NodeType represents the node type (currently we only evaluate source unit node types) NodeType string `json:"nodeType"` + // Nodes is a list of Nodes within the AST + Nodes []Node `json:"nodes"` + // Src is the source file for this AST + Src string `json:"src"` // CanonicalName is the name of the contract definition CanonicalName string `json:"canonicalName,omitempty"` // Kind is a ContractKind that represents what type of contract definition this is (contract, interface, or library) Kind ContractKind `json:"contractKind,omitempty"` } -// GetNodeType implements the Node interface and returns the node type for the contract definition func (s ContractDefinition) GetNodeType() string { return s.NodeType } +func (c *ContractDefinition) UnmarshalJSON(data []byte) error { + // Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later + type Alias ContractDefinition + aux := &struct { + Nodes []json.RawMessage `json:"nodes"` + + *Alias + }{ + Alias: (*Alias)(c), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Iterate through all the nodes of the contract definition + for _, nodeData := range aux.Nodes { + // Unmarshal the node data to retrieve the node type + var nodeType struct { + NodeType string `json:"nodeType"` + } + if err := json.Unmarshal(nodeData, &nodeType); err != nil { + return err + } + + // Unmarshal the contents of the node based on the node type + switch nodeType.NodeType { + case "FunctionDefinition": + // If this is a function definition, unmarshal it + var functionDefinition FunctionDefinition + if err := json.Unmarshal(nodeData, &functionDefinition); err != nil { + return err + } + c.Nodes = append(c.Nodes, functionDefinition) + default: + continue + } + } + + return nil + +} + // AST is the abstract syntax tree type AST struct { // NodeType represents the node type (currently we only evaluate source unit node types) @@ -48,7 +108,6 @@ type AST struct { Src string `json:"src"` } -// UnmarshalJSON unmarshals from JSON func (a *AST) UnmarshalJSON(data []byte) error { // Unmarshal the top-level AST into our own representation. Defer the unmarshaling of all the individual nodes until later type Alias AST @@ -62,11 +121,6 @@ func (a *AST) UnmarshalJSON(data []byte) error { return err } - // Check if nodeType is "SourceUnit". Return early otherwise - if aux.NodeType != "SourceUnit" { - return nil - } - // Iterate through all the nodes of the source unit for _, nodeData := range aux.Nodes { // Unmarshal the node data to retrieve the node type @@ -78,7 +132,6 @@ func (a *AST) UnmarshalJSON(data []byte) error { } // Unmarshal the contents of the node based on the node type - var node Node switch nodeType.NodeType { case "ContractDefinition": // If this is a contract definition, unmarshal it @@ -86,23 +139,30 @@ func (a *AST) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(nodeData, &contractDefinition); err != nil { return err } - node = contractDefinition + a.Nodes = append(a.Nodes, contractDefinition) + + case "FunctionDefinition": + // If this is a function definition, unmarshal it + var functionDefinition FunctionDefinition + if err := json.Unmarshal(nodeData, &functionDefinition); err != nil { + return err + } + a.Nodes = append(a.Nodes, functionDefinition) + // TODO: Add cases for other node types as needed default: continue } - // Append the node - a.Nodes = append(a.Nodes, node) } return nil } -// GetSourceUnitID returns the source unit ID based on the source of the AST -func (a *AST) GetSourceUnitID() int { +// GetSrcMapSourceUnitID returns the source unit ID based on the source of the AST +func GetSrcMapSourceUnitID(src string) int { re := regexp.MustCompile(`[0-9]*:[0-9]*:([0-9]*)`) - sourceUnitCandidates := re.FindStringSubmatch(a.Src) + sourceUnitCandidates := re.FindStringSubmatch(src) if len(sourceUnitCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element sourceUnit, err := strconv.Atoi(sourceUnitCandidates[1]) @@ -112,3 +172,33 @@ func (a *AST) GetSourceUnitID() int { } return -1 } + +// GetSrcMapStart returns the byte offset where the function definition starts in the source file +func GetSrcMapStart(src string) int { + // 95:42:0 returns 95 + re := regexp.MustCompile(`([0-9]*):[0-9]*:[0-9]*`) + startCandidates := re.FindStringSubmatch(src) + + if len(startCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element + start, err := strconv.Atoi(startCandidates[1]) + if err == nil { + return start + } + } + return -1 +} + +// GetSrcMapLength returns the length of the function definition in bytes +func GetSrcMapLength(src string) int { + // 95:42:0 returns 42 + re := regexp.MustCompile(`[0-9]*:([0-9]*):[0-9]*`) + endCandidates := re.FindStringSubmatch(src) + + if len(endCandidates) == 2 { // FindStringSubmatch includes the whole match as the first element + end, err := strconv.Atoi(endCandidates[1]) + if err == nil { + return end + } + } + return -1 +} diff --git a/docs/src/coverage_reports.md b/docs/src/coverage_reports.md index cd24b564..10920207 100644 --- a/docs/src/coverage_reports.md +++ b/docs/src/coverage_reports.md @@ -1,3 +1,44 @@ # Coverage Reports -WIP +## Generating HTML Report from LCOV + +Enable coverage reporting by setting the `corpusDirectory` key in the configuration file and setting the `coverageReports` key to `["lcov", "html"]`. + +```json +{ + "corpusDirectory": "corpus", + "coverageReports": ["lcov", "html"] +} +``` + +### Install lcov and genhtml + +Linux: + +```bash +apt-get install lcov +``` + +MacOS: + +```bash +brew install lcov +``` + +### Generate LCOV Report + +```bash + +genhtml corpus/coverage/lcov.info --output-dir corpus --rc derive_function_end_line=0 +``` + +> [!WARNING] +> ** The `derive_function_end_line` flag is required to prevent the `genhtml` tool from crashing when processing the Solidity source code. ** + +Open the `corpus/index.html` file in your browser or follow the steps to use VSCode below. + +### View Coverage Report in VSCode with Coverage Gutters + +Install the [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension. + +Then, right click in a project file and select `Coverage Gutters: Display Coverage`. diff --git a/docs/src/project_configuration/fuzzing_config.md b/docs/src/project_configuration/fuzzing_config.md index b2bf537d..5adcd067 100644 --- a/docs/src/project_configuration/fuzzing_config.md +++ b/docs/src/project_configuration/fuzzing_config.md @@ -55,6 +55,13 @@ The fuzzing configuration defines the parameters for the fuzzing campaign. can then be re-used/mutated by the fuzzer during the next fuzzing campaign. - **Default**: "" +### `coverageFormats` + +- **Type**: [String] (e.g. `["lcov"]`) +- **Description**: The coverage reports to generate after the fuzzing campaign has completed. The coverage reports are saved + in the `coverage` directory within `crytic-export/` or `corpusDirectory` if configured. +- **Default**: `["lcov", "html"]` + ### `targetContracts` - **Type**: [String] (e.g. `[FirstContract, SecondContract, ThirdContract]`) diff --git a/fuzzing/config/config.go b/fuzzing/config/config.go index e488238c..1ccbfb61 100644 --- a/fuzzing/config/config.go +++ b/fuzzing/config/config.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "errors" + "fmt" "math/big" "os" @@ -60,6 +61,9 @@ type FuzzingConfig struct { // CoverageEnabled describes whether to use coverage-guided fuzzing CoverageEnabled bool `json:"coverageEnabled"` + // CoverageFormats indicate which reports to generate: "lcov" and "html" are supported. + CoverageFormats []string `json:"coverageFormats"` + // TargetContracts are the target contracts for fuzz testing TargetContracts []string `json:"targetContracts"` @@ -391,6 +395,15 @@ func (p *ProjectConfig) Validate() error { } } + // The coverage report format must be either "lcov" or "html" + if p.Fuzzing.CoverageFormats != nil { + for _, report := range p.Fuzzing.CoverageFormats { + if report != "lcov" && report != "html" { + return fmt.Errorf("project configuration must specify only valid coverage reports (lcov, html): %s", report) + } + } + } + // Ensure that the log level is a valid one level, err := zerolog.ParseLevel(p.Logging.Level.String()) if err != nil || level == zerolog.FatalLevel { diff --git a/fuzzing/config/config_defaults.go b/fuzzing/config/config_defaults.go index 10f45dc1..38532a03 100644 --- a/fuzzing/config/config_defaults.go +++ b/fuzzing/config/config_defaults.go @@ -46,6 +46,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) { ConstructorArgs: map[string]map[string]any{}, CorpusDirectory: "", CoverageEnabled: true, + CoverageFormats: []string{"html", "lcov"}, SenderAddresses: []string{ "0x10000", "0x20000", diff --git a/fuzzing/config/gen_fuzzing_config.go b/fuzzing/config/gen_fuzzing_config.go index 6a2784b7..47f780c5 100644 --- a/fuzzing/config/gen_fuzzing_config.go +++ b/fuzzing/config/gen_fuzzing_config.go @@ -23,6 +23,7 @@ func (f FuzzingConfig) MarshalJSON() ([]byte, error) { CallSequenceLength int `json:"callSequenceLength"` CorpusDirectory string `json:"corpusDirectory"` CoverageEnabled bool `json:"coverageEnabled"` + CoverageFormats []string `json:"coverageFormats"` TargetContracts []string `json:"targetContracts"` PredeployedContracts map[string]string `json:"predeployedContracts"` TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` @@ -45,6 +46,7 @@ func (f FuzzingConfig) MarshalJSON() ([]byte, error) { enc.CallSequenceLength = f.CallSequenceLength enc.CorpusDirectory = f.CorpusDirectory enc.CoverageEnabled = f.CoverageEnabled + enc.CoverageFormats = f.CoverageFormats enc.TargetContracts = f.TargetContracts enc.PredeployedContracts = f.PredeployedContracts if f.TargetContractsBalances != nil { @@ -76,6 +78,7 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { CallSequenceLength *int `json:"callSequenceLength"` CorpusDirectory *string `json:"corpusDirectory"` CoverageEnabled *bool `json:"coverageEnabled"` + CoverageFormats []string `json:"coverageFormats"` TargetContracts []string `json:"targetContracts"` PredeployedContracts map[string]string `json:"predeployedContracts"` TargetContractsBalances []*hexutil.Big `json:"targetContractsBalances"` @@ -117,6 +120,9 @@ func (f *FuzzingConfig) UnmarshalJSON(input []byte) error { if dec.CoverageEnabled != nil { f.CoverageEnabled = *dec.CoverageEnabled } + if dec.CoverageFormats != nil { + f.CoverageFormats = dec.CoverageFormats + } if dec.TargetContracts != nil { f.TargetContracts = dec.TargetContracts } diff --git a/fuzzing/coverage/report_generation.go b/fuzzing/coverage/report_generation.go index c9bc2da1..b5125ea7 100644 --- a/fuzzing/coverage/report_generation.go +++ b/fuzzing/coverage/report_generation.go @@ -3,14 +3,14 @@ package coverage import ( _ "embed" "fmt" - "github.com/crytic/medusa/compilation/types" - "github.com/crytic/medusa/utils" "html/template" "math" "os" "path/filepath" "strconv" "time" + + "github.com/crytic/medusa/utils" ) var ( @@ -18,26 +18,8 @@ var ( htmlReportTemplate []byte ) -// GenerateReport takes a set of CoverageMaps and compilations, and produces a coverage report using them, detailing -// all source mapped ranges of the source files which were covered or not. -// Returns an error if one occurred. -func GenerateReport(compilations []types.Compilation, coverageMaps *CoverageMaps, htmlReportPath string) error { - // Perform source analysis. - sourceAnalysis, err := AnalyzeSourceCoverage(compilations, coverageMaps) - if err != nil { - return err - } - - // Finally, export the report data we analyzed. - if htmlReportPath != "" { - err = exportCoverageReport(sourceAnalysis, htmlReportPath) - } - return err -} - -// exportCoverageReport takes a previously performed source analysis and generates an HTML coverage report from it. -// Returns an error if one occurs. -func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) error { +// WriteHTMLReport takes a previously performed source analysis and generates an HTML coverage report from it. +func WriteHTMLReport(sourceAnalysis *SourceAnalysis, reportDir string) (string, error) { // Define mappings onto some useful variables/functions. functionMap := template.FuncMap{ "timeNow": time.Now, @@ -79,21 +61,21 @@ func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) err // Parse our HTML template tmpl, err := template.New("coverage_report.html").Funcs(functionMap).Parse(string(htmlReportTemplate)) if err != nil { - return fmt.Errorf("could not export report, failed to parse report template: %v", err) + return "", fmt.Errorf("could not export report, failed to parse report template: %v", err) } - // If the parent directory doesn't exist, create it. - parentDirectory := filepath.Dir(outputPath) - err = utils.MakeDirectory(parentDirectory) + // If the directory doesn't exist, create it. + err = utils.MakeDirectory(reportDir) if err != nil { - return err + return "", err } // Create our report file - file, err := os.Create(outputPath) + htmlReportPath := filepath.Join(reportDir, "coverage_report.html") + file, err := os.Create(htmlReportPath) if err != nil { _ = file.Close() - return fmt.Errorf("could not export report, failed to open file for writing: %v", err) + return "", fmt.Errorf("could not export report, failed to open file for writing: %v", err) } // Execute the template and write it back to file. @@ -102,5 +84,26 @@ func exportCoverageReport(sourceAnalysis *SourceAnalysis, outputPath string) err if err == nil { err = fileCloseErr } - return err + return htmlReportPath, err +} + +// WriteLCOVReport takes a previously performed source analysis and generates an LCOV report from it. +func WriteLCOVReport(sourceAnalysis *SourceAnalysis, reportDir string) (string, error) { + // Generate the LCOV report. + lcovReport := sourceAnalysis.GenerateLCOVReport() + + // If the directory doesn't exist, create it. + err := utils.MakeDirectory(reportDir) + if err != nil { + return "", err + } + + // Write the LCOV report to a file. + lcovReportPath := filepath.Join(reportDir, "lcov.info") + err = os.WriteFile(lcovReportPath, []byte(lcovReport), 0644) + if err != nil { + return "", fmt.Errorf("could not export LCOV report: %v", err) + } + + return lcovReportPath, nil } diff --git a/fuzzing/coverage/source_analysis.go b/fuzzing/coverage/source_analysis.go index 7705d2c1..98c4bc75 100644 --- a/fuzzing/coverage/source_analysis.go +++ b/fuzzing/coverage/source_analysis.go @@ -2,6 +2,7 @@ package coverage import ( "bytes" + "encoding/json" "fmt" "sort" @@ -55,13 +56,78 @@ func (s *SourceAnalysis) CoveredLineCount() int { return count } +// GenerateLCOVReport generates an LCOV report from the source analysis. +// The spec of the format is here https://github.com/linux-test-project/lcov/blob/07a1127c2b4390abf4a516e9763fb28a956a9ce4/man/geninfo.1#L989 +func (s *SourceAnalysis) GenerateLCOVReport() string { + var linesHit, linesInstrumented int + var buffer bytes.Buffer + buffer.WriteString("TN:\n") + for _, file := range s.SortedFiles() { + // SF: + buffer.WriteString(fmt.Sprintf("SF:%s\n", file.Path)) + for idx, line := range file.Lines { + if line.IsActive { + // DA:, + if line.IsCovered { + buffer.WriteString(fmt.Sprintf("DA:%d,%d\n", idx+1, line.SuccessHitCount)) + linesHit++ + } else { + buffer.WriteString(fmt.Sprintf("DA:%d,%d\n", idx+1, 0)) + } + linesInstrumented++ + } + } + // FN:, + // FNDA:, + for _, fn := range file.Functions { + byteStart := types.GetSrcMapStart(fn.Src) + length := types.GetSrcMapLength(fn.Src) + + startLine := sort.Search(len(file.CumulativeOffsetByLine), func(i int) bool { + return file.CumulativeOffsetByLine[i] > byteStart + }) + endLine := sort.Search(len(file.CumulativeOffsetByLine), func(i int) bool { + return file.CumulativeOffsetByLine[i] > byteStart+length + }) + + // We are treating any line hit in the definition as a hit for the function. + hit := 0 + for i := startLine; i < endLine; i++ { + // index iz zero based, line numbers are 1 based + if file.Lines[i-1].IsActive && file.Lines[i-1].IsCovered { + hit = 1 + } + + } + + // TODO: handle fallback, receive, and constructor + if fn.Name != "" { + buffer.WriteString(fmt.Sprintf("FN:%d,%s\n", startLine, fn.Name)) + buffer.WriteString(fmt.Sprintf("FNDA:%d,%s\n", hit, fn.Name)) + } + + } + buffer.WriteString("end_of_record\n") + } + + return buffer.String() +} + // SourceFileAnalysis describes coverage information for a given source file. type SourceFileAnalysis struct { // Path describes the file path of the source file. This is kept here for access during report generation. Path string + // CumulativeOffsetByLine describes the cumulative byte offset for each line in the source file. + // For example, for a file with 5 lines, the list might look like: [0, 45, 98, 132, 189], where each number is the byte offset of the line's starting position + // This allows us to quickly determine which line a given byte offset falls within using a binary search. + CumulativeOffsetByLine []int + // Lines describes information about a given source line and its coverage. Lines []*SourceLineAnalysis + + // Functions is a list of functions defined in the source file + Functions []*types.FunctionDefinition } // ActiveLineCount returns the count of lines that are marked executable/active within the source file. @@ -130,13 +196,50 @@ func AnalyzeSourceCoverage(compilations []types.Compilation, coverageMaps *Cover return nil, fmt.Errorf("could not perform source code analysis, code was not cached for '%v'", sourcePath) } + lines, cumulativeOffset := parseSourceLines(compilation.SourceCode[sourcePath]) + funcs := make([]*types.FunctionDefinition, 0) + + var ast types.AST + b, err := json.Marshal(compilation.SourcePathToArtifact[sourcePath].Ast) + if err != nil { + return nil, fmt.Errorf("could not encode AST from sources: %v", err) + } + err = json.Unmarshal(b, &ast) + if err != nil { + return nil, fmt.Errorf("could not parse AST from sources: %v", err) + } + + for _, node := range ast.Nodes { + + if node.GetNodeType() == "FunctionDefinition" { + fn := node.(types.FunctionDefinition) + funcs = append(funcs, &fn) + } + if node.GetNodeType() == "ContractDefinition" { + contract := node.(types.ContractDefinition) + if contract.Kind == types.ContractKindInterface { + continue + } + for _, subNode := range contract.Nodes { + if subNode.GetNodeType() == "FunctionDefinition" { + fn := subNode.(types.FunctionDefinition) + funcs = append(funcs, &fn) + } + } + } + + } + // Obtain the parsed source code lines for this source. if _, ok := sourceAnalysis.Files[sourcePath]; !ok { sourceAnalysis.Files[sourcePath] = &SourceFileAnalysis{ - Path: sourcePath, - Lines: parseSourceLines(compilation.SourceCode[sourcePath]), + Path: sourcePath, + CumulativeOffsetByLine: cumulativeOffset, + Lines: lines, + Functions: funcs, } } + } } @@ -231,25 +334,26 @@ func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis // Obtain the source file this element maps to. if sourceFile, ok := sourceAnalysis.Files[sourcePath]; ok { // Mark all lines which fall within this range. - matchedSourceLine := false - for _, sourceLine := range sourceFile.Lines { - // Check if the line is within range - if sourceMapElement.Offset >= sourceLine.Start && sourceMapElement.Offset < sourceLine.End { - // Mark the line active/executable. - sourceLine.IsActive = true - - // 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. - matchedSourceLine = true - } else if matchedSourceLine { - break - } + start := sourceMapElement.Offset + + startLine := sort.Search(len(sourceFile.CumulativeOffsetByLine), func(i int) bool { + return sourceFile.CumulativeOffsetByLine[i] > start + }) + + // index iz zero based, line numbers are 1 based + sourceLine := sourceFile.Lines[startLine-1] + + // Check if the line is within range + if sourceMapElement.Offset < sourceLine.End { + // Mark the line active/executable. + sourceLine.IsActive = true + + // 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 + } } else { return fmt.Errorf("could not perform source code analysis, missing source '%v'", sourcePath) @@ -298,10 +402,11 @@ func filterSourceMaps(compilation types.Compilation, sourceMap types.SourceMap) // parseSourceLines splits the provided source code into SourceLineAnalysis objects. // Returns the SourceLineAnalysis objects. -func parseSourceLines(sourceCode []byte) []*SourceLineAnalysis { +func parseSourceLines(sourceCode []byte) ([]*SourceLineAnalysis, []int) { // Create our lines and a variable to track where our current line start offset is. var lines []*SourceLineAnalysis var lineStart int + var cumulativeOffset []int // Split the source code on new line characters sourceCodeLinesBytes := bytes.Split(sourceCode, []byte("\n")) @@ -317,9 +422,10 @@ func parseSourceLines(sourceCode []byte) []*SourceLineAnalysis { IsCovered: false, IsCoveredReverted: false, }) + cumulativeOffset = append(cumulativeOffset, int(lineStart)) lineStart = lineEnd } // Return the resulting lines - return lines + return lines, cumulativeOffset } diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 0b9b1500..4d862dc0 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -409,6 +409,7 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex fuzzer.config.Fuzzing.TargetContracts = []string{contract.Name()} found = true } else { + // TODO list options for the user to choose from return nil, fmt.Errorf("specify target contract(s)") } } @@ -836,13 +837,33 @@ func (f *Fuzzer) Start() error { f.printExitingResults() // Finally, generate our coverage report if we have set a valid corpus directory. - if err == nil && f.config.Fuzzing.CorpusDirectory != "" { - coverageReportPath := filepath.Join(f.config.Fuzzing.CorpusDirectory, "coverage_report.html") - err = coverage.GenerateReport(f.compilations, f.corpus.CoverageMaps(), coverageReportPath) + if err == nil && len(f.config.Fuzzing.CoverageFormats) > 0 { + // Write to the default directory if we have no corpus directory set. + coverageReportDir := filepath.Join("crytic-export", "coverage") + if f.config.Fuzzing.CorpusDirectory != "" { + coverageReportDir = filepath.Join(f.config.Fuzzing.CorpusDirectory, "coverage") + } + sourceAnalysis, err := coverage.AnalyzeSourceCoverage(f.compilations, f.corpus.CoverageMaps()) + if err != nil { - f.logger.Error("Failed to generate coverage report", err) + f.logger.Error("Failed to analyze source coverage", err) } else { - f.logger.Info("Coverage report saved to file: ", colors.Bold, coverageReportPath, colors.Reset) + var path string + for _, reportType := range f.config.Fuzzing.CoverageFormats { + switch reportType { + case "html": + path, err = coverage.WriteHTMLReport(sourceAnalysis, coverageReportDir) + case "lcov": + path, err = coverage.WriteLCOVReport(sourceAnalysis, coverageReportDir) + default: + err = fmt.Errorf("unsupported coverage report type: %s", reportType) + } + if err != nil { + f.logger.Error(fmt.Sprintf("Failed to generate %s coverage report", reportType), err) + } else { + f.logger.Info(fmt.Sprintf("%s report(s) saved to: %s", reportType, path), colors.Bold, colors.Reset) + } + } } } From 66e60eaa0166ccc8354cb14d47df329cd0733b80 Mon Sep 17 00:00:00 2001 From: Simone <79767264+smonicas@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:47:45 +0200 Subject: [PATCH 105/109] Always save address of a deployed contract in the valueSet (#488) --- fuzzing/fuzzer_worker.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fuzzing/fuzzer_worker.go b/fuzzing/fuzzer_worker.go index dcac1d0f..8ddeb78c 100644 --- a/fuzzing/fuzzer_worker.go +++ b/fuzzing/fuzzer_worker.go @@ -159,6 +159,8 @@ func (fw *FuzzerWorker) getNewCorpusCallSequenceWeight() *big.Int { func (fw *FuzzerWorker) onChainContractDeploymentAddedEvent(event chain.ContractDeploymentsAddedEvent) error { // Do not track the deployed contract if the contract deployment was a dynamic one and testAllContracts is false if !fw.fuzzer.config.Fuzzing.Testing.TestAllContracts && event.DynamicDeployment { + // Add the contract address to our value set so our generator can use it in calls. + fw.valueSet.AddAddress(event.Contract.Address) return nil } From e3120eaa182470e81901629c522d6c7ee5c46065 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 9 Oct 2024 12:09:02 -0500 Subject: [PATCH 106/109] Update CODEOWNERS (#490) --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 85f87d99..a0108738 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @Xenomega @anishnaik @0xalpharush +* @Xenomega @anishnaik From 47fdfd8b74ef25449d7eb96829e71c8a1abbe442 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Tue, 15 Oct 2024 15:14:44 -0400 Subject: [PATCH 107/109] update version (#492) --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index c08518b4..b4d122d9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.6" +const version = "0.1.7" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From c58a72f2f9072c7b31d6a16f4771449e00607e4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:08:24 -0400 Subject: [PATCH 108/109] Fix sigstore dependency and update to v0.1.8 * Bump sigstore/gh-action-sigstore-python from 2.1.1 to 3.0.0 Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases) - [Changelog](https://github.com/sigstore/gh-action-sigstore-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v2.1.1...v3.0.0) --- updated-dependencies: - dependency-name: sigstore/gh-action-sigstore-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * update to v0.1.8 * update to new json format --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: anishnaik --- .github/workflows/ci.yml | 6 +++--- cmd/root.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40c9fbe8..6983fbc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: - name: Sign artifact if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: ./medusa-*.tar.gz @@ -92,7 +92,7 @@ jobs: name: medusa-${{ runner.os }}-${{ runner.arch }} path: | ./medusa-*.tar.gz - ./medusa-*.tar.gz.sigstore + ./medusa-*.tar.gz.sigstore.json release: needs: [build] @@ -117,7 +117,7 @@ jobs: name: "${{ github.ref_name }}" files: | ./medusa-*.tar.gz - ./medusa-*.tar.gz.sigstore + ./medusa-*.tar.gz.sigstore.json lint: runs-on: ubuntu-latest diff --git a/cmd/root.go b/cmd/root.go index b4d122d9..6e710b61 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,7 +7,7 @@ import ( "os" ) -const version = "0.1.7" +const version = "0.1.8" // rootCmd represents the root CLI command object which all other commands stem from. var rootCmd = &cobra.Command{ From 878b0f18375db3812f3e55078b14ff943924d933 Mon Sep 17 00:00:00 2001 From: anishnaik Date: Wed, 16 Oct 2024 16:40:25 -0400 Subject: [PATCH 109/109] update coverage reports documentation (#493) --- docs/src/coverage_reports.md | 44 ---------------------- docs/src/project_configuration/overview.md | 2 +- docs/src/testing/coverage_reports.md | 42 +++++++++++++++++++++ 3 files changed, 43 insertions(+), 45 deletions(-) delete mode 100644 docs/src/coverage_reports.md diff --git a/docs/src/coverage_reports.md b/docs/src/coverage_reports.md deleted file mode 100644 index 10920207..00000000 --- a/docs/src/coverage_reports.md +++ /dev/null @@ -1,44 +0,0 @@ -# Coverage Reports - -## Generating HTML Report from LCOV - -Enable coverage reporting by setting the `corpusDirectory` key in the configuration file and setting the `coverageReports` key to `["lcov", "html"]`. - -```json -{ - "corpusDirectory": "corpus", - "coverageReports": ["lcov", "html"] -} -``` - -### Install lcov and genhtml - -Linux: - -```bash -apt-get install lcov -``` - -MacOS: - -```bash -brew install lcov -``` - -### Generate LCOV Report - -```bash - -genhtml corpus/coverage/lcov.info --output-dir corpus --rc derive_function_end_line=0 -``` - -> [!WARNING] -> ** The `derive_function_end_line` flag is required to prevent the `genhtml` tool from crashing when processing the Solidity source code. ** - -Open the `corpus/index.html` file in your browser or follow the steps to use VSCode below. - -### View Coverage Report in VSCode with Coverage Gutters - -Install the [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension. - -Then, right click in a project file and select `Coverage Gutters: Display Coverage`. diff --git a/docs/src/project_configuration/overview.md b/docs/src/project_configuration/overview.md index 898c6ee1..972bd0c7 100644 --- a/docs/src/project_configuration/overview.md +++ b/docs/src/project_configuration/overview.md @@ -43,7 +43,7 @@ value to `10_000` or `100_000` depending on the use case. You can learn more abo Updating the corpus directory is optional but recommended. The corpus directory determines where corpus items should be stored on disk. A corpus item is a sequence of transactions that increased `medusa`'s coverage of the system. Thus, these corpus items are valuable to store so that they can be re-used for the next fuzzing campaign. Additionally, the directory -will also hold [coverage reports](../coverage_reports.md) which is a valuable tool for debugging and validation. For most cases, you may set +will also hold [coverage reports](../testing/coverage_reports.md) which is a valuable tool for debugging and validation. For most cases, you may set `corpusDirectory`'s value to "corpus". This will create a `corpus/` directory in the same directory as the `medusa.json` file. You can learn more about this option [here](./fuzzing_config.md#corpusdirectory). diff --git a/docs/src/testing/coverage_reports.md b/docs/src/testing/coverage_reports.md index 408f8e18..c0441625 100644 --- a/docs/src/testing/coverage_reports.md +++ b/docs/src/testing/coverage_reports.md @@ -1 +1,43 @@ ## Coverage Reports + +### Generating HTML Report from LCOV + +Enable coverage reporting by setting the `corpusDirectory` key in the configuration file and setting the `coverageReports` key to `["lcov", "html"]`. + +```json +{ + "corpusDirectory": "corpus", + "coverageReports": ["lcov", "html"] +} +``` + +### Install lcov and genhtml + +Linux: + +```bash +apt-get install lcov +``` + +MacOS: + +```bash +brew install lcov +``` + +### Generate LCOV Report + +```bash +genhtml corpus/coverage/lcov.info --output-dir corpus --rc derive_function_end_line=0 +``` + +> [!WARNING] +> ** The `derive_function_end_line` flag is required to prevent the `genhtml` tool from crashing when processing the Solidity source code. ** + +Open the `corpus/index.html` file in your browser or follow the steps to use VSCode below. + +### View Coverage Report in VSCode with Coverage Gutters + +Install the [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension. + +Then, right click in a project file and select `Coverage Gutters: Display Coverage`.