Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simulator improvements #619

Merged
merged 16 commits into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions cmd/simulator/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package config

import (
"errors"
"fmt"
"strings"
"time"

Expand All @@ -26,6 +28,12 @@ const (
TimeoutKey = "timeout"
)

var (
ErrNoEndpoints = errors.New("must specify at least one endpoint")
ErrNoWorkers = errors.New("must specify non-zero number of workers")
ErrNoTxs = errors.New("must specify non-zero number of txs-per-worker")
)

type Config struct {
Endpoints []string `json:"endpoints"`
MaxFeeCap int64 `json:"max-fee-cap"`
Expand All @@ -36,8 +44,8 @@ type Config struct {
Timeout time.Duration `json:"timeout"`
}

func BuildConfig(v *viper.Viper) Config {
return Config{
func BuildConfig(v *viper.Viper) (Config, error) {
c := Config{
Endpoints: v.GetStringSlice(EndpointsKey),
MaxFeeCap: v.GetInt64(MaxFeeCapKey),
MaxTipCap: v.GetInt64(MaxTipCapKey),
Expand All @@ -46,6 +54,24 @@ func BuildConfig(v *viper.Viper) Config {
KeyDir: v.GetString(KeyDirKey),
Timeout: v.GetDuration(TimeoutKey),
}
if len(c.Endpoints) == 0 {
return c, ErrNoEndpoints
}
if c.Workers == 0 {
return c, ErrNoWorkers
}
if c.TxsPerWorker == 0 {
return c, ErrNoTxs
}
// Note: it's technically valid for the fee/tip cap to be 0, but cannot
// be less than 0.
if c.MaxFeeCap < 0 {
return c, fmt.Errorf("invalid max fee cap %d < 0", c.MaxFeeCap)
}
if c.MaxTipCap < 0 {
return c, fmt.Errorf("invalid max tip cap %d <= 0", c.MaxTipCap)
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
}
return c, nil
}

func BuildViper(fs *pflag.FlagSet, args []string) (*viper.Viper, error) {
Expand Down Expand Up @@ -80,12 +106,12 @@ func BuildFlagSet() *pflag.FlagSet {
func addSimulatorFlags(fs *pflag.FlagSet) {
fs.Bool(VersionKey, false, "Print the version and exit.")
fs.String(ConfigFilePathKey, "", "Specify the config path to use to load a YAML config for the simulator")
fs.StringSlice(EndpointsKey, []string{"ws://127.0.0.1:9650/ext/bc/C/ws"}, "Specify a comma separated list of RPC Websocket Endpoints")
fs.Int64(MaxFeeCapKey, 50, "Specify the maximum fee cap to use for transactions denominated in GWei")
fs.Int64(MaxTipCapKey, 1, "Specify the max tip cap for transactions denominated in GWei")
fs.Uint64(TxsPerWorkerKey, 100, "Specify the number of transactions to create per worker.")
fs.Int(WorkersKey, 1, "Specify the number of workers to create for the simulator.")
fs.StringSlice(EndpointsKey, []string{"ws://127.0.0.1:9650/ext/bc/C/ws"}, "Specify a comma separated list of RPC Websocket Endpoints (minimum of 1 endpoint)")
fs.Int64(MaxFeeCapKey, 50, "Specify the maximum fee cap to use for transactions denominated in GWei (must be > 0)")
fs.Int64(MaxTipCapKey, 1, "Specify the max tip cap for transactions denominated in GWei (must be >= 0)")
fs.Uint64(TxsPerWorkerKey, 100, "Specify the number of transactions to create per worker (must be > 0)")
fs.Int(WorkersKey, 1, "Specify the number of workers to create for the simulator (must be > 0).")
fs.String(KeyDirKey, ".simulator/keys", "Specify the directory to save private keys in (INSECURE: only use for testing)")
fs.Duration(TimeoutKey, 5*time.Minute, "Specify the timeout for the simulator to complete.")
fs.Duration(TimeoutKey, 5*time.Minute, "Specify the timeout for the simulator to complete (0 indicates no timeout).")
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
fs.String(LogLevelKey, "info", "Specify the log level to use in the simulator.")
}
30 changes: 24 additions & 6 deletions cmd/simulator/load/funder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ package load

import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"

"github.com/ava-labs/subnet-evm/cmd/simulator/key"
"github.com/ava-labs/subnet-evm/core/types"
"github.com/ava-labs/subnet-evm/ethclient"
"github.com/ava-labs/subnet-evm/params"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)

// DistributeFunds ensures that each address in keys has at least [minFundsPerAddr] by sending funds
// from the key with the highest starting balance.
// This function should never return a set of keys with length less than [numKeys]
// This function will never return a set of keys with length less than [numKeys]
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
func DistributeFunds(ctx context.Context, client ethclient.Client, keys []*key.Key, numKeys int, minFundsPerAddr *big.Int) ([]*key.Key, error) {
if len(keys) < numKeys {
return nil, fmt.Errorf("insufficient number of keys %d < %d", len(keys), numKeys)
Expand Down Expand Up @@ -67,10 +69,6 @@ func DistributeFunds(ctx context.Context, client ethclient.Client, keys []*key.K
if err != nil {
return nil, fmt.Errorf("failed to fetch chainID: %w", err)
}
nonce, err := client.NonceAt(ctx, maxFundsKey.Address, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch nonce of address %s: %w", maxFundsKey.Address, err)
}
gasFeeCap, err := client.EstimateBaseFee(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch estimated base fee: %w", err)
Expand All @@ -83,7 +81,27 @@ func DistributeFunds(ctx context.Context, client ethclient.Client, keys []*key.K

// Generate a sequence of transactions to distribute the required funds.
log.Info("Generating distribution transactions")
txs, err := GenerateFundDistributionTxSequence(maxFundsKey.PrivKey, chainID, signer, nonce, gasFeeCap, gasTipCap, needFundsAddrs, minFundsPerAddr)
addrs := make([]common.Address, len(needFundsAddrs))
copy(addrs, needFundsAddrs)
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
i := 0
txGenerator := func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error) {
tx, err := types.SignNewTx(key, signer, &types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: params.TxGas,
To: &addrs[i],
Data: nil,
Value: requiredFunds,
})
if err != nil {
return nil, err
}
i++
return tx, nil
}
txs, err := GenerateTxSequence(ctx, txGenerator, client, maxFundsKey.PrivKey, uint64(len(addrs)))
if err != nil {
return nil, fmt.Errorf("failed to generate fund distribution sequence from %s of length %d", maxFundsKey.Address, len(needFundsAddrs))
}
Expand Down
47 changes: 31 additions & 16 deletions cmd/simulator/load/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,21 @@ import (
"crypto/ecdsa"
"fmt"
"math/big"
"os"

"github.com/ava-labs/subnet-evm/cmd/simulator/config"
"github.com/ava-labs/subnet-evm/cmd/simulator/key"
"github.com/ava-labs/subnet-evm/core/types"
"github.com/ava-labs/subnet-evm/ethclient"
"github.com/ava-labs/subnet-evm/params"
"github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
)

// CreateLoader creates a WorkerGroup from [config] to perform the specified simulation.
func CreateLoader(ctx context.Context, config config.Config) (*WorkerGroup, error) {
// Construct the arguments for the load simulator
switch {
case len(config.Endpoints) == 0:
fmt.Printf("Must specify at least one clientURI\n")
os.Exit(1)
case len(config.Endpoints) < config.Workers:
if len(config.Endpoints) < config.Workers {
// Ensure there are at least [config.Workers] config.Endpoints by creating
// duplicates as needed.
for i := 0; len(config.Endpoints) < config.Workers; i++ {
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -58,6 +56,8 @@ func CreateLoader(ctx context.Context, config config.Config) (*WorkerGroup, erro
}
}

// Each address needs: params.GWei * MaxFeeCap * params.TxGas * TxsPerWorker total wei
// to fund gas for all of their transactions.
maxFeeCap := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(config.MaxFeeCap))
minFundsPerAddr := new(big.Int).Mul(maxFeeCap, big.NewInt(int64(config.TxsPerWorker*params.TxGas)))
log.Info("Distributing funds", "numTxsPerWorker", config.TxsPerWorker, "minFunds", minFundsPerAddr)
aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -76,25 +76,40 @@ func CreateLoader(ctx context.Context, config config.Config) (*WorkerGroup, erro
bigGwei := big.NewInt(params.GWei)
gasTipCap := new(big.Int).Mul(bigGwei, big.NewInt(config.MaxTipCap))
gasFeeCap := new(big.Int).Mul(bigGwei, big.NewInt(config.MaxFeeCap))

txSequences, err := GenerateTxSequences(ctx, clients[0], pks, gasFeeCap, gasTipCap, config.TxsPerWorker)
client := clients[0]
chainID, err := client.ChainID(ctx)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to fetch chainID: %w", err)
}
if len(clients) < config.Workers {
return nil, fmt.Errorf("less clients %d than requested workers %d", len(clients), config.Workers)
}
if len(senders) < config.Workers {
return nil, fmt.Errorf("less senders %d than requested workers %d", len(senders), config.Workers)
signer := types.LatestSignerForChainID(chainID)

txGenerator := func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error) {
addr := ethcrypto.PubkeyToAddress(key.PublicKey)
tx, err := types.SignNewTx(key, signer, &types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: params.TxGas,
To: &addr,
Data: nil,
Value: common.Big0,
})
if err != nil {
return nil, err
}
return tx, nil
}
if len(txSequences) < config.Workers {
return nil, fmt.Errorf("less txSequences %d than requested workers %d", len(txSequences), config.Workers)
txSequences, err := GenerateTxSequences(ctx, txGenerator, clients[0], pks, config.TxsPerWorker)
if err != nil {
return nil, err
}

aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
wg := NewWorkerGroup(clients[:config.Workers], senders[:config.Workers], txSequences[:config.Workers])
return wg, nil
}

// ExecuteLoader runs the load simulation specified by config.
func ExecuteLoader(ctx context.Context, config config.Config) error {
if config.Timeout > 0 {
var cancel context.CancelFunc
Expand Down
86 changes: 12 additions & 74 deletions cmd/simulator/load/tx_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ import (

"github.com/ava-labs/subnet-evm/core/types"
"github.com/ava-labs/subnet-evm/ethclient"
"github.com/ava-labs/subnet-evm/params"
"github.com/ethereum/go-ethereum/common"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
)

type CreateTxData func() *TxData
type CreateTx func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error)

type TxData struct {
To *common.Address
Expand All @@ -25,46 +24,16 @@ type TxData struct {
Gas uint64
}

aaronbuchwald marked this conversation as resolved.
Show resolved Hide resolved
func GenerateFundDistributionTxSequence(key *ecdsa.PrivateKey, chainID *big.Int, signer types.Signer, startingNonce uint64, gasFeeCap *big.Int, gasTipCap *big.Int, fundAddrs []common.Address, value *big.Int) ([]*types.Transaction, error) {
// Create a closure to use in GenerateTxSequence
addrs := make([]common.Address, len(fundAddrs))
copy(addrs, fundAddrs)
i := 0
return GenerateTxSequence(
key,
chainID,
signer,
startingNonce,
gasFeeCap,
gasTipCap,
func() *TxData {
data := &TxData{
To: &addrs[i],
Data: nil,
Value: new(big.Int).Add(common.Big0, value),
Gas: params.TxGas,
}
i++
return data
},
uint64(len(addrs)),
)
}

func GenerateTxSequence(key *ecdsa.PrivateKey, chainID *big.Int, signer types.Signer, startingNonce uint64, gasFeeCap *big.Int, gasTipCap *big.Int, generator CreateTxData, numTxs uint64) ([]*types.Transaction, error) {
// GenerateTxSequence fetches the current nonce of key and calls [generator] [numTxs] times sequentially to generate a sequence of transactions.
func GenerateTxSequence(ctx context.Context, generator CreateTx, client ethclient.Client, key *ecdsa.PrivateKey, numTxs uint64) ([]*types.Transaction, error) {
address := ethcrypto.PubkeyToAddress(key.PublicKey)
startingNonce, err := client.NonceAt(ctx, address, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch nonce for address %s: %w", address, err)
}
txs := make([]*types.Transaction, 0, numTxs)
for i := uint64(0); i < numTxs; i++ {
txData := generator()
tx, err := types.SignNewTx(key, signer, &types.DynamicFeeTx{
ChainID: chainID,
Nonce: startingNonce + i,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: txData.Gas,
To: txData.To,
Data: txData.Data,
Value: txData.Value,
})
tx, err := generator(key, startingNonce+i)
if err != nil {
return nil, fmt.Errorf("failed to sign tx at index %d: %w", i, err)
}
Expand All @@ -73,41 +42,10 @@ func GenerateTxSequence(key *ecdsa.PrivateKey, chainID *big.Int, signer types.Si
return txs, nil
}

func GenerateNoopTxSequence(key *ecdsa.PrivateKey, chainID *big.Int, signer types.Signer, startingNonce uint64, gasFeeCap *big.Int, gasTipCap *big.Int, address common.Address, numTxs uint64) ([]*types.Transaction, error) {
return GenerateTxSequence(
key,
chainID,
signer,
startingNonce,
gasFeeCap,
gasTipCap,
func() *TxData {
return &TxData{
To: &address,
Data: nil,
Value: common.Big0,
Gas: params.TxGas,
}
},
numTxs,
)
}

func GenerateTxSequences(ctx context.Context, client ethclient.Client, keys []*ecdsa.PrivateKey, gasFeeCap *big.Int, gasTipCap *big.Int, txsPerKey uint64) ([]types.Transactions, error) {
chainID, err := client.ChainID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get chainID: %w", err)
}
signer := types.LatestSignerForChainID(chainID)

txSequences := make([]types.Transactions, len(keys))
func GenerateTxSequences(ctx context.Context, generator CreateTx, client ethclient.Client, keys []*ecdsa.PrivateKey, txsPerKey uint64) ([][]*types.Transaction, error) {
txSequences := make([][]*types.Transaction, len(keys))
for i, key := range keys {
address := ethcrypto.PubkeyToAddress(key.PublicKey)
startingNonce, err := client.NonceAt(ctx, address, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch nonce for address %s at index %d: %w", address, i, err)
}
txs, err := GenerateNoopTxSequence(key, chainID, signer, startingNonce, gasFeeCap, gasTipCap, address, txsPerKey)
txs, err := GenerateTxSequence(ctx, generator, client, key, txsPerKey)
if err != nil {
return nil, fmt.Errorf("failed to generate tx sequence at index %d: %w", i, err)
}
Expand Down
Loading