Skip to content

Commit a6433a9

Browse files
authored
feat(loadtest): QoL changes (#892)
* feat(loadtest): parallel nonce fetching, gas units, stop on insufficient funds * fix: logging * chore: fix logs * fix: log fields * fix: rename flag * feat: `--private-txs` flag * fix: make gen * fix: validate private txs modes * feat: add --rpc-headers flag to loadtest command * fix: lint
1 parent 8ff6d0d commit a6433a9

49 files changed

Lines changed: 923 additions & 296 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/dbbench/dbbench.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ benchLoop:
390390
_, err := db.Get(tmpKey)
391391
keyLock.Unlock()
392392
if err != nil {
393-
log.Error().Str("key", hex.EncodeToString(tmpKey)).Err(err).Msg("db random read error")
393+
log.Error().Str("key", hex.EncodeToString(tmpKey)).Err(err).Msg("DB random read error")
394394
}
395395
wg.Done()
396396
<-pool

cmd/fixnoncegap/fixnoncegap.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func fixNonceGap(cmd *cobra.Command, args []string) error {
112112
var maxNonce uint64
113113
if inputFixNonceGapArgs.maxNonce != 0 {
114114
maxNonce = inputFixNonceGapArgs.maxNonce
115-
log.Info().Uint64("maxNonce", maxNonce).Msg("maxNonce loaded from --max-nonce flag")
115+
log.Info().Uint64("maxNonce", maxNonce).Msg("Loaded max nonce from --max-nonce flag")
116116
} else {
117117
log.Info().Msg("--max-nonce flag not set")
118118
maxNonce, err = getMaxNonce(addr)
@@ -124,7 +124,7 @@ func fixNonceGap(cmd *cobra.Command, args []string) error {
124124

125125
// check if there is a nonce gap
126126
if maxNonce == 0 || currentNonce >= maxNonce {
127-
log.Info().Stringer("addr", addr).Msg("There is no nonce gap.")
127+
log.Info().Stringer("addr", addr).Msg("There is no nonce gap")
128128
return nil
129129
}
130130
log.Info().
@@ -170,15 +170,15 @@ func fixNonceGap(cmd *cobra.Command, args []string) error {
170170
log.Error().Err(err).Msg("Unable to sign tx")
171171
return err
172172
}
173-
log.Info().Stringer("hash", signedTx.Hash()).Msgf("sending tx with nonce %d", txTemplate.Nonce)
173+
log.Info().Stringer("hash", signedTx.Hash()).Msgf("Sending transaction with nonce %d", txTemplate.Nonce)
174174

175175
err = rpcClient.SendTransaction(cmd.Context(), signedTx)
176176
if err != nil {
177177
if strings.Contains(err.Error(), "nonce too low") {
178-
log.Info().Stringer("hash", signedTx.Hash()).Msgf("another tx with nonce %d was mined while trying to increase the fee, skipping it", txTemplate.Nonce)
178+
log.Info().Stringer("hash", signedTx.Hash()).Msgf("Another transaction with nonce %d was mined while trying to increase the fee, skipping", txTemplate.Nonce)
179179
break out
180180
} else if strings.Contains(err.Error(), "already known") {
181-
log.Info().Stringer("hash", signedTx.Hash()).Msgf("same tx with nonce %d already exists, skipping it", txTemplate.Nonce)
181+
log.Info().Stringer("hash", signedTx.Hash()).Msgf("Same transaction with nonce %d already exists, skipping", txTemplate.Nonce)
182182
break out
183183
} else if strings.Contains(err.Error(), "replacement transaction underpriced") ||
184184
strings.Contains(err.Error(), "INTERNAL_ERROR: could not replace existing tx") {
@@ -195,11 +195,11 @@ func fixNonceGap(cmd *cobra.Command, args []string) error {
195195
txTemplateCopy.GasPrice = new(big.Int).Add(txTemplateCopy.GasPrice, big.NewInt(1))
196196
}
197197
tx = types.NewTx(&txTemplateCopy)
198-
log.Info().Stringer("hash", signedTx.Hash()).Msgf("tx with nonce %d is underpriced, increasing fee. From %d To %d", txTemplate.Nonce, oldGasPrice, txTemplateCopy.GasPrice)
198+
log.Info().Stringer("hash", signedTx.Hash()).Msgf("Transaction with nonce %d is underpriced, increasing fee from %d to %d", txTemplate.Nonce, oldGasPrice, txTemplateCopy.GasPrice)
199199
time.Sleep(time.Second)
200200
continue
201201
} else {
202-
log.Info().Stringer("hash", signedTx.Hash()).Msgf("another tx with nonce %d already exists, skipping it", txTemplate.Nonce)
202+
log.Info().Stringer("hash", signedTx.Hash()).Msgf("Another transaction with nonce %d already exists, skipping", txTemplate.Nonce)
203203
break out
204204
}
205205
}
@@ -214,7 +214,7 @@ func fixNonceGap(cmd *cobra.Command, args []string) error {
214214
}
215215

216216
if lastTx != nil {
217-
log.Info().Stringer("hash", lastTx.Hash()).Msg("waiting for the last tx to get mined")
217+
log.Info().Stringer("hash", lastTx.Hash()).Msg("Waiting for the last transaction to get mined")
218218
err := WaitMineTransaction(cmd.Context(), rpcClient, lastTx, 600)
219219
if err != nil {
220220
log.Error().Err(err).Msg("Unable to wait for last tx to get mined")
@@ -261,10 +261,10 @@ func WaitMineTransaction(ctx context.Context, client *ethclient.Client, tx *type
261261
continue
262262
}
263263
if r.Status != 0 {
264-
log.Info().Stringer("hash", r.TxHash).Msg("transaction successful")
264+
log.Info().Stringer("hash", r.TxHash).Msg("Transaction successful")
265265
return nil
266266
} else if r.Status == 0 {
267-
log.Error().Stringer("hash", r.TxHash).Msg("transaction failed")
267+
log.Error().Stringer("hash", r.TxHash).Msg("Transaction failed")
268268
return nil
269269
}
270270
time.Sleep(1 * time.Second)
@@ -273,22 +273,22 @@ func WaitMineTransaction(ctx context.Context, client *ethclient.Client, tx *type
273273
}
274274

275275
func getMaxNonce(addr common.Address) (uint64, error) {
276-
log.Info().Msg("getting max nonce from txpool_content")
276+
log.Info().Msg("Getting max nonce from txpool_content")
277277
maxNonce, err := getMaxNonceFromTxPool(addr)
278278
if err == nil {
279-
log.Info().Uint64("maxNonce", maxNonce).Msg("maxNonce loaded from txpool_content")
279+
log.Info().Uint64("maxNonce", maxNonce).Msg("Loaded max nonce from txpool_content")
280280
return maxNonce, nil
281281
}
282-
log.Warn().Err(err).Msg("unable to get max nonce from txpool_content")
282+
log.Warn().Err(err).Msg("Unable to get max nonce from txpool_content")
283283

284-
log.Info().Msg("getting max nonce from pending nonce")
284+
log.Info().Msg("Getting max nonce from pending nonce")
285285
// if the error is not about txpool_content, falls back to PendingNonceAt
286286
maxNonce, err = rpcClient.PendingNonceAt(context.Background(), addr)
287287
if err != nil {
288288
return 0, err
289289
}
290290

291-
log.Info().Uint64("maxNonce", maxNonce).Msg("maxNonce loaded from pending nonce")
291+
log.Info().Uint64("maxNonce", maxNonce).Msg("Loaded max nonce from pending nonce")
292292
return maxNonce, nil
293293
}
294294

@@ -318,7 +318,7 @@ func getMaxNonceFromTxPool(addr common.Address) (uint64, error) {
318318
nonceInt, ok := new(big.Int).SetString(nonce, 10)
319319
if !ok {
320320
err = fmt.Errorf("invalid nonce found: %s", nonce)
321-
log.Warn().Err(err).Msg("unable to get txpool_content")
321+
log.Warn().Err(err).Msg("Unable to get txpool_content")
322322
return 0, err
323323
}
324324

cmd/foldtrace/foldtrace.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ var FoldTraceCmd = &cobra.Command{
7070

7171
if isCall(op.OP) {
7272
if len(op.Stack) < 6 {
73-
log.Warn().Int("stackLength", len(op.Stack)).Msg("detected a call with a stack that's too short")
73+
log.Warn().Int("stackLength", len(op.Stack)).Msg("Detected a call with a stack that's too short")
7474
} else {
7575
currentLabel = op.OP + " to " + op.Stack[len(op.Stack)-2]
7676
}
@@ -95,7 +95,7 @@ var FoldTraceCmd = &cobra.Command{
9595
folded[currentMetricPath] += metricValue
9696
}
9797

98-
log.Trace().Strs("context", contexts).Uint64("depth", currentDepth).Str("currentLabel", currentLabel).Uint64("pc", op.PC).Msg("trace operation")
98+
log.Trace().Strs("context", contexts).Uint64("depth", currentDepth).Str("currentLabel", currentLabel).Uint64("pc", op.PC).Msg("Trace operation")
9999

100100
}
101101
for context, metric := range folded {
@@ -125,7 +125,7 @@ func getActualUsedGas(index int, td *TraceData) uint64 {
125125

126126
return op.Gas - td.StructLogs[i].Gas
127127
}
128-
log.Warn().Uint64("pc", op.PC).Uint64("depth", op.Depth).Msg("unable to look ahead for gas use")
128+
log.Warn().Uint64("pc", op.PC).Uint64("depth", op.Depth).Msg("Unable to look ahead for gas use")
129129
return op.GasCost
130130
}
131131

cmd/fund/fund.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func runFunding(ctx context.Context) error {
8282
}
8383

8484
// Fund wallets.
85-
log.Debug().Msg("checking if multicall3 is supported")
85+
log.Debug().Msg("Checking if multicall3 is supported")
8686
var multicall3Addr *common.Address
8787
if len(params.Multicall3Address) > 0 {
8888
addr := common.HexToAddress(params.Multicall3Address)
@@ -100,7 +100,7 @@ func runFunding(ctx context.Context) error {
100100
Msg("multicall3 is supported and will be used to fund wallets")
101101
err = fundWalletsWithMulticall3(ctx, c, tops, addresses, multicall3Addr)
102102
} else {
103-
log.Info().Msg("multicall3 is not supported, will use funder contract to fund wallets")
103+
log.Info().Msg("Multicall3 is not supported, will use funder contract to fund wallets")
104104
err = fundWalletsWithFunder(ctx, c, tops, privateKey, addresses, privateKeys)
105105
}
106106
if err != nil {
@@ -378,7 +378,7 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
378378
if params.AccountsPerFundingTx > 0 && params.AccountsPerFundingTx < accsToFundPerTx {
379379
accsToFundPerTx = params.AccountsPerFundingTx
380380
}
381-
log.Debug().Uint64("accsToFundPerTx", accsToFundPerTx).Msg("multicall3 max accounts to fund per tx")
381+
log.Debug().Uint64("accsToFundPerTx", accsToFundPerTx).Msg("Multicall3 max accounts to fund per tx")
382382
chSize := (uint64(len(wallets)) / accsToFundPerTx) + 1
383383

384384
var txsCh chan *types.Transaction
@@ -412,7 +412,7 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
412412
if rl != nil {
413413
iErr = rl.Wait(ctx)
414414
if iErr != nil {
415-
log.Error().Err(iErr).Msg("rate limiter wait failed before funding accounts with multicall3")
415+
log.Error().Err(iErr).Msg("Rate limiter wait failed before funding accounts with multicall3")
416416
return
417417
}
418418
}
@@ -434,7 +434,7 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
434434
}
435435
if iErr != nil {
436436
errCh <- iErr
437-
log.Error().Err(iErr).Msg("failed to fund accounts with multicall3")
437+
log.Error().Err(iErr).Msg("Failed to fund accounts with multicall3")
438438
return
439439
}
440440
log.Info().
@@ -465,7 +465,7 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
465465
return combinedErrors
466466
}
467467

468-
log.Info().Msg("all funding transactions sent, waiting for confirmation...")
468+
log.Info().Msg("All funding transactions sent, waiting for confirmation")
469469

470470
// ensure the txs to fund sending accounts using multicall3 were mined successfully
471471
for tx := range txsCh {
@@ -478,7 +478,7 @@ func fundWalletsWithMulticall3(ctx context.Context, c *ethclient.Client, tops *b
478478

479479
r, err := util.WaitReceipt(ctx, c, tx.Hash())
480480
if err != nil {
481-
log.Error().Err(err).Msg("failed to wait for transaction to fund accounts with multicall3")
481+
log.Error().Err(err).Msg("Failed to wait for transaction to fund accounts with multicall3")
482482
return err
483483
}
484484
if r == nil || r.Status != types.ReceiptStatusSuccessful {

cmd/hash/hash.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func (p *poseidongoldWrapper) Sum(b []byte) []byte {
193193
var err error
194194
for {
195195
n, _ := p.b.Read(buf)
196-
log.Info().Bytes("current-buffer", buf).Msg("summing")
196+
log.Info().Bytes("current-buffer", buf).Msg("Summing")
197197
if n > 64 {
198198
panic("What?? that shouldn't have happened")
199199
}
@@ -205,7 +205,7 @@ func (p *poseidongoldWrapper) Sum(b []byte) []byte {
205205
os.Exit(1)
206206
}
207207
if n < 64 {
208-
log.Info().Int("n", n).Msg("done")
208+
log.Info().Int("n", n).Msg("Done")
209209
break
210210
}
211211

cmd/loadtest/cmd.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ var uniswapCfg = &config.UniswapV3Config{}
3636

3737
// gasManagerCfg holds gas manager configuration.
3838
var gasManagerCfg = &config.GasManagerConfig{}
39-
var gasManagerEnabled bool
4039

4140
// LoadtestCmd represents the loadtest command.
4241
var LoadtestCmd = &cobra.Command{
4342
Use: "loadtest",
4443
Short: "Run a generic load test against an Eth/EVM style JSON-RPC endpoint.",
4544
Long: loadtestUsage,
4645
Args: cobra.NoArgs,
47-
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
46+
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
4847
cfg.RPCURL, err = flag.GetRPCURL(cmd)
4948
if err != nil {
5049
return err
@@ -56,20 +55,24 @@ var LoadtestCmd = &cobra.Command{
5655
zerolog.DurationFieldUnit = time.Second
5756
zerolog.DurationFieldInteger = true
5857

59-
if gasManagerEnabled {
58+
cfg.Headers, err = config.ParseRPCHeaders(cfg.RPCHeaders)
59+
if err != nil {
60+
return err
61+
}
62+
63+
if gasManagerCfg.Enabled {
6064
if err = gasManagerCfg.Validate(); err != nil {
6165
return err
6266
}
67+
cfg.GasManager = gasManagerCfg
6368
}
6469

70+
return nil
71+
},
72+
PreRunE: func(cmd *cobra.Command, args []string) error {
6573
return cfg.Validate()
6674
},
6775
RunE: func(cmd *cobra.Command, args []string) error {
68-
// Attach gas manager config only when explicitly enabled.
69-
cfg.GasManager = nil
70-
if gasManagerEnabled {
71-
cfg.GasManager = gasManagerCfg
72-
}
7376
return loadtest.Run(cmd.Context(), cfg)
7477
},
7578
}
@@ -87,11 +90,6 @@ var uniswapv3Cmd = &cobra.Command{
8790
// Override mode to uniswapv3 and attach configs.
8891
cfg.Modes = []string{"v3"}
8992
cfg.UniswapV3 = uniswapCfg
90-
cfg.GasManager = nil
91-
if gasManagerEnabled {
92-
cfg.GasManager = gasManagerCfg
93-
}
94-
9593
return loadtest.Run(cmd.Context(), cfg)
9694
},
9795
}
@@ -115,6 +113,7 @@ func initPersistentFlags() {
115113
pf.BoolVar(&cfg.EthCallOnly, "eth-call-only", false, "call contracts without sending transactions (incompatible with adaptive rate limiting and summarization)")
116114
pf.BoolVar(&cfg.EthCallOnlyLatestBlock, "eth-call-only-latest", false, "execute on latest block instead of original block in call-only mode with recall")
117115
pf.BoolVar(&cfg.OutputRawTxOnly, "output-raw-tx-only", false, "output raw signed transaction hex without sending (works with most modes except RPC and UniswapV3)")
116+
pf.BoolVar(&cfg.PrivateTxs, "private-txs", false, "send transactions via eth_sendRawTransactionPrivate")
118117
pf.Uint64Var(&cfg.EthAmountInWei, "eth-amount-in-wei", 0, "amount of ether in wei to send per transaction")
119118
pf.Float64Var(&cfg.RateLimit, "rate-limit", 4, "requests per second limit (use negative value to remove limit)")
120119
pf.BoolVar(&cfg.AdaptiveRateLimit, "adaptive-rate-limit", false, "enable AIMD-style congestion control to automatically adjust request rate")
@@ -125,9 +124,9 @@ func initPersistentFlags() {
125124
pf.Float64Var(&cfg.GasPriceMultiplier, "gas-price-multiplier", 1, "a multiplier to increase or decrease the gas price")
126125
pf.Int64Var(&cfg.Seed, "seed", 123456, "a seed for generating random values and addresses")
127126
pf.Uint64Var(&cfg.ForceGasLimit, "gas-limit", 0, "manually specify gas limit (useful to avoid eth_estimateGas or when auto-computation fails)")
128-
pf.Uint64Var(&cfg.ForceGasPrice, "gas-price", 0, "manually specify gas price (useful when auto-detection fails)")
127+
pf.Var(&flag.GasValue{Val: &cfg.ForceGasPrice}, "gas-price", "gas price with unit support (e.g., \"100gwei\", \"1000000000\")")
129128
pf.Uint64Var(&cfg.StartNonce, "nonce", 0, "use this flag to manually set the starting nonce")
130-
pf.Uint64Var(&cfg.ForcePriorityGasPrice, "priority-gas-price", 0, "gas tip price for EIP-1559 transactions")
129+
pf.Var(&flag.GasValue{Val: &cfg.ForcePriorityGasPrice}, "priority-gas-price", "gas tip for EIP-1559 with unit support (e.g., \"2gwei\")")
131130
pf.BoolVar(&cfg.ShouldProduceSummary, "summarize", false, "produce execution summary after load test (can take a long time for large tests)")
132131
pf.Uint64Var(&cfg.BatchSize, "batch-size", 999, "batch size for receipt fetching (default: 999)")
133132
pf.StringVar(&cfg.SummaryOutputMode, "output-mode", "text", "format mode for summary output (json | text)")
@@ -136,13 +135,15 @@ func initPersistentFlags() {
136135
pf.BoolVar(&cfg.FireAndForget, "send-only", false, "alias for --fire-and-forget")
137136
pf.BoolVar(&cfg.CheckForPreconf, "check-preconf", false, "check for preconf status after sending tx")
138137
pf.StringVar(&cfg.PreconfStatsFile, "preconf-stats-file", "", "path for preconf stats JSON output, updated every 2 seconds")
138+
pf.BoolVar(&cfg.StopOnInsufficientFunds, "stop-on-insufficient-funds", false, "stop sending from account when it encounters insufficient funds error")
139+
pf.StringVar(&cfg.RPCHeaders, "rpc-headers", "", "custom HTTP headers for RPC requests (format: \"key1:value1,key2:value2\")")
139140

140141
initGasManagerFlags()
141142
}
142143

143144
func initGasManagerFlags() {
144145
pf := LoadtestCmd.PersistentFlags()
145-
pf.BoolVar(&gasManagerEnabled, "gas-manager-enabled", false, "enable block-based gas manager (oscillation wave + gas budget vault)")
146+
pf.BoolVar(&gasManagerCfg.Enabled, "gas-manager-enabled", false, "enable block-based gas manager (oscillation wave + gas budget vault)")
146147

147148
// Oscillation wave
148149
pf.StringVar(&gasManagerCfg.OscillationWave, "gas-manager-oscillation-wave", "flat", "type of oscillation wave (flat | sine | square | triangle | sawtooth)")
@@ -167,6 +168,7 @@ func initFlags() {
167168
f.StringVar(&cfg.SendingAccountsFile, "sending-accounts-file", "", "file with sending account private keys, one per line (avoids pool queue and preserves accounts across runs)")
168169
f.StringVar(&cfg.DumpSendingAccountsFile, "dump-sending-accounts-file", "", "file path to dump generated private keys when using --sending-accounts-count")
169170
f.Uint64Var(&cfg.AccountsPerFundingTx, "accounts-per-funding-tx", 400, "number of accounts to fund per multicall3 transaction")
171+
f.BoolVar(&cfg.SequentialNonceFetch, "sequential-nonce-fetch", false, "fetch nonces sequentially instead of in parallel (use if hitting rate limits)")
170172
f.Uint64Var(&cfg.MaxBaseFeeWei, "max-base-fee-wei", 0, "maximum base fee in wei (pause sending new transactions when exceeded, useful during network congestion)")
171173
f.StringSliceVarP(&cfg.Modes, "mode", "m", []string{"t"}, `testing mode (can specify multiple like "d,t"):
172174
2, erc20 - send ERC20 tokens

cmd/monitor/monitor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ func (ms *monitorStatus) processBatchesConcurrently(ctx context.Context, rpc *et
452452
return nil
453453
}
454454
if err := backoff.Retry(retryable, b); err != nil {
455-
log.Error().Err(err).Msg("unable to retry")
455+
log.Error().Err(err).Msg("Unable to retry")
456456
errorsMutex.Lock()
457457
errs = append(errs, err)
458458
errorsMutex.Unlock()

cmd/monitor/ui/ui.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ func waitForReceipt(ctx context.Context, rpcClient *ethrpc.Client, txHash string
460460
}
461461

462462
if result.TransactionHash == "" {
463-
log.Info().Msg("Receipt not found, waiting more...")
463+
log.Info().Msg("Receipt not found, waiting more")
464464
time.Sleep(2 * time.Second)
465465
continue
466466
}

cmd/monitorv2/monitorv2.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ var MonitorV2Cmd = &cobra.Command{
5555
go func() {
5656
log.Info().Str("addr", pprofAddr).Msg("Starting pprof server")
5757
if err := http.ListenAndServe(pprofAddr, nil); err != nil {
58-
log.Error().Err(err).Msg("pprof server failed")
58+
log.Error().Err(err).Msg("The pprof server failed")
5959
}
6060
}()
6161
}

0 commit comments

Comments
 (0)