Skip to content

Commit

Permalink
feat(loadtest): 🦄 uniswapv3 loadtest mode (#137)
Browse files Browse the repository at this point in the history
* doc: document how to generate new loadtest mode string

* chore: build uniswap v3 contracts

* chore: remove avail cmds

* chore: generate go bindings for uniswap contracts

* doc: document how to build uniswap contracts and how to generate go bindings

* feat: add uniswap v3 loadtest mode

* doc: update doc

* chore: define deployment config for uniswap

* chore: lint

* feat: enable 1 bp fee tier

* chore: remove unused contracts and deploy multicall

* feat: deploy ProxyAdmin

* chore: clean up

* chore: make lint

* feat: deploy TickLens

* chore: minor change

* chore: refactor to use generics

* chore: clean up

* chore: rename lib contract

* feat: deploy NFTDescriptor and WETH9

* feat: deploy TransparentUpgradeableProxy

* chore: generate NonfungiblePositionManager go binding

* feat: deploy NonfungiblePositionManager contract

* chore: generate go binding for V3Migrator

* feat: deploy V3Migrator

* chore: raise geth gas limit and send more funds to the loadtest account

* chore: update build process

* chore: update contract bytecode

* feat: transfer factory ownership

* chore: generate go binding for UniswapV3Staker

* chore: update build process

* fix: build process

* feat: deploy staker

* chore: generate go bindings for QuoterV2

* feat: deploy QuoterV2

* chore: generate go bindings for SwapRouter02

* chore: clean up build process

* chore: update bindings

* chore: clean up

* fix: build issues

* chore: new bytecodes

* doc: update doc

* feat: deploy SwapRouter02

* feat: transfer ProxyAdmin ownership

* chore: update debug msgs

* fix: update owner address

* chore: clean up

* chore: forge init

* forge install: forge-std

v1.6.1

* chore: forge init

* forge install: forge-std

v1.6.1

* chore: add ERC20 contracts

* fix: remove quotes from ERC20 bytecode

* feat: generate go bindings for ERC20

* feat: deploy the two ERC20 contracts

* feat: approve erc20 spendings

* feat: create pool between the erc20 contracts

* chore: make sure pool is deployed before moving on

* chore: generate go binding for UniswapV3Pool

* feat: initialize, provied liquidity and swap tokens

* chore: clean up

* chore: lint

* doc: update `polycli loadtest` doc

* fix: build process

* chore: update sqrtX96price computation

* chore: refactor

* chore: new bytecode

* chore: remove unused code

* chore: minor update to build process

* chore: document script

* fix: shadowed vars

* chore: test remove artefacts

* chore: nit

* chore: only upload relevant v3-core contracts

* chore: remove all v3-periphery contracts

* chore: upload only relevant v3-periphery contracts

* chore: remove the rest of the contracts

* chore: only upload relevant contracts

* chore: update bytecode

* chore: build script name option change

* chore: bindings script nit

* chore: new bytecode

* chore: refactor

* chore: small nits

* chore: remove unused field

* feat: enable pre-deployed contract mode for uniswap

* fix: retrieve pool instead of creating a new one

* fix: issue with initializing a pool twice

* feat: add loadtest fn

* doc: gen

* chore: make lint

* chore: `go mod tidy`

* chore: update ERC20 contract

* chore: generate new bindings for ERC20

* chore: fix some issues with Swapper and ERC20

* chore: rename ERC20 contract to Swapper

* chore: fix the pool initialization issue

* chore: specify the amount of tokens to mint

* chore: rename contract

* chore: update build process

* chore: update `Swapper` contract

* chore: don't commit tmp/

* chore: hack to update the NonfungibleTokenPositionDescriptorMetaData bytecode

* chore: document how to update the deploy fn of NFTPositionDescriptor

* chore: add a warning in the script

* chore: more debug

* chore: rename TokenA and TokenB into Token0 and Token1

* chore: add debug

* chore: update debug msgs

* feat: move uniswapv3 loadtest to subcommand

* chore: nits

* chore: remove `fmt.Sprintf()`

* chore: clean up

* chore: rename contracts

* chore: update steps

* fix: proxy data

* chore: nit

* chore: clean up

* chore: update `uniswapv3` cmd usage

* chore: update build process

* chore: update binding process

* chore: save nft descriptor lib address to file

* chore: update nftdescriptor lib address

* chore: hack

* fix: issues

* chore: clean up

* chore: diplay tx hashes in trace mode

* chore: use swap-router v1.1.0 instead of v1.3.0

* fix: `blockUntilSuccessful` default case return a nil error

* fix: `TransparentUpgradeableProxy` deployment

* chore: nit

* chore: solve lint issue

* feat: adding more logging

* refactor: minor changes to the setup

* chore: nit

* chore: go mod tidy

* fix: swap issue

* chore: clean up

* chore: nit

* chore: update help msg

* chore: update help msg once again

* chore: add fn to validate url

* doc: document swap method

* doc: nit

* chore: rename swap method

* chore: nit

* doc: document `createPool`

* chore: refactor pool logic

* chore: swap file

* chore: same thing with deloy logic

* chore: same thing with swapper logic

* chore: go mod tidy

* chore: clean up loadtest

* chore: nit

* chore: gen doc

* fix: lint issues

* chore: nit

* chore: more nit

* chore: differentiat between local and persistent flags

* chore: gen doc

* chore: document `deploy.go`

* chore: nit

* chore: document `pool.go`

* lint: transform `fmt.Errorf` into `errors.New`

* chore: document `swap.go`

* chore: nit

* chore: document `swapper.go`

* chore: nit

* doc: document `types.go`

* chore: remove tx hash logs

* chore: nit

* chore: display name of spenders

* chore: nit

* chore: add ERC20 contract name

* chore: more nit

* chore: update `setUniswapV3Allowances` method

* feat: add flag to set the pool fee and the swap amount

* chore: check pool fees flag

* chore: nit

* chore: use fees as percentage as it's easier to type for users

* fix: conversion issue

* chore: gen-doc

* chore: update `Swapper.sol` and specify the amount of token to mint in polycli

* chore: lint

---------

Co-authored-by: John Hilliard <[email protected]>
  • Loading branch information
leovct and praetoriansentry authored Oct 20, 2023
1 parent 81c2bf9 commit 8a40b24
Show file tree
Hide file tree
Showing 76 changed files with 27,318 additions and 88 deletions.
40 changes: 4 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ We run a lot of different blockchain technologies. Different tools often have in
- [Features](#features)
- [Testing](#testing)
- [Contributing](#contributing)
- [References](#references)
- [Reference](#reference)

## Install

Expand Down Expand Up @@ -148,43 +148,11 @@ You can then generate some load to make sure that blocks with transactions are b
$ polycli loadtest --verbosity 700 --chain-id 1337 --concurrency 1 --requests 1000 --rate-limit 5 --mode c http://127.0.0.1:8545
```

## Contributing
# Contributing

The `Makefile` is here to assist you to build the project, run tests, generate documentation or go bindings, etc.
- If you add a new loadtest mode, don't forget to update the loadtest mode string by running the following command: `cd cmd/loadtest && stringer -type=loadTestMode`. You can install [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) with `go install golang.org/x/tools/cmd/stringer@latest`.

```sh
$ make
Usage:
make <target>
help Display this help.

Build
generate Generate protobuf stubs.
build Build go binary.
install Install the go binary.
cross Cross-compile go binaries using CGO.
simplecross Cross-compile go binaries without using CGO.
clean Clean the binary folder.

Test
test Run tests.

Generation
gen-doc Generate documentation for `polycli`.
gen-loadtest-modes Generate loadtest modes strings.
gen-go-bindings Generate go bindings for smart contracts.

Lint
lint Run linters.

Clients
geth Start a local geth node.
avail Start a local avail node.
geth-loadtest Fund test account with 5k ETH and run loadtest against an EVM/Geth chain.
avail-loadtest Run loadtest against an Avail chain.
```

## References
# Reference

Sending some value to the default load testing account.

Expand Down
67 changes: 41 additions & 26 deletions cmd/loadtest/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package loadtest
import (
"crypto/ecdsa"
_ "embed"
"errors"
"fmt"
"math/big"
"math/rand"
Expand Down Expand Up @@ -92,8 +93,8 @@ type (
)

var (
//go:embed usage.md
usage string
//go:embed loadtestUsage.md
loadtestUsage string
inputLoadTestParams loadTestParams
loadTestResults []loadTestSample
loadTestResutsMutex sync.RWMutex
Expand Down Expand Up @@ -157,7 +158,7 @@ var (
var LoadtestCmd = &cobra.Command{
Use: "loadtest url",
Short: "Run a generic load test against an Eth/EVM style JSON-RPC endpoint.",
Long: usage,
Long: loadtestUsage,
RunE: func(cmd *cobra.Command, args []string) error {
err := runLoadTest(cmd.Context())
if err != nil {
Expand All @@ -170,7 +171,7 @@ var LoadtestCmd = &cobra.Command{
zerolog.DurationFieldInteger = true

if len(args) != 1 {
return fmt.Errorf("expected exactly one argument")
return errors.New("expected exactly one argument")
}
url, err := url.Parse(args[0])
if err != nil {
Expand All @@ -183,20 +184,26 @@ var LoadtestCmd = &cobra.Command{
inputLoadTestParams.URL = url

if *inputLoadTestParams.AdaptiveBackoffFactor <= 0.0 {
return fmt.Errorf("the backoff factor needs to be non-zero positive")
return errors.New("the backoff factor needs to be non-zero positive")
}

if *inputLoadTestParams.ContractCallBlockInterval == 0 {
return fmt.Errorf("the contract call block interval must be strictly positive")
return errors.New("the contract call block interval must be strictly positive")
}

return nil
},
}

func init() {
initFlags()
initSubCommands()
}

func initFlags() {
ltp := new(loadTestParams)

// Persistent flags.
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.")
Expand All @@ -213,38 +220,46 @@ func init() {
ltp.AdaptiveRateLimitIncrement = LoadtestCmd.PersistentFlags().Uint64("adaptive-rate-limit-increment", 50, "When using adaptive rate limiting, this flag controls the size of the additive increases.")
ltp.AdaptiveCycleDuration = LoadtestCmd.PersistentFlags().Uint64("adaptive-cycle-duration-seconds", 10, "When using adaptive rate limiting, this flag controls how often we check the queue size and adjust the rates")
ltp.AdaptiveBackoffFactor = LoadtestCmd.PersistentFlags().Float64("adaptive-backoff-factor", 2, "When using adaptive rate limiting, this flag controls our multiplicative decrease value.")
ltp.Modes = LoadtestCmd.PersistentFlags().StringSliceP("mode", "m", []string{"t"}, `The testing mode to use. It can be multiple like: "t,c,d,f"
t - sending transactions
d - deploy contract
c - call random contract functions
f - call specific contract function
p - call random precompiled contracts
a - call a specific precompiled contract address
s - store mode
r - random modes
2 - ERC20 Transfers
7 - ERC721 Mints
R - total recall
rpc - call random rpc methods`)
ltp.Function = LoadtestCmd.PersistentFlags().Uint64P("function", "f", 1, "A specific function to be called if running with `--mode f` or a specific precompiled contract when running with `--mode a`")
ltp.Iterations = LoadtestCmd.PersistentFlags().Uint64P("iterations", "i", 1, "If we're making contract calls, this controls how many times the contract will execute the instruction in a loop. If we are making ERC721 Mints, this indicates the minting batch size")
ltp.ByteCount = LoadtestCmd.PersistentFlags().Uint64P("byte-count", "b", 1024, "If we're in store mode, this controls how many bytes we'll try to store in our contract")
ltp.Seed = LoadtestCmd.PersistentFlags().Int64("seed", 123456, "A seed for generating random values and addresses")
ltp.LtAddress = LoadtestCmd.PersistentFlags().String("lt-address", "", "The address of a pre-deployed load test contract")
ltp.ERC20Address = LoadtestCmd.PersistentFlags().String("erc20-address", "", "The address of a pre-deployed erc 20 contract")
ltp.ERC721Address = LoadtestCmd.PersistentFlags().String("erc721-address", "", "The address of a pre-deployed erc 721 contract")
ltp.ContractCallNumberOfBlocksToWaitFor = LoadtestCmd.PersistentFlags().Uint64("contract-call-nb-blocks-to-wait-for", 30, "The number of blocks to wait for before giving up on a contract deployment")
ltp.ContractCallBlockInterval = LoadtestCmd.PersistentFlags().Uint64("contract-call-block-interval", 1, "During deployment, this flag controls if we should check every block, every other block, or every nth block to determine that the contract has been deployed")
ltp.ForceContractDeploy = LoadtestCmd.PersistentFlags().Bool("force-contract-deploy", false, "Some load test modes don't require a contract deployment. Set this flag to true to force contract deployments. This will still respect the --lt-address flags.")
ltp.ForceGasLimit = LoadtestCmd.PersistentFlags().Uint64("gas-limit", 0, "In environments where the gas limit can't be computed on the fly, we can specify it manually. This can also be used to avoid eth_estimateGas")
ltp.ForceGasPrice = LoadtestCmd.PersistentFlags().Uint64("gas-price", 0, "In environments where the gas price can't be determined automatically, we can specify it manually")
ltp.ForcePriorityGasPrice = LoadtestCmd.PersistentFlags().Uint64("priority-gas-price", 0, "Specify Gas Tip Price in the case of EIP-1559")
ltp.ShouldProduceSummary = LoadtestCmd.PersistentFlags().Bool("summarize", false, "Should we produce an execution summary after the load test has finished. If you're running a large load test, this can take a long time")
ltp.BatchSize = LoadtestCmd.PersistentFlags().Uint64("batch-size", 999, "Number of batches to perform at a time for receipt fetching. Default is 999 requests at a time.")
ltp.SummaryOutputMode = LoadtestCmd.PersistentFlags().String("output-mode", "text", "Format mode for summary output (json | text)")
ltp.LegacyTransactionMode = LoadtestCmd.PersistentFlags().Bool("legacy", false, "Send a legacy transaction instead of an EIP1559 transaction.")
ltp.RecallLength = LoadtestCmd.PersistentFlags().Uint64("recall-blocks", 50, "The number of blocks that we'll attempt to fetch for recall")

// Local flags.
ltp.Modes = LoadtestCmd.Flags().StringSliceP("mode", "m", []string{"t"}, `The testing mode to use. It can be multiple like: "t,c,d,f"
t - sending transactions
d - deploy contract
c - call random contract functions
f - call specific contract function
p - call random precompiled contracts
a - call a specific precompiled contract address
s - store mode
r - random modes
2 - ERC20 transfers
7 - ERC721 mints
v3 - UniswapV3 swaps
R - total recall
rpc - call random rpc methods`)
ltp.Function = LoadtestCmd.Flags().Uint64P("function", "f", 1, "A specific function to be called if running with `--mode f` or a specific precompiled contract when running with `--mode a`")
ltp.ByteCount = LoadtestCmd.Flags().Uint64P("byte-count", "b", 1024, "If we're in store mode, this controls how many bytes we'll try to store in our contract")
ltp.LtAddress = LoadtestCmd.Flags().String("lt-address", "", "The address of a pre-deployed load test contract")
ltp.ERC20Address = LoadtestCmd.Flags().String("erc20-address", "", "The address of a pre-deployed ERC20 contract")
ltp.ERC721Address = LoadtestCmd.Flags().String("erc721-address", "", "The address of a pre-deployed ERC721 contract")
ltp.ForceContractDeploy = LoadtestCmd.Flags().Bool("force-contract-deploy", false, "Some load test modes don't require a contract deployment. Set this flag to true to force contract deployments. This will still respect the --lt-address flags.")
ltp.RecallLength = LoadtestCmd.Flags().Uint64("recall-blocks", 50, "The number of blocks that we'll attempt to fetch for recall")

inputLoadTestParams = *ltp

// TODO Compression
}

func initSubCommands() {
LoadtestCmd.AddCommand(uniswapV3LoadTestCmd)
}
50 changes: 42 additions & 8 deletions cmd/loadtest/loadtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import (
"sync"
"time"

uniswapv3loadtest "github.com/maticnetwork/polygon-cli/cmd/loadtest/uniswapv3"
"github.com/maticnetwork/polygon-cli/contracts"
"github.com/maticnetwork/polygon-cli/contracts/tokens"

"github.com/maticnetwork/polygon-cli/metrics"
"github.com/maticnetwork/polygon-cli/rpctypes"
"github.com/maticnetwork/polygon-cli/util"
Expand Down Expand Up @@ -51,6 +53,7 @@ const (
loadTestModeERC721
loadTestModePrecompiledContracts
loadTestModePrecompiledContract
loadTestModeUniswapV3

// All the modes AFTER random mode will not be used when mode random is selected
loadTestModeRandom
Expand All @@ -61,6 +64,8 @@ const (
codeQualityPrivateKey = "42b6e34dc21598a807dc19d7784c71b2a7a01f6480dc6f58258f78e539f1a1fa"
)

var errWaitingPeriodExhausted = errors.New("waiting period exhausted")

func characterToLoadTestMode(mode string) (loadTestMode, error) {
switch mode {
case "t", "transaction":
Expand All @@ -87,6 +92,8 @@ func characterToLoadTestMode(mode string) (loadTestMode, error) {
return loadTestModePrecompiledContracts, nil
case "R", "recall":
return loadTestModeRecall, nil
case "v3", "uniswapv3":
return loadTestModeUniswapV3, nil
case "rpc":
return loadTestModeRPC, nil
default:
Expand Down Expand Up @@ -222,7 +229,7 @@ func initializeLoadTestParams(ctx context.Context, c *ethclient.Client) error {

modes := *inputLoadTestParams.Modes
if len(modes) == 0 {
return fmt.Errorf("expected at least one mode")
return errors.New("expected at least one mode")
}

inputLoadTestParams.ParsedModes = make([]loadTestMode, 0)
Expand All @@ -242,18 +249,18 @@ func initializeLoadTestParams(ctx context.Context, c *ethclient.Client) error {
}

if hasMode(loadTestModeRandom, inputLoadTestParams.ParsedModes) && inputLoadTestParams.MultiMode {
return fmt.Errorf("random mode can't be used in combinations with any other modes")
return errors.New("random mode can't be used in combinations with any other modes")
}
if hasMode(loadTestModeRPC, inputLoadTestParams.ParsedModes) && inputLoadTestParams.MultiMode && !*inputLoadTestParams.CallOnly {
return fmt.Errorf("rpc mode must be called with call-only when multiple modes are used")
return errors.New("rpc mode must be called with call-only when multiple modes are used")
} else if hasMode(loadTestModeRPC, inputLoadTestParams.ParsedModes) {
log.Trace().Msg("setting call only mode since we're doing RPC testing")
*inputLoadTestParams.CallOnly = true
}
// TODO check for duplicate modes?

if *inputLoadTestParams.CallOnly && *inputLoadTestParams.AdaptiveRateLimit {
return fmt.Errorf("using call only with adaptive rate limit doesn't make sense")
return errors.New("using call only with adaptive rate limit doesn't make sense")
}

randSrc = rand.New(rand.NewSource(*inputLoadTestParams.Seed))
Expand Down Expand Up @@ -477,14 +484,38 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
log.Debug().Str("erc721Addr", erc721Addr.String()).Msg("Obtained erc 721 contract address")
}

uniswapAddresses := uniswapv3loadtest.UniswapV3Addresses{
FactoryV3: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapFactoryV3),
Multicall: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapMulticall),
ProxyAdmin: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapProxyAdmin),
TickLens: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapTickLens),
NFTDescriptorLib: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapNFTLibDescriptor),
NonfungibleTokenPositionDescriptor: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapNonfungibleTokenPositionDescriptor),
TransparentUpgradeableProxy: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapUpgradeableProxy),
NonfungiblePositionManager: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapNonfungiblePositionManager),
Migrator: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapMigrator),
Staker: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapStaker),
QuoterV2: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapQuoterV2),
SwapRouter02: ethcommon.HexToAddress(*uniswapv3LoadTestParams.UniswapSwapRouter),
WETH9: ethcommon.HexToAddress(*uniswapv3LoadTestParams.WETH9),
}
var uniswapV3Config uniswapv3loadtest.UniswapV3Config
var poolConfig uniswapv3loadtest.PoolConfig
if mode == loadTestModeUniswapV3 || mode == loadTestModeRandom {
uniswapV3Config, poolConfig, err = initUniswapV3Loadtest(ctx, c, tops, cops, uniswapAddresses, *ltp.FromETHAddress)
if err != nil {
return err
}
}

var recallTransactions []rpctypes.PolyTransaction
if mode == loadTestModeRecall {
recallTransactions, err = getRecallTransactions(ctx, c, rpc)
if err != nil {
return err
}
if len(recallTransactions) == 0 {
return fmt.Errorf("we weren't able to fetch any recall transactions")
return errors.New("we weren't able to fetch any recall transactions")
}
log.Debug().Int("txs", len(recallTransactions)).Msg("retrieved transactions for total recall")
}
Expand Down Expand Up @@ -570,6 +601,9 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
startReq, endReq, tErr = loadTestCallPrecompiledContracts(ctx, c, myNonceValue, ltContract, false)
case loadTestModeRecall:
startReq, endReq, tErr = loadTestRecall(ctx, c, myNonceValue, recallTransactions[int(currentNonce)%len(recallTransactions)])
case loadTestModeUniswapV3:
swapAmountIn := big.NewInt(int64(*uniswapv3LoadTestParams.SwapAmountInput))
startReq, endReq, tErr = runUniswapV3Loadtest(ctx, c, myNonceValue, uniswapV3Config, poolConfig, swapAmountIn)
case loadTestModeRPC:
startReq, endReq, tErr = loadTestRPC(ctx, c, myNonceValue, indexedActivity)
default:
Expand Down Expand Up @@ -676,7 +710,7 @@ func getERC20Contract(ctx context.Context, c *ethclient.Client, tops *bind.Trans
return err
}
if balance.Uint64() == 0 {
err = fmt.Errorf("ERC20 Balance is Zero")
err = errors.New("ERC20 Balance is Zero")
return err
}
return nil
Expand Down Expand Up @@ -750,8 +784,8 @@ func blockUntilSuccessful(ctx context.Context, c *ethclient.Client, f func() err
blockDiff = currentBlockNumber % currStartBlockNumber
}
if blockDiff > numberOfBlocksToWaitFor {
log.Error().Err(err).Dur("elapsedTimeSeconds", elapsed).Msg("Exhausted waiting period")
return err
log.Error().Err(err).Dur("elapsedTimeSeconds", elapsed).Msg("waiting period exhausted")
return errWaitingPeriodExhausted
}

currentBlockNumber, err = c.BlockNumber(ctx)
Expand Down
File renamed without changes.
11 changes: 6 additions & 5 deletions cmd/loadtest/loadtestmode_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8a40b24

Please sign in to comment.