Skip to content

Commit 717ab2e

Browse files
authored
feat: add flag --wait-for-receipt to loadtest (#665)
* feat: add flag wait-for-receipt to loadtest * make gen * add Exponential Backoff with Jitter to loadtest --wait-for-receipt
1 parent 347eb59 commit 717ab2e

File tree

6 files changed

+108
-24
lines changed

6 files changed

+108
-24
lines changed

cmd/loadtest/account.go

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ func (ap *AccountPool) FundAccounts(ctx context.Context) error {
445445
Str("txHash", tx.Hash().Hex()).
446446
Msg("transaction to fund account sent")
447447

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

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

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

938-
// Waits for the transaction to be mined
939-
func (ap *AccountPool) waitMined(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) {
940-
ctxTimeout, cancel := context.WithTimeout(ctx, time.Minute)
941-
defer cancel()
942-
receipt, err := bind.WaitMined(ctxTimeout, ap.client, tx)
943-
if err != nil {
944-
log.Error().
945-
Str("txHash", tx.Hash().Hex()).
946-
Err(err).
947-
Msg("Unable to wait for transaction to be mined")
948-
return nil, err
949-
}
950-
return receipt, nil
951-
}
952-
953938
// Returns the address and private key of the given private key
954939
func getAddressAndPrivateKeyHex(ctx context.Context, privateKey *ecdsa.PrivateKey) (string, string) {
955940
privateKeyBytes := crypto.FromECDSA(privateKey)

cmd/loadtest/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ type (
8888
KeepFundedAmount *bool
8989
SendingAddressesFile *string
9090
Proxy *string
91+
WaitForReceipt *bool
92+
ReceiptRetryMax *uint
93+
ReceiptRetryInitialDelayMs *uint
9194

9295
// Computed
9396
CurrentGasPrice *big.Int
@@ -285,6 +288,9 @@ v3, uniswapv3 - Perform UniswapV3 swaps`)
285288
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")
286289
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")
287290
ltp.Proxy = LoadtestCmd.Flags().String("proxy", "", "Use the proxy specified")
291+
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.")
292+
ltp.ReceiptRetryMax = LoadtestCmd.Flags().Uint("receipt-retry-max", 30, "Maximum number of attempts to poll for transaction receipt when --wait-for-receipt is enabled.")
293+
ltp.ReceiptRetryInitialDelayMs = LoadtestCmd.Flags().Uint("receipt-retry-initial-delay-ms", 100, "Initial delay in milliseconds for receipt polling retry. Uses exponential backoff with jitter.")
288294

289295
inputLoadTestParams = *ltp
290296

cmd/loadtest/blob.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func generateRandomBlobData(size int) ([]byte, error) {
2929
return nil, err
3030
}
3131
if n != size {
32-
return nil, fmt.Errorf("Could not create random blob data with size %d: %v", size, err)
32+
return nil, fmt.Errorf("could not create random blob data with size %d: %v", size, err)
3333
}
3434
return data, nil
3535
}
@@ -56,7 +56,7 @@ func createBlob(data []byte) kzg4844.Blob {
5656
func generateBlobCommitment(data []byte) (*BlobCommitment, error) {
5757
dataLen := len(data)
5858
if dataLen > params.BlobTxFieldElementsPerBlob*(params.BlobTxBytesPerFieldElement-1) {
59-
return nil, fmt.Errorf("Blob data longer than allowed (length: %v, limit: %v)", dataLen, params.BlobTxFieldElementsPerBlob*(params.BlobTxBytesPerFieldElement-1))
59+
return nil, fmt.Errorf("blob data longer than allowed (length: %v, limit: %v)", dataLen, params.BlobTxFieldElementsPerBlob*(params.BlobTxBytesPerFieldElement-1))
6060
}
6161
blobCommitment := BlobCommitment{
6262
Blob: createBlob(data),
@@ -66,13 +66,13 @@ func generateBlobCommitment(data []byte) (*BlobCommitment, error) {
6666
// Generate blob commitment
6767
blobCommitment.Commitment, err = kzg4844.BlobToCommitment(&blobCommitment.Blob)
6868
if err != nil {
69-
return nil, fmt.Errorf("Failed generating blob commitment: %w", err)
69+
return nil, fmt.Errorf("failed generating blob commitment: %w", err)
7070
}
7171

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

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

9292
if blobRefBytes == nil {
93-
return fmt.Errorf("Unknown blob ref")
93+
return fmt.Errorf("unknown blob ref")
9494
}
9595
blobBytes = append(blobBytes, blobRefBytes...)
9696

9797
blobCommitment, err := generateBlobCommitment(blobBytes)
9898
if err != nil {
99-
return fmt.Errorf("Invalid blob: %w", err)
99+
return fmt.Errorf("invalid blob: %w", err)
100100
}
101101

102102
tx.BlobHashes = append(tx.BlobHashes, blobCommitment.VersionedHash)

cmd/loadtest/loadtest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,12 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
899899
if !*inputLoadTestParams.SendOnly {
900900
recordSample(routineID, requestID, tErr, startReq, endReq, sendingTops.Nonce.Uint64())
901901
}
902+
if tErr == nil && *inputLoadTestParams.WaitForReceipt {
903+
receiptMaxRetries := *inputLoadTestParams.ReceiptRetryMax
904+
receiptRetryInitialDelayMs := *inputLoadTestParams.ReceiptRetryInitialDelayMs
905+
_, tErr = waitReceiptWithRetries(ctx, c, ltTxHash, receiptMaxRetries, receiptRetryInitialDelayMs)
906+
}
907+
902908
if tErr != nil {
903909
log.Error().
904910
Int64("routineID", routineID).

cmd/loadtest/tx.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package loadtest
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/rand"
7+
"time"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
"github.com/ethereum/go-ethereum/ethclient"
12+
)
13+
14+
// waitReceipt waits for a transaction receipt with default parameters.
15+
func waitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash) (*types.Receipt, error) {
16+
return internalWaitReceipt(ctx, client, txHash, 0, 0, 0)
17+
}
18+
19+
// waitReceiptWithRetries waits for a transaction receipt with retries and exponential backoff.
20+
func waitReceiptWithRetries(ctx context.Context, client *ethclient.Client, txHash common.Hash, maxRetries uint, initialDelayMs uint) (*types.Receipt, error) {
21+
return internalWaitReceipt(ctx, client, txHash, maxRetries, initialDelayMs, 0)
22+
}
23+
24+
// waitReceiptWithTimeout waits for a transaction receipt with a specified timeout.
25+
func waitReceiptWithTimeout(ctx context.Context, client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*types.Receipt, error) {
26+
return internalWaitReceipt(ctx, client, txHash, 0, 0, timeout)
27+
}
28+
29+
// waitReceiptWithRetriesAndTimeout waits for a transaction receipt with retries, exponential backoff, and a timeout.
30+
func internalWaitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash, maxRetries uint, initialDelayMs uint, timeout time.Duration) (*types.Receipt, error) {
31+
// Set defaults for zero values
32+
effectiveTimeout := timeout
33+
if effectiveTimeout == 0 {
34+
effectiveTimeout = 1 * time.Minute // Default: 1 minute
35+
}
36+
37+
// Create context with timeout
38+
timeoutCtx, cancel := context.WithTimeout(ctx, effectiveTimeout)
39+
defer cancel()
40+
41+
effectiveInitialDelayMs := initialDelayMs
42+
if effectiveInitialDelayMs == 0 {
43+
effectiveInitialDelayMs = 100 // Default: 100ms
44+
} else if effectiveInitialDelayMs < 10 {
45+
effectiveInitialDelayMs = 10 // Minimum 10ms
46+
}
47+
48+
for attempt := uint(0); ; attempt++ {
49+
receipt, err := client.TransactionReceipt(timeoutCtx, txHash)
50+
if err == nil && receipt != nil {
51+
return receipt, nil
52+
}
53+
54+
// If maxRetries > 0 and we've reached the limit, exit
55+
// Note: effectiveMaxRetries is always > 0 due to default above
56+
if maxRetries > 0 && attempt >= maxRetries-1 {
57+
return nil, fmt.Errorf("failed to get receipt after %d attempts: %w", maxRetries, err)
58+
}
59+
60+
// Calculate delay
61+
baseDelay := time.Duration(effectiveInitialDelayMs) * time.Millisecond
62+
exponentialDelay := baseDelay * time.Duration(1<<attempt)
63+
64+
// Add cap to prevent extremely long delays
65+
maxDelay := 30 * time.Second
66+
if exponentialDelay > maxDelay {
67+
exponentialDelay = maxDelay
68+
}
69+
70+
maxJitter := exponentialDelay / 2
71+
if maxJitter <= 0 {
72+
maxJitter = 1 * time.Millisecond
73+
}
74+
jitter := time.Duration(rand.Int63n(int64(maxJitter)))
75+
totalDelay := exponentialDelay + jitter
76+
77+
select {
78+
case <-timeoutCtx.Done():
79+
return nil, timeoutCtx.Err()
80+
case <-time.After(totalDelay):
81+
// Continue
82+
}
83+
}
84+
}

doc/polycli_loadtest.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ The codebase has a contract that used for load testing. It's written in Solidity
157157
--proxy string Use the proxy specified
158158
--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)
159159
--recall-blocks uint The number of blocks that we'll attempt to fetch for recall (default 50)
160+
--receipt-retry-initial-delay-ms uint Initial delay in milliseconds for receipt polling retry. Uses exponential backoff with jitter. (default 100)
161+
--receipt-retry-max uint Maximum number of attempts to poll for transaction receipt when --wait-for-receipt is enabled. (default 30)
160162
-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)
161163
-r, --rpc-url string The RPC endpoint url (default "http://localhost:8545")
162164
--seed int A seed for generating random values and addresses (default 123456)
@@ -168,6 +170,7 @@ The codebase has a contract that used for load testing. It's written in Solidity
168170
-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)
169171
--to-address string The address that we're going to send to (default "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
170172
--to-random When doing a transfer test, should we send to random addresses rather than DEADBEEFx5
173+
--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.
171174
```
172175
173176
The command also inherits flags from parent commands.

0 commit comments

Comments
 (0)