Skip to content
Open
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
56 changes: 56 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"encoding/json"
"fmt"
"math/big"
"strings"
"time"

"github.com/ethereum/go-ethereum/common"
)

// LoadConfig stores the configuration for load-related settings.
Expand Down Expand Up @@ -57,6 +60,14 @@ func (c *LoadConfig) GetChainID() *big.Int {
type AccountConfig struct {
NewAccountRate float64 `json:"newAccountRate,omitempty"`
Accounts int `json:"count,omitempty"`
// DeterministicEvmStressKeys uses types.EvmStressPrivateKey (sender indices
// 1..count). Fund those Cosmos accounts in genesis to match the pool.
DeterministicEvmStressKeys bool `json:"deterministicEvmStressKeys,omitempty"`
// SingleUseSenders gives each pooled account at most one turn as sender;
// when exhausted, generation stops (ok=false). One transaction per pooled sender.
// Incompatible with newAccountRate > 0 (validated at pool creation) and with
// settings.prewarm (validated at startup via ValidatePrewarmAccountPools).
SingleUseSenders bool `json:"singleUseSenders,omitempty"`
}

// Scenario represents each scenario in the load configuration.
Expand All @@ -67,4 +78,49 @@ type Scenario struct {
GasPicker *GasPicker `json:"gasPicker,omitempty"`
GasFeeCapPicker *GasPicker `json:"gasFeeCapPicker,omitempty"`
GasTipCapPicker *GasPicker `json:"gasTipCapPicker,omitempty"`
// FixedReceiver is an optional hex EVM address. When set, all transactions
// in this scenario are sent to this single address (single-recipient stress
// mode). If empty, the receiver is picked from the account pool as usual.
FixedReceiver string `json:"fixedReceiver,omitempty"`
}

// ValidatePrewarmAccountPools returns an error if prewarm is enabled while any
// account pool uses singleUseSenders. Prewarm iterates the same pools and is
// incompatible with single-use exhaustion semantics.
func ValidatePrewarmAccountPools(cfg *LoadConfig, prewarm bool) error {
if !prewarm || cfg == nil {
return nil
}
if cfg.Accounts != nil && cfg.Accounts.SingleUseSenders {
return fmt.Errorf("settings.prewarm cannot be used with accounts.singleUseSenders (prewarm shares the same account pool)")
}
for i, sc := range cfg.Scenarios {
if sc.Accounts != nil && sc.Accounts.SingleUseSenders {
return fmt.Errorf("settings.prewarm cannot be used with scenarios[%d] (%q) accounts.singleUseSenders", i, sc.Name)
}
}
return nil
}

// ValidateFixedReceiverAddresses returns an error if any scenario has a
// non-empty fixedReceiver that is not a valid 20-byte hex address (0x + 40 hex).
// Malformed values would otherwise be passed to common.HexToAddress and can
// map to the zero address without an obvious failure at config load time.
func ValidateFixedReceiverAddresses(cfg *LoadConfig) error {
if cfg == nil {
return nil
}
for i, sc := range cfg.Scenarios {
addr := strings.TrimSpace(sc.FixedReceiver)
if addr == "" {
continue
}
if !common.IsHexAddress(addr) {
return fmt.Errorf(
"scenarios[%d] (%q): invalid fixedReceiver %q (want 0x-prefixed 40 hex characters)",
i, sc.Name, sc.FixedReceiver,
)
}
}
return nil
}
31 changes: 31 additions & 0 deletions config/fixed_receiver_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateFixedReceiverAddresses(t *testing.T) {
require.NoError(t, ValidateFixedReceiverAddresses(nil))
require.NoError(t, ValidateFixedReceiverAddresses(&LoadConfig{}))
require.NoError(t, ValidateFixedReceiverAddresses(&LoadConfig{
Scenarios: []Scenario{
{Name: "A", FixedReceiver: ""},
{Name: "B", FixedReceiver: " "},
{Name: "C", FixedReceiver: "0x0000000000000000000000000000000000000000"},
{Name: "D", FixedReceiver: "0xDC5b20847F43d67928F49Cd4f85D696b5A7617B5"},
},
}))

err := ValidateFixedReceiverAddresses(&LoadConfig{
Scenarios: []Scenario{{Name: "bad", FixedReceiver: "0xinvalid"}},
})
require.Error(t, err)
require.Contains(t, err.Error(), "fixedReceiver")

err = ValidateFixedReceiverAddresses(&LoadConfig{
Scenarios: []Scenario{{Name: "short", FixedReceiver: "0xabc"}},
})
require.Error(t, err)
}
29 changes: 29 additions & 0 deletions config/prewarm_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestValidatePrewarmAccountPools(t *testing.T) {
require.NoError(t, ValidatePrewarmAccountPools(nil, true))
require.NoError(t, ValidatePrewarmAccountPools(&LoadConfig{}, false))
require.NoError(t, ValidatePrewarmAccountPools(&LoadConfig{
Accounts: &AccountConfig{SingleUseSenders: true},
}, false))

err := ValidatePrewarmAccountPools(&LoadConfig{
Accounts: &AccountConfig{SingleUseSenders: true},
}, true)
require.Error(t, err)
require.Contains(t, err.Error(), "prewarm")

err = ValidatePrewarmAccountPools(&LoadConfig{
Scenarios: []Scenario{
{Name: "S", Accounts: &AccountConfig{SingleUseSenders: true}},
},
}, true)
require.Error(t, err)
require.Contains(t, err.Error(), "scenarios[0]")
}
64 changes: 42 additions & 22 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ type Generator interface {

// scenarioInstance represents a scenario instance with its configuration
type scenarioInstance struct {
Name string
Weight int
Scenario scenarios.TxGenerator
Accounts types.AccountPool
Deployed bool
Name string
Weight int
Scenario scenarios.TxGenerator
ScenarioConfig config.Scenario
Accounts types.AccountPool
Deployed bool
}

func accountsFromConfig(ac *config.AccountConfig) []*types.Account {
if ac == nil {
return nil
}
if ac.DeterministicEvmStressKeys {
return types.GenerateEvmStressSenderAccounts(ac.Accounts)
}
return types.GenerateAccounts(ac.Accounts)
}

// configBasedGenerator manages scenario creation and deployment from config
Expand All @@ -47,11 +58,16 @@ func (g *configBasedGenerator) createScenarios() error {

// Create shared account pool if top-level account config exists
if g.config.Accounts != nil {
accounts := types.GenerateAccounts(g.config.Accounts.Accounts)
g.sharedAccounts = types.NewAccountPool(&types.AccountConfig{
Accounts: accounts,
NewAccountRate: g.config.Accounts.NewAccountRate,
accounts := accountsFromConfig(g.config.Accounts)
pool, err := types.NewAccountPool(&types.AccountConfig{
Accounts: accounts,
NewAccountRate: g.config.Accounts.NewAccountRate,
SingleUseSenders: g.config.Accounts.SingleUseSenders,
})
if err != nil {
return fmt.Errorf("shared account pool: %w", err)
}
g.sharedAccounts = pool
g.accountPools = append(g.accountPools, g.sharedAccounts)
}

Expand All @@ -63,14 +79,17 @@ func (g *configBasedGenerator) createScenarios() error {
var accountPool types.AccountPool
if scenarioCfg.Accounts != nil {
// Scenario defines its own account settings - create separate pool
accountCount := scenarioCfg.Accounts.Accounts
newAccountRate := scenarioCfg.Accounts.NewAccountRate

accounts := types.GenerateAccounts(accountCount)
accountPool = types.NewAccountPool(&types.AccountConfig{
Accounts: accounts,
NewAccountRate: newAccountRate,
sa := scenarioCfg.Accounts
accounts := accountsFromConfig(sa)
pool, err := types.NewAccountPool(&types.AccountConfig{
Accounts: accounts,
NewAccountRate: sa.NewAccountRate,
SingleUseSenders: sa.SingleUseSenders,
})
if err != nil {
return fmt.Errorf("scenario %q account pool: %w", scenarioCfg.Name, err)
}
accountPool = pool
g.accountPools = append(g.accountPools, accountPool)
} else if g.sharedAccounts != nil {
// Use shared account pool from top-level config
Expand Down Expand Up @@ -98,11 +117,12 @@ func (g *configBasedGenerator) createScenarios() error {

// Create scenario instance
instance := &scenarioInstance{
Name: name,
Weight: scenarioCfg.Weight,
Scenario: scenario,
Accounts: accountPool,
Deployed: false,
Name: name,
Weight: scenarioCfg.Weight,
Scenario: scenario,
ScenarioConfig: scenarioCfg,
Accounts: accountPool,
Deployed: false,
}

g.instances = append(g.instances, instance)
Expand Down Expand Up @@ -170,7 +190,7 @@ func (g *configBasedGenerator) createWeightedGenerator() (Generator, error) {
continue
}
// Create a scenarioGenerator for this scenario instance
gen := NewScenarioGenerator(instance.Accounts, instance.Scenario)
gen := NewScenarioGenerator(instance.Accounts, instance.Scenario, instance.ScenarioConfig)

// Add to weighted config with the specified weight
weightedConfigs = append(weightedConfigs, WeightedConfig(instance.Weight, gen))
Expand Down
24 changes: 21 additions & 3 deletions generator/scenario.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
package generator

import (
"strings"
"sync"

"github.com/ethereum/go-ethereum/common"

"github.com/sei-protocol/sei-load/config"
"github.com/sei-protocol/sei-load/generator/scenarios"
"github.com/sei-protocol/sei-load/types"
)

type scenarioGenerator struct {
scenario scenarios.TxGenerator
accountPool types.AccountPool
scenarioCfg config.Scenario
mu sync.RWMutex
}

func NewScenarioGenerator(accounts types.AccountPool,
txg scenarios.TxGenerator) Generator {
txg scenarios.TxGenerator, scenarioCfg config.Scenario) Generator {
return &scenarioGenerator{
scenario: txg,
accountPool: accounts,
scenarioCfg: scenarioCfg,
}
}

Expand All @@ -35,11 +41,23 @@ func (g *scenarioGenerator) GenerateN(n int) []*types.LoadTx {

func (g *scenarioGenerator) Generate() (*types.LoadTx, bool) {
sender := g.accountPool.NextAccount()
receiver := g.accountPool.NextAccount()
if sender == nil {
return nil, false
}
var receiver common.Address
if addr := strings.TrimSpace(g.scenarioCfg.FixedReceiver); addr != "" {
receiver = common.HexToAddress(addr)
} else {
rcv := g.accountPool.NextAccount()
if rcv == nil {
return nil, false
}
receiver = rcv.Address
}
return g.scenario.Generate(&types.TxScenario{
Name: g.scenario.Name(),
Sender: sender,
Receiver: receiver.Address,
Receiver: receiver,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

singleUseSenders without fixedReceiver consumes accounts as receivers

Medium Severity

When singleUseSenders is true but fixedReceiver is empty, Generate() calls NextAccount() twice per transaction—once for the sender and once for the receiver. Both calls advance the single-use pool index, so half the accounts are consumed as receivers and never get their "one turn as sender." With N accounts this produces only N/2 transactions instead of N. There's no validation preventing this misconfiguration even though singleUseSenders only works correctly with fixedReceiver.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 93fbf10. Configure here.

}), true
}

Expand Down
41 changes: 41 additions & 0 deletions generator/scenario_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package generator

import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"

"github.com/sei-protocol/sei-load/config"
"github.com/sei-protocol/sei-load/generator/scenarios"
"github.com/sei-protocol/sei-load/types"
)

func TestScenarioGenerator_FixedReceiverUsesOnePoolAccount(t *testing.T) {
accs := types.GenerateAccounts(2)
pool, err := types.NewAccountPool(&types.AccountConfig{
Accounts: accs,
NewAccountRate: 0,
SingleUseSenders: true,
})
require.NoError(t, err)
evm := scenarios.NewEVMTransferScenario(config.Scenario{})
cfg := &config.LoadConfig{ChainID: 713714}
evm.Deploy(cfg, accs[0])

fixed := "0x00000000000000000000000000000000000000aa"
want := common.HexToAddress(fixed)
gen := NewScenarioGenerator(pool, evm, config.Scenario{FixedReceiver: fixed})

tx1, ok := gen.Generate()
require.True(t, ok)
require.NotNil(t, tx1)
require.Equal(t, want, tx1.Scenario.Receiver)

tx2, ok := gen.Generate()
require.True(t, ok)
require.NotNil(t, tx2)

_, ok = gen.Generate()
require.False(t, ok)
}
6 changes: 5 additions & 1 deletion generator/scenarios/Disperse.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ type DisperseScenario struct {
func NewDisperseScenario(cfg config.Scenario) TxGenerator {
scenario := &DisperseScenario{}
scenario.ContractScenarioBase = NewContractScenarioBase[bindings.Disperse](scenario, cfg)
scenario.pool = types.NewAccountPool(&types.AccountConfig{
pool, err := types.NewAccountPool(&types.AccountConfig{
NewAccountRate: 1.0,
})
if err != nil {
panic("disperse account pool: " + err.Error())
}
scenario.pool = pool
return scenario
}

Expand Down
Loading
Loading