diff --git a/README.md b/README.md index 0a2e6f18..ca5ed170 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ $ make geth-loadtest You can view the state of the chain using `polycli`. ```bash -$ polycli monitor http://127.0.0.1:8545 +$ polycli monitor --rpc-url http://127.0.0.1:8545 ``` ![polycli monitor](doc/assets/monitor.gif) @@ -145,7 +145,7 @@ true You can then generate some load to make sure that blocks with transactions are being created. Note that the chain id of local geth is `1337`. ```bash -$ polycli loadtest --verbosity 700 --chain-id 1337 --concurrency 1 --requests 1000 --rate-limit 5 --mode c http://127.0.0.1:8545 +$ polycli loadtest --verbosity 700 --chain-id 1337 --concurrency 1 --requests 1000 --rate-limit 5 --mode c --rpc-url http://127.0.0.1:8545 ``` # Contributing diff --git a/cmd/loadtest/app.go b/cmd/loadtest/app.go index a3118834..e2c529e9 100644 --- a/cmd/loadtest/app.go +++ b/cmd/loadtest/app.go @@ -3,18 +3,16 @@ package loadtest import ( "crypto/ecdsa" _ "embed" - "errors" "fmt" "math/big" "math/rand" - "net/url" "sync" "time" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/maticnetwork/polygon-cli/rpctypes" + "github.com/maticnetwork/polygon-cli/util" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" "golang.org/x/time/rate" ) @@ -38,6 +36,7 @@ type ( } loadTestParams struct { // inputs + RPCUrl *string Requests *int64 Concurrency *int64 BatchSize *uint64 @@ -45,7 +44,6 @@ type ( ToRandom *bool CallOnly *bool CallOnlyLatestBlock *bool - URL *url.URL ChainID *uint64 PrivateKey *string ToAddress *string @@ -156,43 +154,41 @@ var ( // LoadtestCmd represents the loadtest command var LoadtestCmd = &cobra.Command{ - Use: "loadtest url", + Use: "loadtest", Short: "Run a generic load test against an Eth/EVM style JSON-RPC endpoint.", Long: loadtestUsage, - RunE: func(cmd *cobra.Command, args []string) error { - err := runLoadTest(cmd.Context()) - if err != nil { - return err - } - return nil - }, - Args: func(cmd *cobra.Command, args []string) error { + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { zerolog.DurationFieldUnit = time.Second zerolog.DurationFieldInteger = true - if len(args) != 1 { - return errors.New("expected exactly one argument") - } - url, err := url.Parse(args[0]) - if err != nil { - log.Error().Err(err).Msg("Unable to parse url input error") - return err - } - if url.Scheme != "http" && url.Scheme != "https" && url.Scheme != "ws" && url.Scheme != "wss" { - return fmt.Errorf("the scheme %s is not supported", url.Scheme) - } - inputLoadTestParams.URL = url + return checkLoadtestFlags() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runLoadTest(cmd.Context()) + }, +} - if *inputLoadTestParams.AdaptiveBackoffFactor <= 0.0 { - return errors.New("the backoff factor needs to be non-zero positive") - } +func checkLoadtestFlags() error { + ltp := inputLoadTestParams - if *inputLoadTestParams.ContractCallBlockInterval == 0 { - return errors.New("the contract call block interval must be strictly positive") - } + // Check `rpc-url` flag. + if ltp.RPCUrl == nil { + panic("RPC URL is empty") + } + if err := util.ValidateUrl(*ltp.RPCUrl); err != nil { + return err + } - return nil - }, + if ltp.AdaptiveBackoffFactor != nil && *ltp.AdaptiveBackoffFactor <= 0.0 { + return fmt.Errorf("the backoff factor needs to be non-zero positive") + } + + if ltp.ContractCallBlockInterval != nil && *ltp.ContractCallBlockInterval == 0 { + return fmt.Errorf("the contract call block interval must be strictly positive") + } + + return nil } func init() { @@ -204,6 +200,7 @@ func initFlags() { ltp := new(loadTestParams) // Persistent flags. + ltp.RPCUrl = LoadtestCmd.PersistentFlags().StringP("rpc-url", "r", "http://localhost:8545", "The RPC endpoint url") ltp.Requests = LoadtestCmd.PersistentFlags().Int64P("requests", "n", 1, "Number of requests to perform for the benchmarking session. The default is to just perform a single request which usually leads to non-representative benchmarking results.") ltp.Concurrency = LoadtestCmd.PersistentFlags().Int64P("concurrency", "c", 1, "Number of requests to perform concurrently. Default is one request at a time.") ltp.TimeLimit = LoadtestCmd.PersistentFlags().Int64P("time-limit", "t", -1, "Maximum number of seconds to spend for benchmarking. Use this to benchmark within a fixed total amount of time. Per default there is no time limit.") diff --git a/cmd/loadtest/loadtest.go b/cmd/loadtest/loadtest.go index f79f4f14..a438a5d4 100644 --- a/cmd/loadtest/loadtest.go +++ b/cmd/loadtest/loadtest.go @@ -344,7 +344,7 @@ func runLoadTest(ctx context.Context) error { overallTimer = new(time.Timer) } - rpc, err := ethrpc.DialContext(ctx, inputLoadTestParams.URL.String()) + rpc, err := ethrpc.DialContext(ctx, *inputLoadTestParams.RPCUrl) if err != nil { log.Error().Err(err).Msg("Unable to dial rpc") return err diff --git a/cmd/loadtest/loadtestUsage.md b/cmd/loadtest/loadtestUsage.md index 7b62205a..a2fd04b4 100644 --- a/cmd/loadtest/loadtestUsage.md +++ b/cmd/loadtest/loadtestUsage.md @@ -55,13 +55,13 @@ The default private key is: `42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258 Here is a simple example that runs 1000 requests at a max rate of 1 request per second against the http rpc endpoint on localhost. It's running in transaction mode so it will perform simple transactions send to the default address. ```bash -$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 1000 --rate-limit 1 --mode t http://localhost:8888 +$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 1000 --rate-limit 1 --mode t --rpc-url http://localhost:8888 ``` Another example, a bit slower, and that specifically calls the [LOG4](https://www.evm.codes/#a4) function in the load test contract in a loop for 25,078 iterations. That number was picked specifically to require almost all of the gas for a single transaction. ```bash -$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 50 --rate-limit 0.5 --mode f --function 164 --iterations 25078 http://private.validator-001.devnet02.pos-v3.polygon.private:8545 +$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 50 --rate-limit 0.5 --mode f --function 164 --iterations 25078 --rpc-url http://private.validator-001.devnet02.pos-v3.polygon.private:8545 ``` ### Load Test Contract @@ -74,6 +74,6 @@ The codebase has a contract that used for load testing. It's written in Yul and 3. Run `abigen` - `$ abigen --abi LoadTester.abi --pkg contracts --type LoadTester --bin LoadTester.bin --out loadtester.go` 4. Run the loadtester to enure it deploys and runs successfully - - `$ polycli loadtest --verbosity 700 http://127.0.0.1:8541` + - `$ polycli loadtest --verbosity 700 --rpc-url http://127.0.0.1:8541` ``` diff --git a/cmd/loadtest/uniswapv3.go b/cmd/loadtest/uniswapv3.go index 2d0116e4..133bd2ef 100644 --- a/cmd/loadtest/uniswapv3.go +++ b/cmd/loadtest/uniswapv3.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "math/big" - "net/url" "time" "github.com/spf13/cobra" @@ -16,7 +15,6 @@ import ( "github.com/ethereum/go-ethereum/ethclient" uniswapv3loadtest "github.com/maticnetwork/polygon-cli/cmd/loadtest/uniswapv3" "github.com/maticnetwork/polygon-cli/contracts/uniswapv3" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -30,8 +28,9 @@ var uniswapV3LoadTestCmd = &cobra.Command{ Use: "uniswapv3 url", Short: "Run Uniswapv3-like load test against an Eth/EVm style JSON-RPC endpoint.", Long: uniswapv3Usage, + Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { - return checkFlags() + return checkUniswapV3LoadtestFlags() }, RunE: func(cmd *cobra.Command, args []string) error { // Override root command `mode` flag. @@ -44,25 +43,9 @@ var uniswapV3LoadTestCmd = &cobra.Command{ } return nil }, - Args: func(cmd *cobra.Command, args []string) error { - zerolog.DurationFieldUnit = time.Second - zerolog.DurationFieldInteger = true - - if len(args) != 1 { - return errors.New("expected exactly one argument") - } - - url, err := validateUrl(args[0]) - if err != nil { - return err - } - inputLoadTestParams.URL = url - - return nil - }, } -func checkFlags() error { +func checkUniswapV3LoadtestFlags() error { // Check pool fees. switch fees := *uniswapv3LoadTestParams.PoolFees; fees { case float64(uniswapv3loadtest.StableTier), float64(uniswapv3loadtest.StandardTier), float64(uniswapv3loadtest.ExoticTier): @@ -79,24 +62,6 @@ func checkFlags() error { return nil } -func validateUrl(input string) (*url.URL, error) { - url, err := url.Parse(input) - if err != nil { - log.Error().Err(err).Msg("Unable to parse url input error") - return nil, err - } - - if url.Scheme == "" { - return nil, errors.New("the scheme has not been specified") - } - switch url.Scheme { - case "http", "https", "ws", "wss": - return url, nil - default: - return nil, fmt.Errorf("the scheme %s is not supported", url.Scheme) - } -} - type params struct { UniswapFactoryV3, UniswapMulticall, UniswapProxyAdmin, UniswapTickLens, UniswapNFTLibDescriptor, UniswapNonfungibleTokenPositionDescriptor, UniswapUpgradeableProxy, UniswapNonfungiblePositionManager, UniswapMigrator, UniswapStaker, UniswapQuoterV2, UniswapSwapRouter, WETH9, UniswapPoolToken0, UniswapPoolToken1 *string PoolFees *float64 diff --git a/cmd/loadtest/uniswapv3Usage.md b/cmd/loadtest/uniswapv3Usage.md index 46301cd2..e9e62113 100644 --- a/cmd/loadtest/uniswapv3Usage.md +++ b/cmd/loadtest/uniswapv3Usage.md @@ -3,13 +3,13 @@ The `uniswapv3` command is a subcommand of the `loadtest` tool. It is meant to g You can either chose to deploy the full UniswapV3 contract suite. ```sh -polycli loadtest uniswapv3 http://localhost:8545 +polycli loadtest uniswapv3 ``` Or to use pre-deployed contracts to speed up the process. ```bash -polycli loadtest uniswapv3 http://localhost:8545 \ +polycli loadtest uniswapv3 \ --uniswap-factory-v3-address 0xc5f46e00822c828e1edcc12cf98b5a7b50c9e81b \ --uniswap-migrator-address 0x24951726c5d22a3569d5474a1e74734a09046cd9 \ --uniswap-multicall-address 0x0e695f36ade2a12abea51622e80f105e125d1d6e \ diff --git a/cmd/monitor/cmd.go b/cmd/monitor/cmd.go new file mode 100644 index 00000000..2f57cd04 --- /dev/null +++ b/cmd/monitor/cmd.go @@ -0,0 +1,70 @@ +package monitor + +import ( + _ "embed" + "fmt" + "strconv" + "time" + + "github.com/maticnetwork/polygon-cli/util" + "github.com/spf13/cobra" +) + +var ( + //go:embed usage.md + usage string + + // flags + rpcUrl string + batchSizeValue string + intervalStr string +) + +// MonitorCmd represents the monitor command +var MonitorCmd = &cobra.Command{ + Use: "monitor", + Short: "Monitor blocks using a JSON-RPC endpoint.", + Long: usage, + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + return checkFlags() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return monitor(cmd.Context()) + }, +} + +func init() { + MonitorCmd.PersistentFlags().StringVarP(&rpcUrl, "rpc-url", "r", "http://localhost:8545", "The RPC endpoint url") + MonitorCmd.PersistentFlags().StringVarP(&batchSizeValue, "batch-size", "b", "auto", "Number of requests per batch") + MonitorCmd.PersistentFlags().StringVarP(&intervalStr, "interval", "i", "5s", "Amount of time between batch block rpc calls") +} + +func checkFlags() (err error) { + // Check rpc-url flag. + if err = util.ValidateUrl(rpcUrl); err != nil { + return + } + + // Check interval duration flag. + interval, err = time.ParseDuration(intervalStr) + if err != nil { + return err + } + + // Check batch-size flag. + if batchSizeValue == "auto" { + batchSize = -1 + } else { + batchSize, err = strconv.Atoi(batchSizeValue) + if batchSize == 0 { + return fmt.Errorf("batch-size can't be equal to zero") + } + if err != nil { + // Failed to convert to int, handle the error + return fmt.Errorf("batch-size needs to be an integer") + } + } + + return nil +} diff --git a/cmd/monitor/monitor.go b/cmd/monitor/monitor.go index 6159d469..82c7c8cf 100644 --- a/cmd/monitor/monitor.go +++ b/cmd/monitor/monitor.go @@ -3,14 +3,13 @@ package monitor import ( "context" "fmt" - "github.com/maticnetwork/polygon-cli/util" "math/big" - "net/url" "sort" - "strconv" "sync" "time" + "github.com/maticnetwork/polygon-cli/util" + _ "embed" "github.com/ethereum/go-ethereum/ethclient" @@ -22,25 +21,17 @@ import ( "github.com/maticnetwork/polygon-cli/metrics" "github.com/maticnetwork/polygon-cli/rpctypes" "github.com/rs/zerolog/log" - "github.com/spf13/cobra" ) var ( - //go:embed usage.md - usage string - windowSize int - batchSizeValue string - batchSize int - intervalStr string - interval time.Duration - - one = big.NewInt(1) - zero = big.NewInt(0) - selectedBlock rpctypes.PolyBlock - + windowSize int + batchSize int + interval time.Duration + one = big.NewInt(1) + zero = big.NewInt(0) + selectedBlock rpctypes.PolyBlock currentlyFetchingHistoryLock sync.RWMutex - - observedPendingTxs historicalRange + observedPendingTxs historicalRange ) type ( @@ -91,6 +82,47 @@ const ( monitorModeBlock ) +func monitor(ctx context.Context) error { + rpc, err := ethrpc.DialContext(ctx, rpcUrl) + if err != nil { + log.Error().Err(err).Msg("Unable to dial rpc") + return err + } + ec := ethclient.NewClient(rpc) + + ms := new(monitorStatus) + ms.MaxBlockRetrieved = big.NewInt(0) + ms.BlocksLock.Lock() + ms.Blocks = make(map[string]rpctypes.PolyBlock, 0) + ms.BlocksLock.Unlock() + ms.ChainID = big.NewInt(0) + ms.PendingCount = 0 + observedPendingTxs = make(historicalRange, 0) + + isUiRendered := false + errChan := make(chan error) + go func() { + for { + err = fetchBlocks(ctx, ec, ms, rpc, isUiRendered) + if err != nil { + continue + } + + if !isUiRendered { + go func() { + errChan <- renderMonitorUI(ctx, ec, ms, rpc) + }() + isUiRendered = true + } + + time.Sleep(interval) + } + }() + + err = <-errChan + return err +} + func getChainState(ctx context.Context, ec *ethclient.Client) (*chainState, error) { var err error cs := new(chainState) @@ -244,85 +276,6 @@ func shouldLoadMoreHistory(ctx context.Context, ms *monitorStatus) bool { return true } -// monitorCmd represents the monitor command -var MonitorCmd = &cobra.Command{ - Use: "monitor url", - Short: "Monitor blocks using a JSON-RPC endpoint.", - Long: usage, - Args: cobra.MinimumNArgs(1), - PreRunE: func(cmd *cobra.Command, args []string) error { - // validate url argument - _, err := url.Parse(args[0]) - if err != nil { - return err - } - - // validate interval duration - if interval, err = time.ParseDuration(intervalStr); err != nil { - return err - } - - // validate batch-size flag - if batchSizeValue == "auto" { - batchSize = -1 - } else { - batchSize, err = strconv.Atoi(batchSizeValue) - if batchSize == 0 { - return fmt.Errorf("batch-size can't be equal to zero") - } - if err != nil { - // Failed to convert to int, handle the error - return fmt.Errorf("batch-size needs to be an integer") - } - } - - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - rpc, err := ethrpc.DialContext(ctx, args[0]) - if err != nil { - log.Error().Err(err).Msg("Unable to dial rpc") - return err - } - ec := ethclient.NewClient(rpc) - - ms := new(monitorStatus) - - ms.MaxBlockRetrieved = big.NewInt(0) - ms.BlocksLock.Lock() - ms.Blocks = make(map[string]rpctypes.PolyBlock, 0) - ms.BlocksLock.Unlock() - ms.ChainID = big.NewInt(0) - ms.PendingCount = 0 - observedPendingTxs = make(historicalRange, 0) - - isUiRendered := false - errChan := make(chan error) - go func() { - for { - err = fetchBlocks(ctx, ec, ms, rpc, isUiRendered) - if err != nil { - continue - } - - if !isUiRendered { - go func() { - errChan <- renderMonitorUI(ctx, ec, ms, rpc) - }() - isUiRendered = true - } - - time.Sleep(interval) - } - }() - - err = <-errChan - return err - }, -} - func (ms *monitorStatus) getBlockRange(ctx context.Context, from, to *big.Int, rpc *ethrpc.Client) error { blms := make([]ethrpc.BatchElem, 0) for i := from; i.Cmp(to) != 1; i.Add(i, one) { @@ -369,11 +322,6 @@ func (ms *monitorStatus) getBlockRange(ctx context.Context, from, to *big.Int, r return nil } -func init() { - MonitorCmd.PersistentFlags().StringVarP(&batchSizeValue, "batch-size", "b", "auto", "Number of requests per batch") - MonitorCmd.PersistentFlags().StringVarP(&intervalStr, "interval", "i", "5s", "Amount of time between batch block rpc calls") -} - func setUISkeleton() (blockTable *widgets.List, grid *ui.Grid, blockGrid *ui.Grid, termUi uiSkeleton) { blockTable = widgets.NewList() blockTable.TextStyle = ui.NewStyle(ui.ColorWhite) diff --git a/cmd/rpcfuzz/cmd.go b/cmd/rpcfuzz/cmd.go new file mode 100644 index 00000000..38e3a12c --- /dev/null +++ b/cmd/rpcfuzz/cmd.go @@ -0,0 +1,105 @@ +package rpcfuzz + +import ( + _ "embed" + "fmt" + "regexp" + "strings" + + "github.com/ethereum/go-ethereum/crypto" + fuzz "github.com/google/gofuzz" + "github.com/maticnetwork/polygon-cli/cmd/rpcfuzz/argfuzz" + "github.com/maticnetwork/polygon-cli/util" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var ( + //go:embed usage.md + usage string + + // flags + rpcUrl *string + testPrivateHexKey *string + testContractAddress *string + testNamespaces *string + testFuzz *bool + testFuzzNum *int + seed *int64 + testOutputExportPath *string + testExportJson *bool + testExportCSV *bool + testExportMarkdown *bool + testExportHTML *bool +) + +var RPCFuzzCmd = &cobra.Command{ + Use: "rpcfuzz", + Short: "Continually run a variety of RPC calls and fuzzers.", + Long: usage, + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + return checkFlags() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runRpcFuzz(cmd.Context()) + }, +} + +func init() { + flagSet := RPCFuzzCmd.PersistentFlags() + + rpcUrl = flagSet.StringP("rpc-url", "r", "http://localhost:8545", "The RPC endpoint url") + testPrivateHexKey = flagSet.String("private-key", codeQualityPrivateKey, "The hex encoded private key that we'll use to sending transactions") + testContractAddress = flagSet.String("contract-address", "0x6fda56c57b0acadb96ed5624ac500c0429d59429", "The address of a contract that can be used for testing") + testNamespaces = flagSet.String("namespaces", fmt.Sprintf("eth,web3,net,debug,%s", rpcTestRawHTTPNamespace), "Comma separated list of rpc namespaces to test") + testFuzz = flagSet.Bool("fuzz", false, "Flag to indicate whether to fuzz input or not.") + testFuzzNum = flagSet.Int("fuzzn", 100, "Number of times to run the fuzzer per test.") + seed = flagSet.Int64("seed", 123456, "A seed for generating random values within the fuzzer") + testOutputExportPath = flagSet.String("export-path", "", "The directory export path of the output of the tests. Must pair this with either --json, --csv, --md, or --html") + testExportJson = flagSet.Bool("json", false, "Flag to indicate that output will be exported as a JSON.") + testExportCSV = flagSet.Bool("csv", false, "Flag to indicate that output will be exported as a CSV.") + testExportMarkdown = flagSet.Bool("md", false, "Flag to indicate that output will be exported as a Markdown.") + testExportHTML = flagSet.Bool("html", false, "Flag to indicate that output will be exported as a HTML.") + + argfuzz.SetSeed(seed) + + fuzzer = fuzz.New() + fuzzer.Funcs(argfuzz.FuzzRPCArgs) +} + +func checkFlags() (err error) { + // Check rpc-url flag. + if rpcUrl == nil { + panic("RPC URL is empty") + } + if err = util.ValidateUrl(*rpcUrl); err != nil { + return + } + + // Check private key flag. + privateKey, err := crypto.HexToECDSA(*testPrivateHexKey) + if err != nil { + log.Error().Err(err).Msg("Couldn't process the hex private key") + return err + } + ethAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + log.Info().Str("ethAddress", ethAddress.String()).Msg("Loaded private key") + + // Check namespace flag. + nsValidator := regexp.MustCompile("^[a-z0-9]*$") + rawNameSpaces := strings.Split(*testNamespaces, ",") + enabledNamespaces = make([]string, 0) + for _, ns := range rawNameSpaces { + if !nsValidator.MatchString(ns) { + return fmt.Errorf("the namespace %s is not valid", ns) + } + enabledNamespaces = append(enabledNamespaces, ns+"_") + } + log.Info().Strs("namespaces", enabledNamespaces).Msg("Enabling namespaces") + + testPrivateKey = privateKey + testEthAddress = ethAddress + + return nil +} diff --git a/cmd/rpcfuzz/rpcfuzz.go b/cmd/rpcfuzz/rpcfuzz.go index f132b7a0..edafd1a5 100644 --- a/cmd/rpcfuzz/rpcfuzz.go +++ b/cmd/rpcfuzz/rpcfuzz.go @@ -32,15 +32,12 @@ import ( ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" - ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/rpc" fuzz "github.com/google/gofuzz" - "github.com/maticnetwork/polygon-cli/cmd/rpcfuzz/argfuzz" "github.com/maticnetwork/polygon-cli/cmd/rpcfuzz/testreporter" "github.com/maticnetwork/polygon-cli/rpctypes" "github.com/rs/zerolog/log" - "github.com/spf13/cobra" "github.com/xeipuuv/gojsonschema" ) @@ -168,28 +165,13 @@ const ( ) var ( - //go:embed usage.md - usage string - testPrivateHexKey *string - testContractAddress *string testPrivateKey *ecdsa.PrivateKey testEthAddress ethcommon.Address - testNamespaces *string - testFuzz *bool - testFuzzNum *int - seed *int64 - testOutputExportPath *string - testExportJson *bool - testExportCSV *bool - testExportMarkdown *bool - testExportHTML *bool testAccountNonce uint64 testAccountNonceMutex sync.Mutex currentChainID *big.Int fuzzer *fuzz.Fuzzer - - enabledNamespaces []string - + enabledNamespaces []string // in the future allTests could be used to for // fuzzing.. E.g. loop over the various tests, and mutate the // Args before sending @@ -202,6 +184,83 @@ var ( testResultMutex sync.Mutex ) +func runRpcFuzz(ctx context.Context) error { + if *testOutputExportPath != "" && !*testExportJson && !*testExportCSV && !*testExportMarkdown && !*testExportHTML { + log.Warn().Msg("Setting --export-path must pair with a export type: --json, --csv, --md, or --html") + } + + rpcClient, err := rpc.DialContext(ctx, *rpcUrl) + if err != nil { + return err + } + nonce, err := GetTestAccountNonce(ctx, rpcClient) + if err != nil { + return err + } + chainId, err := GetCurrentChainID(ctx, rpcClient) + if err != nil { + return err + } + testAccountNonce = nonce + currentChainID = chainId + + log.Trace().Uint64("nonce", nonce).Uint64("chainid", chainId.Uint64()).Msg("Doing test setup") + setupTests(ctx, rpcClient) + + httpClient := &http.Client{} + wrappedHTTPClient := wrappedHttpClient{httpClient, *rpcUrl} + + for _, t := range allTests { + if !shouldRunTest(t) { + log.Trace().Str("name", t.GetName()).Str("method", t.GetMethod()).Msg("Skipping test") + continue + } + log.Trace().Str("name", t.GetName()).Str("method", t.GetMethod()).Msg("Running Test") + + currTestResult := CallRPCAndValidate(ctx, rpcClient, wrappedHTTPClient, t) + testResults.AddTestResult(currTestResult) + + if *testFuzz { + fuzzedTestsGroup.Add(1) + + log.Info().Str("method", t.GetMethod()).Msg("Running with fuzzed args") + go func(t RPCTest) { + defer fuzzedTestsGroup.Done() + currTestResult := CallRPCWithFuzzAndValidate(ctx, rpcClient, t) + testResultsCh <- currTestResult + }(t) + } + } + + go func() { + for currTestResult := range testResultsCh { + testResultMutex.Lock() + testResults.AddTestResult(currTestResult) + testResultMutex.Unlock() + } + }() + + fuzzedTestsGroup.Wait() + close(testResultsCh) + + testResults.GenerateTabularResult() + if *testExportJson { + testResults.ExportResultToJSON(filepath.Join(*testOutputExportPath, "output.json")) + } + if *testExportCSV { + testResults.ExportResultToCSV(filepath.Join(*testOutputExportPath, "output.csv")) + } + if *testExportMarkdown { + testResults.ExportResultToMarkdown(filepath.Join(*testOutputExportPath, "output.md")) + } + if *testExportHTML { + testResults.ExportResultToHTML(filepath.Join(*testOutputExportPath, "output.html")) + } + testResults.PrintTabularResult() + + return nil +} + // setupTests will add all of the `RPCTests` to the `allTests` slice. func setupTests(ctx context.Context, rpcClient *rpc.Client) { // cast rpc --rpc-url localhost:8545 net_version @@ -1911,121 +1970,6 @@ type wrappedHttpClient struct { url string } -var RPCFuzzCmd = &cobra.Command{ - Use: "rpcfuzz http://localhost:8545", - Short: "Continually run a variety of RPC calls and fuzzers.", - Long: usage, - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - if *testOutputExportPath != "" && !*testExportJson && !*testExportCSV && !*testExportMarkdown && !*testExportHTML { - log.Warn().Msg("Setting --export-path must pair with a export type: --json, --csv, --md, or --html") - } - - url := args[0] - rpcClient, err := rpc.DialContext(ctx, url) - if err != nil { - return err - } - nonce, err := GetTestAccountNonce(ctx, rpcClient) - if err != nil { - return err - } - chainId, err := GetCurrentChainID(ctx, rpcClient) - if err != nil { - return err - } - testAccountNonce = nonce - currentChainID = chainId - - log.Trace().Uint64("nonce", nonce).Uint64("chainid", chainId.Uint64()).Msg("Doing test setup") - setupTests(ctx, rpcClient) - - httpClient := &http.Client{} - wrappedHTTPClient := wrappedHttpClient{httpClient, url} - - for _, t := range allTests { - if !shouldRunTest(t) { - log.Trace().Str("name", t.GetName()).Str("method", t.GetMethod()).Msg("Skipping test") - continue - } - log.Trace().Str("name", t.GetName()).Str("method", t.GetMethod()).Msg("Running Test") - - currTestResult := CallRPCAndValidate(ctx, rpcClient, wrappedHTTPClient, t) - testResults.AddTestResult(currTestResult) - - if *testFuzz { - fuzzedTestsGroup.Add(1) - - log.Info().Str("method", t.GetMethod()).Msg("Running with fuzzed args") - go func(t RPCTest) { - defer fuzzedTestsGroup.Done() - currTestResult := CallRPCWithFuzzAndValidate(ctx, rpcClient, t) - testResultsCh <- currTestResult - }(t) - } - } - - go func() { - for currTestResult := range testResultsCh { - testResultMutex.Lock() - testResults.AddTestResult(currTestResult) - testResultMutex.Unlock() - } - }() - - fuzzedTestsGroup.Wait() - close(testResultsCh) - - testResults.GenerateTabularResult() - if *testExportJson { - testResults.ExportResultToJSON(filepath.Join(*testOutputExportPath, "output.json")) - } - if *testExportCSV { - testResults.ExportResultToCSV(filepath.Join(*testOutputExportPath, "output.csv")) - } - if *testExportMarkdown { - testResults.ExportResultToMarkdown(filepath.Join(*testOutputExportPath, "output.md")) - } - if *testExportHTML { - testResults.ExportResultToHTML(filepath.Join(*testOutputExportPath, "output.html")) - } - testResults.PrintTabularResult() - - return nil - }, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return fmt.Errorf("expected 1 argument, but got %d", len(args)) - } - - privateKey, err := ethcrypto.HexToECDSA(*testPrivateHexKey) - if err != nil { - log.Error().Err(err).Msg("Couldn't process the hex private key") - return err - } - - ethAddress := ethcrypto.PubkeyToAddress(privateKey.PublicKey) - log.Info().Str("ethAddress", ethAddress.String()).Msg("Loaded private key") - - nsValidator := regexp.MustCompile("^[a-z0-9]*$") - rawNameSpaces := strings.Split(*testNamespaces, ",") - enabledNamespaces = make([]string, 0) - for _, ns := range rawNameSpaces { - if !nsValidator.MatchString(ns) { - return fmt.Errorf("the namespace %s is not valid", ns) - } - enabledNamespaces = append(enabledNamespaces, ns+"_") - } - log.Info().Strs("namespaces", enabledNamespaces).Msg("enabling namespaces") - - testPrivateKey = privateKey - testEthAddress = ethAddress - - return nil - }, -} - func shouldRunTest(t RPCTest) bool { var testNamespace string switch t.(type) { @@ -2042,24 +1986,3 @@ func shouldRunTest(t RPCTest) bool { } return false } - -func init() { - flagSet := RPCFuzzCmd.PersistentFlags() - - testPrivateHexKey = flagSet.String("private-key", codeQualityPrivateKey, "The hex encoded private key that we'll use to sending transactions") - testContractAddress = flagSet.String("contract-address", "0x6fda56c57b0acadb96ed5624ac500c0429d59429", "The address of a contract that can be used for testing") - testNamespaces = flagSet.String("namespaces", fmt.Sprintf("eth,web3,net,debug,%s", rpcTestRawHTTPNamespace), "Comma separated list of rpc namespaces to test") - testFuzz = flagSet.Bool("fuzz", false, "Flag to indicate whether to fuzz input or not.") - testFuzzNum = flagSet.Int("fuzzn", 100, "Number of times to run the fuzzer per test.") - seed = flagSet.Int64("seed", 123456, "A seed for generating random values within the fuzzer") - testOutputExportPath = flagSet.String("export-path", "", "The directory export path of the output of the tests. Must pair this with either --json, --csv, --md, or --html") - testExportJson = flagSet.Bool("json", false, "Flag to indicate that output will be exported as a JSON.") - testExportCSV = flagSet.Bool("csv", false, "Flag to indicate that output will be exported as a CSV.") - testExportMarkdown = flagSet.Bool("md", false, "Flag to indicate that output will be exported as a Markdown.") - testExportHTML = flagSet.Bool("html", false, "Flag to indicate that output will be exported as a HTML.") - - argfuzz.SetSeed(seed) - - fuzzer = fuzz.New() - fuzzer.Funcs(argfuzz.FuzzRPCArgs) -} diff --git a/doc/polycli_loadtest.md b/doc/polycli_loadtest.md index 485c5b6a..366655a2 100644 --- a/doc/polycli_loadtest.md +++ b/doc/polycli_loadtest.md @@ -14,7 +14,7 @@ Run a generic load test against an Eth/EVM style JSON-RPC endpoint. ```bash -polycli loadtest url [flags] +polycli loadtest [flags] ``` ## Usage @@ -76,13 +76,13 @@ The default private key is: `42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258 Here is a simple example that runs 1000 requests at a max rate of 1 request per second against the http rpc endpoint on localhost. It's running in transaction mode so it will perform simple transactions send to the default address. ```bash -$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 1000 --rate-limit 1 --mode t http://localhost:8888 +$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 1000 --rate-limit 1 --mode t --rpc-url http://localhost:8888 ``` Another example, a bit slower, and that specifically calls the [LOG4](https://www.evm.codes/#a4) function in the load test contract in a loop for 25,078 iterations. That number was picked specifically to require almost all of the gas for a single transaction. ```bash -$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 50 --rate-limit 0.5 --mode f --function 164 --iterations 25078 http://private.validator-001.devnet02.pos-v3.polygon.private:8545 +$ polycli loadtest --verbosity 700 --chain-id 1256 --concurrency 1 --requests 50 --rate-limit 0.5 --mode f --function 164 --iterations 25078 --rpc-url http://private.validator-001.devnet02.pos-v3.polygon.private:8545 ``` ### Load Test Contract @@ -95,7 +95,7 @@ The codebase has a contract that used for load testing. It's written in Yul and 3. Run `abigen` - `$ abigen --abi LoadTester.abi --pkg contracts --type LoadTester --bin LoadTester.bin --out loadtester.go` 4. Run the loadtester to enure it deploys and runs successfully - - `$ polycli loadtest --verbosity 700 http://127.0.0.1:8541` + - `$ polycli loadtest --verbosity 700 --rpc-url http://127.0.0.1:8541` ``` @@ -144,6 +144,7 @@ The codebase has a contract that used for load testing. It's written in Yul and --rate-limit float An overall limit to the number of requests per second. Give a number less than zero to remove this limit all together (default 4) --recall-blocks uint The number of blocks that we'll attempt to fetch for recall (default 50) -n, --requests int Number of requests to perform for the benchmarking session. The default is to just perform a single request which usually leads to non-representative benchmarking results. (default 1) + -r, --rpc-url string The RPC endpoint url (default "http://localhost:8545") --seed int A seed for generating random values and addresses (default 123456) --send-amount string The amount of wei that we'll send every transaction (default "0x38D7EA4C68000") --steady-state-tx-pool-size uint When using adaptive rate limiting, this value sets the target queue size. If the queue is smaller than this value, we'll speed up. If the queue is smaller than this value, we'll back off. (default 1000) diff --git a/doc/polycli_loadtest_uniswapv3.md b/doc/polycli_loadtest_uniswapv3.md index 95f7308a..94a87c60 100644 --- a/doc/polycli_loadtest_uniswapv3.md +++ b/doc/polycli_loadtest_uniswapv3.md @@ -24,13 +24,13 @@ The `uniswapv3` command is a subcommand of the `loadtest` tool. It is meant to g You can either chose to deploy the full UniswapV3 contract suite. ```sh -polycli loadtest uniswapv3 http://localhost:8545 +polycli loadtest uniswapv3 ``` Or to use pre-deployed contracts to speed up the process. ```bash -polycli loadtest uniswapv3 http://localhost:8545 \ +polycli loadtest uniswapv3 \ --uniswap-factory-v3-address 0xc5f46e00822c828e1edcc12cf98b5a7b50c9e81b \ --uniswap-migrator-address 0x24951726c5d22a3569d5474a1e74734a09046cd9 \ --uniswap-multicall-address 0x0e695f36ade2a12abea51622e80f105e125d1d6e \ @@ -98,6 +98,7 @@ The command also inherits flags from parent commands. --private-key string The hex encoded private key that we'll use to send transactions (default "42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa") --rate-limit float An overall limit to the number of requests per second. Give a number less than zero to remove this limit all together (default 4) -n, --requests int Number of requests to perform for the benchmarking session. The default is to just perform a single request which usually leads to non-representative benchmarking results. (default 1) + -r, --rpc-url string The RPC endpoint url (default "http://localhost:8545") --seed int A seed for generating random values and addresses (default 123456) --send-amount string The amount of wei that we'll send every transaction (default "0x38D7EA4C68000") --steady-state-tx-pool-size uint When using adaptive rate limiting, this value sets the target queue size. If the queue is smaller than this value, we'll speed up. If the queue is smaller than this value, we'll back off. (default 1000) diff --git a/doc/polycli_monitor.md b/doc/polycli_monitor.md index 70a80007..5c31844f 100644 --- a/doc/polycli_monitor.md +++ b/doc/polycli_monitor.md @@ -14,7 +14,7 @@ Monitor blocks using a JSON-RPC endpoint. ```bash -polycli monitor url [flags] +polycli monitor [flags] ``` ## Usage @@ -31,6 +31,7 @@ If you're experiencing missing blocks, try adjusting the `--batch-size` and `--i -b, --batch-size string Number of requests per batch (default "auto") -h, --help help for monitor -i, --interval string Amount of time between batch block rpc calls (default "5s") + -r, --rpc-url string The RPC endpoint url (default "http://localhost:8545") ``` The command also inherits flags from parent commands. diff --git a/doc/polycli_rpcfuzz.md b/doc/polycli_rpcfuzz.md index 7ff7dc6a..da2c9bb3 100644 --- a/doc/polycli_rpcfuzz.md +++ b/doc/polycli_rpcfuzz.md @@ -14,7 +14,7 @@ Continually run a variety of RPC calls and fuzzers. ```bash -polycli rpcfuzz http://localhost:8545 [flags] +polycli rpcfuzz [flags] ``` ## Usage @@ -114,6 +114,7 @@ $ docker run -v $PWD/contracts:/contracts ethereum/solc:stable --storage-layout --md Flag to indicate that output will be exported as a Markdown. --namespaces string Comma separated list of rpc namespaces to test (default "eth,web3,net,debug,raw") --private-key string The hex encoded private key that we'll use to sending transactions (default "42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa") + -r, --rpc-url string The RPC endpoint url (default "http://localhost:8545") --seed int A seed for generating random values within the fuzzer (default 123456) ``` diff --git a/scripts/clients.mk b/scripts/clients.mk index e9a7b6b5..7ed95447 100644 --- a/scripts/clients.mk +++ b/scripts/clients.mk @@ -37,10 +37,10 @@ geth-loadtest: build fund ## Run loadtest against an EVM/Geth chain. sleep 5 $(BUILD_DIR)/$(BIN_NAME) loadtest \ --verbosity 700 \ + --rpc-url http://127.0.0.1:$(PORT) \ --chain-id 1337 \ + --mode random \ --concurrency 1 \ --requests 1000 \ --rate-limit 100 \ - --mode r \ - --legacy \ - http://127.0.0.1:$(PORT) + --legacy diff --git a/util/url.go b/util/url.go new file mode 100644 index 00000000..504f55e9 --- /dev/null +++ b/util/url.go @@ -0,0 +1,28 @@ +package util + +import ( + "errors" + "fmt" + "net/url" + + "github.com/rs/zerolog/log" +) + +// ValidateUrl checks if a string URL can be parsed and if it has a valid scheme. +func ValidateUrl(input string) error { + url, err := url.Parse(input) + if err != nil { + log.Error().Err(err).Msg("Unable to parse url input error") + return err + } + + if url.Scheme == "" { + return errors.New("the scheme has not been specified") + } + switch url.Scheme { + case "http", "https", "ws", "wss": + return nil + default: + return fmt.Errorf("the scheme '%s' is not supported", url.Scheme) + } +}