Skip to content
Closed
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
46 changes: 46 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ type LoadConfig struct {
Funding *FundingConfig `json:"funding,omitempty"`
// Path to write a JSON report of the load test.
ReportPath string `json:"reportPath,omitempty"`
// gasFeeCapWei is the fee cap every transaction this run sends declares,
// resolved once from the chain at startup.
//
// It is deliberately not a profile field. A fee cap written down is a number
// that goes stale the next time the chain reprices, and a cap under the base
// fee is rejected before the transaction reaches the EVM. Sei's live networks
// have moved past two of the three caps this repo used to hard-code.
gasFeeCapWei *big.Int
// Seed roots the PRNG behind every workload draw: key and size
// distributions, gas pickers, operation mixes, and account selection. The
// same seed and the same config reproduce the same draw sequence.
Expand Down Expand Up @@ -159,6 +167,44 @@ func (c *LoadConfig) GetChainID() *big.Int {
return big.NewInt(c.ChainID)
}

// SetGasFeeCap records the cap resolved from the chain. The preparation step
// calls it once, before anything is signed.
func (c *LoadConfig) SetGasFeeCap(wei *big.Int) {
c.gasFeeCapWei = new(big.Int).Set(wei)
}

// GetGasFeeCap returns the cap resolved from the chain, and whether one was
// resolved.
//
// It reports absence rather than a default, because a default is what the three
// hard-coded caps were. A caller that cannot proceed without one says so.
func (c *LoadConfig) GetGasFeeCap() (*big.Int, bool) {
if c.gasFeeCapWei == nil {
return nil, false
}
return new(big.Int).Set(c.gasFeeCapWei), true
}

// GetGasFeeCapMultiplier returns what to scale the chain's reported gas price by.
func (c *LoadConfig) GetGasFeeCapMultiplier() float64 {
if c.Settings == nil || c.Settings.GasFeeCapMultiplier < 1 {
return DefaultSettings().GasFeeCapMultiplier
}
return c.Settings.GasFeeCapMultiplier
}

// GetGasMargin returns the margin to apply to what the chain quotes for a call.
//
// It falls back to the default when a config carries no settings, which is how a
// config assembled in code rather than parsed from a profile arrives. A margin
// of zero would declare no gas at all.
func (c *LoadConfig) GetGasMargin() float64 {
if c.Settings == nil || c.Settings.GasMargin < 1 {
return DefaultSettings().GasMargin
}
return c.Settings.GasMargin
}

// AccountConfig stores the configuration for account generation.
type AccountConfig struct {
NewAccountRate float64 `json:"newAccountRate,omitempty"`
Expand Down
23 changes: 23 additions & 0 deletions config/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,38 @@ const (
OpERC721Mint = "erc721_mint"
// OpDisperseEther is Disperse disperseEtherFixed(address[]).
OpDisperseEther = "disperse_ether"
// OpSwapAToB is AMM swapAToB(uint256), the A-for-B leg of a pair.
OpSwapAToB = "swap_a_to_b"
// OpSwapBToA is AMM swapBToA(uint256), the B-for-A leg.
OpSwapBToA = "swap_b_to_a"
)

// StorageRWOperations is the operation set the storagerw scenario draws from.
var StorageRWOperations = NewOperationSet(OpRmw, OpRead, OpWrite)

// AMMOperations is the operation set the amm scenario draws from: both legs of
// one pair, so a profile can weight the direction. A profile that sets no mix
// draws the first name for every transaction, which is one direction — the
// contract holds its reserves between a floor and a ceiling, so that run prices
// the same at its end as at its start.
var AMMOperations = NewOperationSet(OpSwapAToB, OpSwapBToA)

// scenarioOperations maps a scenario's wire name, lowercased, to the operations
// it supports. A scenario absent from the table supports none.
var scenarioOperations = map[string]*OperationSet{
"storagerw": StorageRWOperations,
"amm": AMMOperations,
}

// OperationNamesFor returns every operation a scenario can draw, or nil if it
// draws none. It is exported so a test can check the whole vocabulary a
// scenario reaches the metrics with, rather than only its default.
func OperationNamesFor(scenario string) []string {
set := operationsFor(scenario)
if set == nil {
return nil
}
return set.Names()
}

// operationsFor returns the operations a scenario supports, or nil if it
Expand Down
40 changes: 40 additions & 0 deletions config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,34 @@ type Settings struct {
// coordinated-omission fix), "closed_loop" (default) keeps the legacy
// generate-then-send lockstep as the regression baseline.
ArrivalModel string `json:"arrivalModel,omitempty"`
// GasMargin multiplies what the chain quotes for a call, to give the limit
// room the quote does not carry.
//
// It is a margin on execution, not on calldata: the calldata part is a closed
// form over the exact bytes on the wire and needs none. The quote itself
// already carries about 1.5%, because the node stops its search once the
// bracket is that tight.
//
// Erring high is close to free and erring low is not. Sei fills a block
// against two budgets: one charged at the declared limit and one charged at
// what the transaction spends, and the declared one binds only past four
// times the spend. Below that, margin costs no block space, only the balance
// each in-flight transaction locks. A limit under what a call needs, by
// contrast, lands in a block, burns the whole limit, and reports as sent.
GasMargin float64 `json:"gasMargin,omitempty"`
// GasFeeCapMultiplier scales what the chain reports gas costs into the fee
// cap every transaction declares.
//
// The cap is a ceiling, not a price: a transaction pays the base fee and the
// cap only says how high it will follow one. So a generous multiple costs
// nothing per transaction. What it does cost is balance, because the chain
// locks the cap times the gas limit while a transaction is in flight.
//
// It needs to be generous because the base fee moves. Sei raises it by up to
// about 1.9% per block while blocks are full, which is the state a load run
// is trying to produce, and a transaction whose cap the base fee has passed
// is rejected before it reaches the EVM.
GasFeeCapMultiplier float64 `json:"gasFeeCapMultiplier,omitempty"`
// MaxInFlight bounds concurrent in-flight sends in the open-loop model;
// txs that would exceed it at their scheduled instant are dropped and
// counted rather than throttling the arrival clock.
Expand All @@ -56,6 +84,12 @@ func (s Settings) Validate() error {
if s.MaxInFlight <= 0 {
return fmt.Errorf("MaxInFlight = %v, want > 0", s.MaxInFlight)
}
if s.GasFeeCapMultiplier < 1 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] NaN < 1 and +Inf < 1 are both false, so a config carrying either passes validation. GetGasFeeCapMultiplier (config/config.go) repeats the same < 1 test and hands the value straight through, and scaleWei then evaluates int64(factor * scale) at generator/fee.go:50 — an out-of-range float-to-int conversion is implementation-defined in Go and yields MinInt64 on amd64, producing a large negative fee cap that every subsequent transaction declares. A finite but huge multiplier (>~9.2e15) overflows the same conversion.

Suggest rejecting non-finite values and bounding the upper end explicitly, e.g. if math.IsNaN(s.GasFeeCapMultiplier) || s.GasFeeCapMultiplier < 1 || s.GasFeeCapMultiplier > someMax.

return fmt.Errorf("GasFeeCapMultiplier = %v, want >= 1: a cap under what the chain charges is rejected before the transaction reaches the EVM", s.GasFeeCapMultiplier)
}
if s.GasMargin < 1 {
return fmt.Errorf("GasMargin = %v, want >= 1: a margin below 1 declares less gas than the chain quoted, so every transaction burns its limit", s.GasMargin)
}
return nil
}

Expand All @@ -80,6 +114,8 @@ func DefaultSettings() Settings {
PostSummaryFlushDelay: Duration(25 * time.Second),
ArrivalModel: ArrivalModelClosedLoop,
MaxInFlight: 10_000,
GasMargin: 1.20,
GasFeeCapMultiplier: 5,
}
}

Expand Down Expand Up @@ -133,6 +169,8 @@ func InitializeViper(cmd *cobra.Command) error {
viper.SetDefault("postSummaryFlushDelay", defaults.PostSummaryFlushDelay.ToDuration())
viper.SetDefault("arrivalModel", defaults.ArrivalModel)
viper.SetDefault("maxInFlight", defaults.MaxInFlight)
viper.SetDefault("gasMargin", defaults.GasMargin)
viper.SetDefault("gasFeeCapMultiplier", defaults.GasFeeCapMultiplier)
return nil
}

Expand Down Expand Up @@ -177,5 +215,7 @@ func ResolveSettings() *Settings {
PostSummaryFlushDelay: Duration(viper.GetDuration("postSummaryFlushDelay")),
ArrivalModel: viper.GetString("arrivalModel"),
MaxInFlight: viper.GetInt("maxInFlight"),
GasMargin: viper.GetFloat64("gasMargin"),
GasFeeCapMultiplier: viper.GetFloat64("gasFeeCapMultiplier"),
}
}
18 changes: 15 additions & 3 deletions config/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ func TestDefaultSettings(t *testing.T) {
PostSummaryFlushDelay: Duration(25 * time.Second),
ArrivalModel: ArrivalModelClosedLoop,
MaxInFlight: 10_000,
GasMargin: 1.20,
GasFeeCapMultiplier: 5,
}

if defaults != expected {
Expand All @@ -170,22 +172,32 @@ func TestSettingsValidate(t *testing.T) {
}{
{
name: "positive max-in-flight is valid",
settings: Settings{MaxInFlight: 1},
settings: Settings{MaxInFlight: 1, GasMargin: 1, GasFeeCapMultiplier: 1},
},
{
name: "default settings are valid",
settings: DefaultSettings(),
},
{
name: "zero max-in-flight is rejected",
settings: Settings{MaxInFlight: 0},
settings: Settings{MaxInFlight: 0, GasMargin: 1, GasFeeCapMultiplier: 1},
wantErr: "MaxInFlight = 0, want > 0",
},
{
name: "negative max-in-flight is rejected",
settings: Settings{MaxInFlight: -1},
settings: Settings{MaxInFlight: -1, GasMargin: 1, GasFeeCapMultiplier: 1},
wantErr: "MaxInFlight = -1, want > 0",
},
{
name: "a fee cap multiplier below one is rejected",
settings: Settings{MaxInFlight: 1, GasMargin: 1, GasFeeCapMultiplier: 0.5},
wantErr: "GasFeeCapMultiplier = 0.5, want >= 1",
},
{
name: "a margin below one is rejected",
settings: Settings{MaxInFlight: 1, GasMargin: 0.9, GasFeeCapMultiplier: 1},
wantErr: "GasMargin = 0.9, want >= 1",
},
}

for _, tt := range tests {
Expand Down
8 changes: 6 additions & 2 deletions funder/funder.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ func FundAccounts(ctx context.Context, cfg *config.LoadConfig, root types.Accoun
return fmt.Errorf("funder: transactor: %w", err)
}
auth.Context = ctx
auth.GasTipCap = big.NewInt(1_000_000_000) // 1 gwei (chain min fee)
auth.GasFeeCap = big.NewInt(100_000_000_000) // 100 gwei
feeCap, ok := cfg.GetGasFeeCap()
if !ok {
return fmt.Errorf("funder: no fee cap resolved from the chain")
}
auth.GasTipCap = big.NewInt(1_000_000_000) // 1 gwei (chain min fee)
auth.GasFeeCap = feeCap

disperse, err := deployDisperse(ctx, client, auth)
if err != nil {
Expand Down
Loading
Loading