Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 3 additions & 18 deletions cmd/loadtest/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ func (ap *AccountPool) FundAccounts(ctx context.Context) error {
Str("txHash", tx.Hash().Hex()).
Msg("transaction to fund account sent")

receipt, err := ap.waitMined(ctx, tx)
receipt, err := waitReceipt(ctx, ap.client, tx.Hash())
if err != nil {
log.Error().
Str("address", tx.To().Hex()).
Expand Down Expand Up @@ -670,7 +670,7 @@ func (ap *AccountPool) ReturnFunds(ctx context.Context) error {
Str("txHash", tx.Hash().Hex()).
Msg("transaction to return funds sent")

_, err = ap.waitMined(ctx, tx)
_, err = waitReceiptWithTimeout(ctx, ap.client, tx.Hash(), time.Minute)
if err != nil {
log.Error().
Str("address", tx.To().Hex()).
Expand Down Expand Up @@ -852,7 +852,7 @@ func (ap *AccountPool) fund(ctx context.Context, acc Account, forcedNonce *uint6

// Wait for the transaction to be mined
if waitToFund {
receipt, err := ap.waitMined(ctx, signedTx)
receipt, err := waitReceipt(ctx, ap.client, signedTx.Hash())
if err != nil {
log.Error().
Str("address", acc.address.Hex()).
Expand Down Expand Up @@ -935,21 +935,6 @@ func (ap *AccountPool) createEOATransferTx(ctx context.Context, sender *ecdsa.Pr
return signedTx, nil
}

// Waits for the transaction to be mined
func (ap *AccountPool) waitMined(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) {
ctxTimeout, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
receipt, err := bind.WaitMined(ctxTimeout, ap.client, tx)
if err != nil {
log.Error().
Str("txHash", tx.Hash().Hex()).
Err(err).
Msg("Unable to wait for transaction to be mined")
return nil, err
}
return receipt, nil
}

// Returns the address and private key of the given private key
func getAddressAndPrivateKeyHex(ctx context.Context, privateKey *ecdsa.PrivateKey) (string, string) {
privateKeyBytes := crypto.FromECDSA(privateKey)
Expand Down
6 changes: 6 additions & 0 deletions cmd/loadtest/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ type (
KeepFundedAmount *bool
SendingAddressesFile *string
Proxy *string
WaitForReceipt *bool
ReceiptRetryMax *uint
ReceiptRetryInitialDelayMs *uint

// Computed
CurrentGasPrice *big.Int
Expand Down Expand Up @@ -285,6 +288,9 @@ v3, uniswapv3 - Perform UniswapV3 swaps`)
ltp.ContractCallPayable = LoadtestCmd.Flags().Bool("contract-call-payable", false, "Use this flag if the function is payable, the value amount passed will be from --eth-amount-in-wei. This must be paired up with --mode contract-call and --contract-address")
ltp.InscriptionContent = LoadtestCmd.Flags().String("inscription-content", `data:,{"p":"erc-20","op":"mint","tick":"TEST","amt":"1"}`, "The inscription content that will be encoded as calldata. This must be paired up with --mode inscription")
ltp.Proxy = LoadtestCmd.Flags().String("proxy", "", "Use the proxy specified")
ltp.WaitForReceipt = LoadtestCmd.Flags().Bool("wait-for-receipt", false, "If set to true, the load test will wait for the transaction receipt to be mined. If set to false, the load test will not wait for the transaction receipt and will just send the transaction.")
ltp.ReceiptRetryMax = LoadtestCmd.Flags().Uint("receipt-retry-max", 30, "Maximum number of attempts to poll for transaction receipt when --wait-for-receipt is enabled.")
ltp.ReceiptRetryInitialDelayMs = LoadtestCmd.Flags().Uint("receipt-retry-initial-delay-ms", 100, "Initial delay in milliseconds for receipt polling retry. Uses exponential backoff with jitter.")

inputLoadTestParams = *ltp

Expand Down
12 changes: 6 additions & 6 deletions cmd/loadtest/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func generateRandomBlobData(size int) ([]byte, error) {
return nil, err
}
if n != size {
return nil, fmt.Errorf("Could not create random blob data with size %d: %v", size, err)
return nil, fmt.Errorf("could not create random blob data with size %d: %v", size, err)
}
return data, nil
}
Expand All @@ -56,7 +56,7 @@ func createBlob(data []byte) kzg4844.Blob {
func generateBlobCommitment(data []byte) (*BlobCommitment, error) {
dataLen := len(data)
if dataLen > params.BlobTxFieldElementsPerBlob*(params.BlobTxBytesPerFieldElement-1) {
return nil, fmt.Errorf("Blob data longer than allowed (length: %v, limit: %v)", dataLen, params.BlobTxFieldElementsPerBlob*(params.BlobTxBytesPerFieldElement-1))
return nil, fmt.Errorf("blob data longer than allowed (length: %v, limit: %v)", dataLen, params.BlobTxFieldElementsPerBlob*(params.BlobTxBytesPerFieldElement-1))
}
blobCommitment := BlobCommitment{
Blob: createBlob(data),
Expand All @@ -66,13 +66,13 @@ func generateBlobCommitment(data []byte) (*BlobCommitment, error) {
// Generate blob commitment
blobCommitment.Commitment, err = kzg4844.BlobToCommitment(&blobCommitment.Blob)
if err != nil {
return nil, fmt.Errorf("Failed generating blob commitment: %w", err)
return nil, fmt.Errorf("failed generating blob commitment: %w", err)
}

// Generate blob proof
blobCommitment.Proof, err = kzg4844.ComputeBlobProof(&blobCommitment.Blob, blobCommitment.Commitment)
if err != nil {
return nil, fmt.Errorf("Failed generating blob proof: %w", err)
return nil, fmt.Errorf("failed generating blob proof: %w", err)
}

// Build versioned hash
Expand All @@ -90,13 +90,13 @@ func appendBlobCommitment(tx *types.BlobTx) error {
blobRefBytes, _ = generateRandomBlobData(blobLen)

if blobRefBytes == nil {
return fmt.Errorf("Unknown blob ref")
return fmt.Errorf("unknown blob ref")
}
blobBytes = append(blobBytes, blobRefBytes...)

blobCommitment, err := generateBlobCommitment(blobBytes)
if err != nil {
return fmt.Errorf("Invalid blob: %w", err)
return fmt.Errorf("invalid blob: %w", err)
}

tx.BlobHashes = append(tx.BlobHashes, blobCommitment.VersionedHash)
Expand Down
6 changes: 6 additions & 0 deletions cmd/loadtest/loadtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,12 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
if !*inputLoadTestParams.SendOnly {
recordSample(routineID, requestID, tErr, startReq, endReq, sendingTops.Nonce.Uint64())
}
if tErr == nil && *inputLoadTestParams.WaitForReceipt {
receiptMaxRetries := *inputLoadTestParams.ReceiptRetryMax
receiptRetryInitialDelayMs := *inputLoadTestParams.ReceiptRetryInitialDelayMs
_, tErr = waitReceiptWithRetries(ctx, c, ltTxHash, receiptMaxRetries, receiptRetryInitialDelayMs)
}

if tErr != nil {
log.Error().
Int64("routineID", routineID).
Expand Down
84 changes: 84 additions & 0 deletions cmd/loadtest/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package loadtest

import (
"context"
"fmt"
"math/rand"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
)

// waitReceipt waits for a transaction receipt with default parameters.
func waitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash) (*types.Receipt, error) {
return internalWaitReceipt(ctx, client, txHash, 0, 0, 0)
}

// waitReceiptWithRetries waits for a transaction receipt with retries and exponential backoff.
func waitReceiptWithRetries(ctx context.Context, client *ethclient.Client, txHash common.Hash, maxRetries uint, initialDelayMs uint) (*types.Receipt, error) {
return internalWaitReceipt(ctx, client, txHash, maxRetries, initialDelayMs, 0)
}

// waitReceiptWithTimeout waits for a transaction receipt with a specified timeout.
func waitReceiptWithTimeout(ctx context.Context, client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*types.Receipt, error) {
return internalWaitReceipt(ctx, client, txHash, 0, 0, timeout)
}

// waitReceiptWithRetriesAndTimeout waits for a transaction receipt with retries, exponential backoff, and a timeout.
func internalWaitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash, maxRetries uint, initialDelayMs uint, timeout time.Duration) (*types.Receipt, error) {
// Set defaults for zero values
effectiveTimeout := timeout
if effectiveTimeout == 0 {
effectiveTimeout = 1 * time.Minute // Default: 1 minute
}

// Create context with timeout
timeoutCtx, cancel := context.WithTimeout(ctx, effectiveTimeout)
defer cancel()

effectiveInitialDelayMs := initialDelayMs
if effectiveInitialDelayMs == 0 {
effectiveInitialDelayMs = 100 // Default: 100ms
} else if effectiveInitialDelayMs < 10 {
effectiveInitialDelayMs = 10 // Minimum 10ms
}

for attempt := uint(0); ; attempt++ {
receipt, err := client.TransactionReceipt(timeoutCtx, txHash)
if err == nil && receipt != nil {
return receipt, nil
}

// If maxRetries > 0 and we've reached the limit, exit
// Note: effectiveMaxRetries is always > 0 due to default above
if maxRetries > 0 && attempt >= maxRetries-1 {
return nil, fmt.Errorf("failed to get receipt after %d attempts: %w", maxRetries, err)
}

// Calculate delay
baseDelay := time.Duration(effectiveInitialDelayMs) * time.Millisecond
exponentialDelay := baseDelay * time.Duration(1<<attempt)

// Add cap to prevent extremely long delays
maxDelay := 30 * time.Second
if exponentialDelay > maxDelay {
exponentialDelay = maxDelay
}

maxJitter := exponentialDelay / 2
if maxJitter <= 0 {
maxJitter = 1 * time.Millisecond
}
jitter := time.Duration(rand.Int63n(int64(maxJitter)))
totalDelay := exponentialDelay + jitter

select {
case <-timeoutCtx.Done():
return nil, timeoutCtx.Err()
case <-time.After(totalDelay):
// Continue
}
}
}
3 changes: 3 additions & 0 deletions doc/polycli_loadtest.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ The codebase has a contract that used for load testing. It's written in Solidity
--proxy string Use the proxy specified
--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)
--receipt-retry-initial-delay-ms uint Initial delay in milliseconds for receipt polling retry. Uses exponential backoff with jitter. (default 100)
--receipt-retry-max uint Maximum number of attempts to poll for transaction receipt when --wait-for-receipt is enabled. (default 30)
-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)
Expand All @@ -168,6 +170,7 @@ The codebase has a contract that used for load testing. It's written in Solidity
-t, --time-limit int 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. (default -1)
--to-address string The address that we're going to send to (default "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
--to-random When doing a transfer test, should we send to random addresses rather than DEADBEEFx5
--wait-for-receipt If set to true, the load test will wait for the transaction receipt to be mined. If set to false, the load test will not wait for the transaction receipt and will just send the transaction.
```

The command also inherits flags from parent commands.
Expand Down