diff --git a/chains/ethereum/runner/runner.go b/chains/ethereum/runner/runner.go index 66f6f33..6ca16a0 100644 --- a/chains/ethereum/runner/runner.go +++ b/chains/ethereum/runner/runner.go @@ -3,7 +3,7 @@ package runner import ( "context" "fmt" - "math/rand" + "math/big" "net" "net/http" "slices" @@ -92,11 +92,36 @@ func NewRunner(ctx context.Context, logger *zap.Logger, spec loadtesttypes.LoadT return nil, err } + isNonPersistentRun := (spec.NumBatches > 0 && spec.SendInterval > 0) || spec.NumOfBlocks > 0 + if isNonPersistentRun { + if err := preFundDerivedWallets(ctx, logger, wallets); err != nil { + return nil, fmt.Errorf("prefund derived wallets: %w", err) + } + } + + initialWallets := spec.InitialWallets + if initialWallets <= 0 { + initialWallets = 1 + } + + if isNonPersistentRun { + if initialWallets < spec.NumWallets { + logger.Info( + "prefund complete; using all wallets for sender distribution", + zap.Int("initial_wallets", initialWallets), + zap.Int("num_wallets", spec.NumWallets), + ) + } + initialWallets = spec.NumWallets + } else if spec.InitialWallets <= 0 { + logger.Info("initial_wallets not set; defaulting to 1", zap.Int("num_wallets", spec.NumWallets)) + } + var distribution txfactory.TxDistribution - if spec.InitialWallets > 0 && spec.InitialWallets < spec.NumWallets { + if initialWallets > 0 && initialWallets < spec.NumWallets { logger.Info( "Using TxDistributionBootstrapped", - zap.Int("initial_wallets", spec.InitialWallets), + zap.Int("initial_wallets", initialWallets), zap.Int("num_wallets", spec.NumWallets), ) distribution = txdistribution.NewBootstrapped(logger, wallets, spec.InitialWallets) @@ -318,16 +343,16 @@ func (r *Runner) Run(ctx context.Context) (loadtesttypes.LoadTestResult, error) } func (r *Runner) buildLoad(msgSpec loadtesttypes.LoadTestMsg, useBaseline bool) ([]*gethtypes.Transaction, error) { - // For ERC20 transactions, use optimal sender selection from factory + // Deployments must come from wallet[0] and must not advance sender distribution state. var fromWallet *wallet.InteractingWallet - switch msgSpec.Type { - case inttypes.MsgTransferERC0, inttypes.MsgNativeTransferERC20: - fromWallet = r.txFactory.GetNextSender() - case inttypes.MsgDeployERC20, inttypes.MsgCreateContract: + if msgSpec.Type == inttypes.MsgDeployERC20 || msgSpec.Type == inttypes.MsgCreateContract { fromWallet = r.wallets[0] - default: - // For non-ERC20 transactions, keep random selection - fromWallet = r.wallets[rand.Intn(len(r.wallets))] + } else { + fromWallet = r.txFactory.GetNextSender() + } + + if fromWallet == nil { + return nil, nil } nonce, ok := r.nonces.Load(fromWallet.Address()) @@ -353,3 +378,93 @@ func (r *Runner) buildLoad(msgSpec loadtesttypes.LoadTestMsg, useBaseline bool) r.nonces.Store(fromWallet.Address(), lastTx.Nonce()+1) return txs, nil } + +func preFundDerivedWallets(ctx context.Context, logger *zap.Logger, wallets []*wallet.InteractingWallet) error { + if len(wallets) <= 1 { + return nil + } + + balanceMu := sync.Mutex{} + unfunded := make([]int, 0, len(wallets)-1) + balanceEG := errgroup.Group{} + balanceEG.SetLimit(16) + for i := 1; i < len(wallets); i++ { + i := i + balanceEG.Go(func() error { + bal, err := wallets[i].GetBalance(ctx) + if err != nil { + return fmt.Errorf("getting balance for wallet[%d] %s: %w", i, wallets[i].FormattedAddress(), err) + } + if bal.Sign() == 0 { + balanceMu.Lock() + unfunded = append(unfunded, i) + balanceMu.Unlock() + } + return nil + }) + } + if err := balanceEG.Wait(); err != nil { + return err + } + + if len(unfunded) == 0 { + return nil + } + + funder := wallets[0] + funderBal, err := funder.GetBalance(ctx) + if err != nil { + return fmt.Errorf("getting funder balance for %s: %w", funder.FormattedAddress(), err) + } + if funderBal.Sign() == 0 { + return fmt.Errorf("funder %s has zero balance", funder.FormattedAddress()) + } + + denom := big.NewInt(int64((len(unfunded) + 1) * 2)) + amountPerWallet := new(big.Int).Div(funderBal, denom) + if amountPerWallet.Sign() == 0 { + return fmt.Errorf("insufficient funder balance %s to pre-fund %d wallets", funderBal.String(), len(unfunded)) + } + + startNonce, err := funder.GetNonce(ctx) + if err != nil { + return fmt.Errorf("getting funder nonce for %s: %w", funder.FormattedAddress(), err) + } + + type prefundTx struct { + walletIndex int + address common.Address + txHash common.Hash + } + prefundTxs := make([]prefundTx, 0, len(unfunded)) + nextNonce := startNonce + + for _, i := range unfunded { + to := wallets[i].Address() + nonce := nextNonce + nextNonce++ + txHash, err := funder.CreateAndSendTransaction(ctx, &to, amountPerWallet, 21_000, nil, nil, &nonce) + if err != nil { + return fmt.Errorf("sending prefund tx to wallet[%d] %s: %w", i, to.Hex(), err) + } + prefundTxs = append(prefundTxs, prefundTx{walletIndex: i, address: to, txHash: txHash}) + } + + receiptEG := errgroup.Group{} + receiptEG.SetLimit(16) + for i := range prefundTxs { + tx := prefundTxs[i] + receiptEG.Go(func() error { + if _, err := funder.WaitForTxReceipt(ctx, tx.txHash, 30*time.Second); err != nil { + return fmt.Errorf("waiting prefund receipt for wallet[%d] %s (tx=%s): %w", tx.walletIndex, tx.address.Hex(), tx.txHash.Hex(), err) + } + logger.Info("prefunded derived wallet", zap.Int("wallet_index", tx.walletIndex), zap.String("address", tx.address.Hex()), zap.String("amount", amountPerWallet.String())) + return nil + }) + } + if err := receiptEG.Wait(); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index f14cf7d..b3516ee 100644 --- a/go.mod +++ b/go.mod @@ -62,12 +62,13 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cilium/ipam v0.0.0-20230509084518-fd66eae7909b // indirect - github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/cockroachdb/errors v1.12.0 // indirect github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect diff --git a/go.sum b/go.sum index 5635751..e575c76 100644 --- a/go.sum +++ b/go.sum @@ -185,11 +185,17 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= @@ -217,6 +223,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=