Files
zeekay d32e075809 refactor: update to vm v1.0.18, migrate log and metrics packages
- Update vm dependency to v1.0.18
- Remove internal log package in favor of luxfi/log
- Migrate prometheus to gatherer package
2026-01-11 21:38:34 -08:00

249 lines
7.4 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package load
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"os"
"os/signal"
"strconv"
"syscall"
"time"
ethcrypto "github.com/luxfi/crypto"
"github.com/luxfi/evm/cmd/simulator/config"
"github.com/luxfi/evm/cmd/simulator/key"
"github.com/luxfi/evm/cmd/simulator/metrics"
"github.com/luxfi/evm/cmd/simulator/txs"
"github.com/luxfi/evm/ethclient"
"github.com/luxfi/evm/params"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
ethparams "github.com/luxfi/geth/params"
log "github.com/luxfi/log"
"golang.org/x/sync/errgroup"
)
const (
MetricsEndpoint = "/metrics" // Endpoint for the Prometheus Metrics Server
)
// Loader executes a series of worker/tx sequence pairs.
// Each worker/txSequence pair issues [batchSize] transactions, confirms all
// of them as accepted, and then moves to the next batch until the txSequence
// is exhausted.
type Loader[T txs.THash] struct {
clients []txs.Worker[T]
txSequences []txs.TxSequence[T]
batchSize uint64
metrics *metrics.Metrics
}
func New[T txs.THash](
clients []txs.Worker[T],
txSequences []txs.TxSequence[T],
batchSize uint64,
metrics *metrics.Metrics,
) *Loader[T] {
return &Loader[T]{
clients: clients,
txSequences: txSequences,
batchSize: batchSize,
metrics: metrics,
}
}
func (l *Loader[T]) Execute(ctx context.Context) error {
log.Info("Constructing tx agents...", "numAgents", len(l.txSequences))
agents := make([]txs.Agent[T], 0, len(l.txSequences))
for i := 0; i < len(l.txSequences); i++ {
agents = append(agents, txs.NewIssueNAgent(l.txSequences[i], l.clients[i], l.batchSize, l.metrics))
}
log.Info("Starting tx agents...")
eg := errgroup.Group{}
for _, agent := range agents {
eg.Go(func() error {
return agent.Execute(ctx)
})
}
log.Info("Waiting for tx agents...")
if err := eg.Wait(); err != nil {
return err
}
log.Info("Tx agents completed successfully.")
return nil
}
// ConfirmReachedTip finds the max height any client has reached and then ensures every client
// reaches at least that height.
//
// This allows the network to continue to roll forward and creates a synchronization point to ensure
// that every client in the loader has reached at least the max height observed of any client at
// the time this function was called.
func (l *Loader[T]) ConfirmReachedTip(ctx context.Context) error {
maxHeight := uint64(0)
for i, client := range l.clients {
latestHeight, err := client.LatestHeight(ctx)
if err != nil {
return fmt.Errorf("client %d failed to get latest height: %w", i, err)
}
if latestHeight > maxHeight {
maxHeight = latestHeight
}
}
eg := errgroup.Group{}
for i, client := range l.clients {
eg.Go(func() error {
for {
latestHeight, err := client.LatestHeight(ctx)
if err != nil {
return fmt.Errorf("failed to get latest height from client %d: %w", i, err)
}
if latestHeight >= maxHeight {
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("failed to get latest height from client %d: %w", i, ctx.Err())
case <-time.After(time.Second):
}
}
})
}
return eg.Wait()
}
// ExecuteLoader creates txSequences from [config] and has txAgents execute the specified simulation.
func ExecuteLoader(ctx context.Context, config config.Config) error {
if config.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, config.Timeout)
defer cancel()
}
// Create buffered sigChan to receive SIGINT notifications
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
// Create context with cancel
ctx, cancel := context.WithCancel(ctx)
go func() {
// Blocks until we receive a SIGINT notification or if parent context is done
select {
case <-sigChan:
case <-ctx.Done():
}
// Cancel the child context and end all processes
cancel()
}()
m := metrics.NewDefaultMetrics()
metricsCtx := context.Background()
ms := m.Serve(metricsCtx, strconv.Itoa(int(config.MetricsPort)), MetricsEndpoint)
defer ms.Shutdown()
// Construct the arguments for the load simulator
clients := make([]ethclient.Client, 0, len(config.Endpoints))
for i := 0; i < config.Workers; i++ {
clientURI := config.Endpoints[i%len(config.Endpoints)]
client, err := ethclient.Dial(clientURI)
if err != nil {
return fmt.Errorf("failed to dial client at %s: %w", clientURI, err)
}
clients = append(clients, client)
}
keys, err := key.LoadAll(ctx, config.KeyDir)
if err != nil {
return err
}
// Ensure there are at least [config.Workers] keys and save any newly generated ones.
if len(keys) < config.Workers {
for i := 0; len(keys) < config.Workers; i++ {
newKey, err := key.Generate()
if err != nil {
return fmt.Errorf("failed to generate %d new key: %w", i, err)
}
if err := newKey.Save(config.KeyDir); err != nil {
return fmt.Errorf("failed to save %d new key: %w", i, err)
}
keys = append(keys, newKey)
}
}
// Each address needs: params.GWei * MaxFeeCap * ethparams.TxGas * TxsPerWorker total wei
// to fund gas for all of their transactions.
maxFeeCap := new(big.Int).Mul(big.NewInt(params.GWei), big.NewInt(config.MaxFeeCap))
minFundsPerAddr := new(big.Int).Mul(maxFeeCap, big.NewInt(int64(config.TxsPerWorker*ethparams.TxGas)))
fundStart := time.Now()
log.Info("Distributing funds", "numTxsPerWorker", config.TxsPerWorker, "minFunds", minFundsPerAddr)
keys, err = DistributeFunds(ctx, clients[0], keys, config.Workers, minFundsPerAddr, m)
if err != nil {
return err
}
log.Info("Distributed funds successfully", "time", time.Since(fundStart))
pks := make([]*ecdsa.PrivateKey, 0, len(keys))
senders := make([]common.Address, 0, len(keys))
for _, key := range keys {
pks = append(pks, key.PrivKey)
senders = append(senders, key.Address)
}
bigGwei := big.NewInt(params.GWei)
gasTipCap := new(big.Int).Mul(bigGwei, big.NewInt(config.MaxTipCap))
gasFeeCap := new(big.Int).Mul(bigGwei, big.NewInt(config.MaxFeeCap))
client := clients[0]
chainID, err := client.ChainID(ctx)
if err != nil {
return fmt.Errorf("failed to fetch chainID: %w", err)
}
signer := types.LatestSignerForChainID(chainID)
log.Info("Creating transaction sequences...")
txGenerator := func(key *ecdsa.PrivateKey, nonce uint64) (*types.Transaction, error) {
cryptoAddr := ethcrypto.PubkeyToAddress(key.PublicKey)
addr := common.BytesToAddress(cryptoAddr[:])
return types.SignNewTx(key, signer, &types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Gas: ethparams.TxGas,
To: &addr,
Data: nil,
Value: common.Big0,
})
}
txSequenceStart := time.Now()
txSequences, err := txs.GenerateTxSequences(ctx, txGenerator, clients[0], pks, config.TxsPerWorker, false)
if err != nil {
return err
}
log.Info("Created transaction sequences successfully", "time", time.Since(txSequenceStart))
workers := make([]txs.Worker[*types.Transaction], 0, len(clients))
for i, client := range clients {
cryptoAddr := ethcrypto.PubkeyToAddress(pks[i].PublicKey)
commonAddr := common.BytesToAddress(cryptoAddr[:])
workers = append(workers, NewSingleAddressTxWorker(ctx, client, commonAddr))
}
loader := New(workers, txSequences, config.BatchSize, m)
err = loader.Execute(ctx)
prerr := m.Print(config.MetricsOutput) // Print regardless of execution error
if prerr != nil {
log.Warn("Failed to print metrics", "error", prerr)
}
return err
}