Skip to content

Commit 530c5ab

Browse files
authored
feat: [loadtest] add --max-base-fee-wei flag (#681)
* loadtest: add --max-base-fee-gwei flag * replace logic to get current base fee * max-base-fee-gwei is now max-base-fee-wei * make gen-doc * log review
1 parent 74b2f4e commit 530c5ab

File tree

4 files changed

+137
-25
lines changed

4 files changed

+137
-25
lines changed

cmd/loadtest/account.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func NewAccountPool(ctx context.Context, client *ethclient.Client, fundingPrivat
141141
return nil, fmt.Errorf("unable to get latestBlockNumber: %w", err)
142142
}
143143

144-
return &AccountPool{
144+
ap := &AccountPool{
145145
currentAccountIndex: 0,
146146
client: client,
147147
accounts: make([]Account, 0),
@@ -151,7 +151,21 @@ func NewAccountPool(ctx context.Context, client *ethclient.Client, fundingPrivat
151151
accountsPositions: make(map[common.Address]int),
152152
latestBlockNumber: latestBlockNumber,
153153
clientRateLimiter: rate.NewLimiter(rate.Every(50*time.Millisecond), 1),
154-
}, nil
154+
}
155+
156+
if !ap.isFundingEnabled() {
157+
if ap.isCallOnly() {
158+
log.Debug().
159+
Msg("sending account funding is disabled in call only mode")
160+
}
161+
162+
if !ap.hasFundingAmount() {
163+
log.Debug().
164+
Msg("sending account funding is disabled due to funding amount being zero")
165+
}
166+
}
167+
168+
return ap, nil
155169
}
156170

157171
// Adds N random accounts to the pool
@@ -489,7 +503,6 @@ func (ap *AccountPool) ReturnFunds(ctx context.Context) error {
489503

490504
if !ap.isFundingEnabled() {
491505
log.Debug().
492-
Uint64("fundingAmount", ap.fundingAmount.Uint64()).
493506
Msg("account funding is disabled, skipping returning funds from sending accounts")
494507
return nil
495508
}
@@ -780,21 +793,13 @@ func (ap *AccountPool) Next(ctx context.Context) (Account, error) {
780793
if ap.currentAccountIndex >= len(ap.accounts) {
781794
ap.currentAccountIndex = 0
782795
}
783-
log.Debug().
784-
Str("address", account.address.Hex()).
785-
Str("nonce", fmt.Sprintf("%d", account.nonce)).
786-
Msg("account returned from pool")
787796
return account, nil
788797
}
789798

790799
// Checks multiple conditions of the account and funds it if needed
791800
func (ap *AccountPool) fundAccountIfNeeded(ctx context.Context, account Account, forcedNonce *uint64, waitToFund bool) (*types.Transaction, error) {
792801
// If funding amount is zero, skip funding entirely
793802
if !ap.isFundingEnabled() {
794-
log.Debug().
795-
Uint64("fundingAmount", ap.fundingAmount.Uint64()).
796-
Stringer("address", account.address).
797-
Msg("funding disabled - skipping account funding for account")
798803
return nil, nil
799804
}
800805

@@ -952,23 +957,25 @@ func (ap *AccountPool) createEOATransferTx(ctx context.Context, sender *ecdsa.Pr
952957
}
953958

954959
func (ap *AccountPool) isFundingEnabled() bool {
955-
callOnly := *inputLoadTestParams.EthCallOnly
956-
if callOnly {
957-
log.Debug().
958-
Msg("sending account funding is disabled in call only mode")
960+
if ap.isCallOnly() {
959961
return false
960962
}
961963

962-
hasFundingAmount := ap.fundingAmount != nil && ap.fundingAmount.Cmp(big.NewInt(0)) > 0
963-
if !hasFundingAmount {
964-
log.Debug().
965-
Msg("sending account funding is disabled due to funding amount being zero")
964+
if !ap.hasFundingAmount() {
966965
return false
967966
}
968967

969968
return true
970969
}
971970

971+
func (ap *AccountPool) isCallOnly() bool {
972+
return *inputLoadTestParams.EthCallOnly
973+
}
974+
975+
func (ap *AccountPool) hasFundingAmount() bool {
976+
return ap.fundingAmount != nil && ap.fundingAmount.Cmp(big.NewInt(0)) > 0
977+
}
978+
972979
func (ap *AccountPool) isRefundingEnabled() bool {
973980
if !ap.isFundingEnabled() {
974981
log.Debug().

cmd/loadtest/app.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type (
8888
WaitForReceipt *bool
8989
ReceiptRetryMax *uint
9090
ReceiptRetryInitialDelayMs *uint
91+
MaxBaseFeeWei *uint64
9192

9293
// Computed
9394
CurrentGasPrice *big.Int
@@ -251,6 +252,7 @@ func initFlags() {
251252
ltp.PreFundSendingAccounts = LoadtestCmd.Flags().Bool("pre-fund-sending-accounts", false, "If set to true, the sending accounts will be funded at the start of the execution, otherwise all accounts will be funded when used for the first time.")
252253
ltp.RefundRemainingFunds = LoadtestCmd.Flags().Bool("refund-remaining-funds", false, "If set to true, the funded amount will be refunded to the funding account. Otherwise, the funded amount will remain in the sending accounts.")
253254
ltp.SendingAccountsFile = LoadtestCmd.Flags().String("sending-accounts-file", "", "The file containing the sending accounts private keys, one per line. This is useful for avoiding pool account queue but also to keep the same sending accounts for different execution cycles.")
255+
ltp.MaxBaseFeeWei = LoadtestCmd.Flags().Uint64("max-base-fee-wei", 0, "The maximum base fee in wei. If the base fee exceeds this value, sending tx will be paused and while paused, existing in-flight transactions continue to confirmation, but no additional SendTransaction calls occur. This is useful to avoid sending transactions when the network is congested.")
254256

255257
// Local flags.
256258
ltp.Modes = LoadtestCmd.Flags().StringSliceP("mode", "m", []string{"t"}, `The testing mode to use. It can be multiple like: "d,t"

cmd/loadtest/loadtest.go

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"net/http"
1515
"net/url"
1616
"slices"
17+
"sync/atomic"
1718

1819
"os"
1920
"os/signal"
@@ -378,9 +379,15 @@ func initializeAccountPool(ctx context.Context, c *ethclient.Client, privateKey
378379
return fmt.Errorf("unable to set account pool. %w", err)
379380
}
380381

381-
// Check if we need to pre-fund sending accounts
382-
if sendingAccountsCount == 0 || callOnly {
383-
log.Info().Msg("No sending accounts to pre-fund or call-only mode is enabled. Skipping pre-funding of sending accounts.")
382+
// check if there are sending accounts to pre fund
383+
if sendingAccountsCount == 0 {
384+
log.Info().Msg("No sending accounts to pre-fund. Skipping pre-funding of sending accounts.")
385+
return nil
386+
}
387+
388+
// checks if call only is enabled
389+
if callOnly {
390+
log.Info().Msg("call only mode is enabled. Skipping pre-funding of sending accounts.")
384391
return nil
385392
}
386393

@@ -651,9 +658,9 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
651658
if *ltp.RateLimit <= 0.0 {
652659
rl = nil
653660
}
654-
rateLimitCtx, cancel := context.WithCancel(ctx)
655661

656-
defer cancel()
662+
rateLimitCtx, rateLimitCancel := context.WithCancel(ctx)
663+
defer rateLimitCancel()
657664
if *ltp.AdaptiveRateLimit && rl != nil {
658665
go updateRateLimit(rateLimitCtx, rl, rpc, accountPool, steadyStateTxPoolSize, adaptiveRateLimitIncrement, time.Duration(*ltp.AdaptiveCycleDuration)*time.Second, *ltp.AdaptiveBackoffFactor)
659666
}
@@ -776,6 +783,8 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
776783
return err
777784
}
778785

786+
mustCheckMaxBaseFee, maxBaseFeeCtxCancel, waitBaseFeeToDrop := setupBaseFeeMonitoring(ctx, c, ltp)
787+
779788
log.Debug().Msg("Starting main load test loop")
780789
var wg sync.WaitGroup
781790
for routineID := int64(0); routineID < maxRoutines; routineID++ {
@@ -830,6 +839,21 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
830839
return
831840
}
832841
sendingTops.Nonce = new(big.Int).SetUint64(account.nonce)
842+
843+
if mustCheckMaxBaseFee {
844+
waiting := false
845+
for waitBaseFeeToDrop.Load() {
846+
if !waiting {
847+
waiting = true
848+
log.Debug().
849+
Int64("routineID", routineID).
850+
Int64("requestID", requestID).
851+
Msg("go routine is waiting for base fee to drop")
852+
}
853+
time.Sleep(time.Second)
854+
}
855+
}
856+
833857
sendingTops = configureTransactOpts(ctx, c, sendingTops)
834858

835859
switch localMode {
@@ -925,21 +949,99 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
925949
Stringer("txhash", ltTxHash).
926950
Any("nonce", sendingTops.Nonce).
927951
Str("mode", localMode.String()).
952+
Str("sendingAddress", sendingTops.From.String()).
928953
Msg("Request")
929954
}
930955
wg.Done()
931956
}(routineID)
932957
}
933958
log.Trace().Msg("Finished starting go routines. Waiting..")
934959
wg.Wait()
935-
cancel()
960+
rateLimitCancel()
961+
maxBaseFeeCtxCancel()
936962
if *ltp.EthCallOnly {
937963
return nil
938964
}
939965

940966
return nil
941967
}
942968

969+
func setupBaseFeeMonitoring(ctx context.Context, c *ethclient.Client, ltp loadTestParams) (bool, context.CancelFunc, *atomic.Bool) {
970+
// monitor max base fee if configured
971+
maxBaseFeeCtx, maxBaseFeeCtxCancel := context.WithCancel(ctx)
972+
mustCheckMaxBaseFee := *ltp.MaxBaseFeeWei > 0
973+
var waitBaseFeeToDrop atomic.Bool
974+
waitBaseFeeToDrop.Store(false)
975+
if mustCheckMaxBaseFee {
976+
log.Info().
977+
Msg("max base fee monitoring enabled")
978+
979+
wg := sync.WaitGroup{}
980+
wg.Add(1)
981+
// start a goroutine to monitor the base fee while load test is running
982+
go func(ctx context.Context, c *ethclient.Client, waitToDrop *atomic.Bool, maxBaseFeeWei uint64) {
983+
firstRun := true
984+
for {
985+
select {
986+
case <-ctx.Done():
987+
return
988+
default:
989+
currentBaseFeeIsGreaterThanMax, currentBaseFeeWei, err := isCurrentBaseFeeGreaterThanMaxBaseFee(ctx, c, maxBaseFeeWei)
990+
if err != nil {
991+
log.Error().
992+
Err(err).
993+
Msg("Error checking base fee during load test")
994+
} else {
995+
if currentBaseFeeIsGreaterThanMax {
996+
if !waitToDrop.Load() {
997+
log.Warn().
998+
Msgf("PAUSE: base fee %d Wei > limit %d Wei", currentBaseFeeWei.Uint64(), maxBaseFeeWei)
999+
waitToDrop.Store(true)
1000+
}
1001+
} else if waitToDrop.Load() {
1002+
log.Info().
1003+
Msgf("RESUME: base fee %d Wei ≤ limit %d Wei", currentBaseFeeWei.Uint64(), maxBaseFeeWei)
1004+
waitToDrop.Store(false)
1005+
}
1006+
1007+
if firstRun {
1008+
firstRun = false
1009+
wg.Done()
1010+
}
1011+
}
1012+
time.Sleep(time.Second)
1013+
}
1014+
}
1015+
}(maxBaseFeeCtx, c, &waitBaseFeeToDrop, *ltp.MaxBaseFeeWei)
1016+
1017+
// wait for first run to complete so we know if we need to wait or not for base fee to drop
1018+
wg.Wait()
1019+
}
1020+
return mustCheckMaxBaseFee, maxBaseFeeCtxCancel, &waitBaseFeeToDrop
1021+
}
1022+
1023+
func isCurrentBaseFeeGreaterThanMaxBaseFee(ctx context.Context, c *ethclient.Client, maxBaseFee uint64) (bool, *big.Int, error) {
1024+
header, err := c.HeaderByNumber(ctx, nil)
1025+
if errors.Is(err, context.Canceled) {
1026+
log.Debug().Msg("max base fee monitoring context canceled")
1027+
return false, nil, nil
1028+
} else if err != nil {
1029+
log.Error().Err(err).Msg("Unable to get latest block header to check base fee")
1030+
return false, nil, err
1031+
}
1032+
1033+
if header.BaseFee != nil {
1034+
currentBaseFee := header.BaseFee
1035+
if currentBaseFee.Cmp(new(big.Int).SetUint64(maxBaseFee)) > 0 {
1036+
return true, currentBaseFee, nil
1037+
} else {
1038+
return false, currentBaseFee, nil
1039+
}
1040+
}
1041+
1042+
return false, nil, nil
1043+
}
1044+
9431045
func getLoadTestContract(ctx context.Context, c *ethclient.Client, tops *bind.TransactOpts, cops *bind.CallOpts) (ltAddr ethcommon.Address, ltContract *tester.LoadTester, err error) {
9441046
ltAddr = ethcommon.HexToAddress(*inputLoadTestParams.LoadtestContractAddress)
9451047

doc/polycli_loadtest.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ The codebase has a contract that used for load testing. It's written in Solidity
107107
--inscription-content string The inscription content that will be encoded as calldata. This must be paired up with --mode inscription (default "data:,{\"p\":\"erc-20\",\"op\":\"mint\",\"tick\":\"TEST\",\"amt\":\"1\"}")
108108
--legacy Send a legacy transaction instead of an EIP1559 transaction.
109109
--loadtest-contract-address string The address of a pre-deployed load test contract
110+
--max-base-fee-wei uint The maximum base fee in wei. If the base fee exceeds this value, sending tx will be paused and while paused, existing in-flight transactions continue to confirmation, but no additional SendTransaction calls occur. This is useful to avoid sending transactions when the network is congested.
110111
-m, --mode strings The testing mode to use. It can be multiple like: "d,t"
111112
2, erc20 - Send ERC20 tokens
112113
7, erc721 - Mint ERC721 tokens

0 commit comments

Comments
 (0)