-
Notifications
You must be signed in to change notification settings - Fork 3
feat: read gas limits, fee caps and contract addresses from the chain #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
33d1584
6c7e41d
137675a
7ce00e6
60d63a0
d51fb14
26c1a64
a773910
b0e54d4
6ce1027
fa93aa7
010bd04
511fc8f
6f0233d
ee754ed
f88fea4
47202fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package generator | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
| "math/big" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/ethclient" | ||
|
|
||
| loadutils "github.com/sei-protocol/sei-load/utils" | ||
| ) | ||
|
|
||
| // feeCapTimeout bounds the one call that resolves the fee cap. ethclient over | ||
| // HTTP sets no deadline of its own. | ||
| const feeCapTimeout = 30 * time.Second | ||
|
|
||
| // resolveGasFeeCap asks the chain what gas costs and records the cap every | ||
| // transaction this run declares. | ||
| // | ||
| // It runs before anything is signed, because a contract deployment carries a cap | ||
| // too and a deployment priced under the base fee is rejected the same way a load | ||
| // transaction is. | ||
| // | ||
| // The alternative was a constant, and this repo carried three of them: 20 gwei | ||
| // for a contract call, 100 for a deployment, 200 for a native transfer. Sei's | ||
| // live base fee is 50, so one of the three was already rejecting every | ||
| // transaction it priced and the other two were guesses that happened to clear. | ||
| func (g *generatorBuilder) resolveGasFeeCap(ctx context.Context, client *ethclient.Client) error { | ||
| return loadutils.WithinBudget(ctx, feeCapTimeout, "fee cap", func(ctx context.Context) error { | ||
| suggested, err := client.SuggestGasPrice(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("ask the chain what gas costs: %w", err) | ||
| } | ||
| if suggested.Sign() <= 0 { | ||
| return fmt.Errorf("the chain reported a gas price of %s, so no cap can be derived from it", suggested) | ||
| } | ||
| cap := scaleWei(suggested, g.config.GetGasFeeCapMultiplier()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] Still open from the previous round: the cap is derived from the chain but the tip stayed a constant. |
||
| g.config.SetGasFeeCap(cap) | ||
| log.Printf("⛽ gas price %s wei, fee cap %s wei (x%.1f)", suggested, cap, g.config.GetGasFeeCapMultiplier()) | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| // scaleWei multiplies a wei amount by a fractional factor without leaving the | ||
| // integer domain, so a large price cannot lose precision through float64. | ||
| func scaleWei(wei *big.Int, factor float64) *big.Int { | ||
| const scale = 1000 | ||
| num := big.NewInt(int64(factor * scale)) | ||
| out := new(big.Int).Mul(wei, num) | ||
| return out.Div(out, big.NewInt(scale)) | ||
| } | ||
|
|
||
| // mockGasFeeCap records a placeholder cap for a run that reaches no chain. | ||
| func (g *generatorBuilder) mockGasFeeCap() { | ||
| g.config.SetGasFeeCap(big.NewInt(mockGasFeeCapWei)) | ||
| } | ||
|
|
||
| // mockGasFeeCapWei is what a dry run declares. A dry run sends nothing, so this | ||
| // is a placeholder rather than a measurement. | ||
| const mockGasFeeCapWei = 100_000_000_000 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package generator | ||
|
|
||
| import ( | ||
| "math" | ||
| "math/big" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // seiBaseFeeGrowthPerBlock is the most Sei raises the base fee in one block | ||
| // while blocks are full, which is the state a load run exists to produce. | ||
| const seiBaseFeeGrowthPerBlock = 1.019 | ||
|
|
||
| // blocksOfHeadroom returns how many consecutive full blocks the base fee can | ||
| // climb through before it passes cap. | ||
| func blocksOfHeadroom(cap *big.Int, baseFee int64) float64 { | ||
| ratio, _ := new(big.Float).Quo(new(big.Float).SetInt(cap), big.NewFloat(float64(baseFee))).Float64() | ||
| return math.Log(ratio) / math.Log(seiBaseFeeGrowthPerBlock) | ||
| } | ||
|
|
||
| // TestTheFeeCapSurvivesBaseFeeDrift is the property the whole change exists for. | ||
| // | ||
| // A cap under the base fee is rejected by the fee ante before the transaction | ||
| // reaches the EVM, after the nonce is consumed, so it produces a failed receipt | ||
| // rather than no receipt at all. Clearing the base fee at the instant of the | ||
| // read is not enough: the run then fills blocks, which is what makes the base | ||
| // fee climb, so the cap has to clear it by enough to outlive the climb. | ||
| // | ||
| // The fixtures are what the three live networks reported: arctic-1 at 10 gwei | ||
| // base and 11 suggested, pacific-1 and atlantic-2 at 50 and 55. The constant | ||
| // this change removed declared 20 gwei, which cleared the first and not the | ||
| // other two. | ||
| func TestTheFeeCapSurvivesBaseFeeDrift(t *testing.T) { | ||
| // Enough blocks that a run notices the climb and can be restarted, rather | ||
| // than starting to fail seconds after it reaches full blocks. | ||
| const wantBlocks = 30 | ||
|
|
||
| for _, tc := range []struct { | ||
| name string | ||
| baseFee int64 | ||
| suggested int64 | ||
| }{ | ||
| {"arctic-1", 10_000_000_000, 11_000_000_000}, | ||
| {"pacific-1", 50_000_000_000, 55_000_000_000}, | ||
| {"atlantic-2", 50_000_000_000, 55_000_000_000}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| cap := scaleWei(big.NewInt(tc.suggested), 5) | ||
| require.Positive(t, cap.Cmp(big.NewInt(tc.baseFee)), | ||
| "the cap is at or under the base fee, so the ante rejects every transaction before it runs") | ||
| require.GreaterOrEqual(t, blocksOfHeadroom(cap, tc.baseFee), float64(wantBlocks), | ||
| "the cap clears the base fee by only %.0f blocks of growth, so it lapses shortly after the run fills blocks", | ||
| blocksOfHeadroom(cap, tc.baseFee)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestScalingKeepsPrecisionAtChainScale guards the arithmetic. A price is wei, | ||
| // which passes what a float64 holds exactly, so scaling through one would move | ||
| // the cap by an amount nothing else in the run would explain. | ||
| func TestScalingKeepsPrecisionAtChainScale(t *testing.T) { | ||
| huge, ok := new(big.Int).SetString("123456789012345678901234567890", 10) | ||
| require.True(t, ok) | ||
|
|
||
| require.Equal(t, "246913578024691357802469135780", scaleWei(huge, 2).String(), | ||
| "doubling a chain-scale price did not double it, so the cap is derived through a lossy conversion") | ||
| require.Equal(t, "55000000000", scaleWei(big.NewInt(11_000_000_000), 5).String()) | ||
| require.Equal(t, "16500000000", scaleWei(big.NewInt(11_000_000_000), 1.5).String()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package generator_test | ||
|
|
||
| import ( | ||
| "math/big" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/sei-protocol/sei-load/config" | ||
| "github.com/sei-protocol/sei-load/generator" | ||
| "github.com/sei-protocol/sei-load/generator/scenarios" | ||
| "github.com/sei-protocol/sei-load/types" | ||
| ) | ||
|
|
||
| // TestStartupResolvesTheFeeCapFromTheChain drives the real startup path against | ||
| // a chain and asserts the cap it came away with. | ||
| // | ||
| // The arithmetic has its own test. This one asserts startup actually applies it: | ||
| // a resolver that read the price and forgot to scale it would leave the run | ||
| // declaring what the chain charges right now, with no room for the base fee to | ||
| // climb once the run starts filling blocks. | ||
| func TestStartupResolvesTheFeeCapFromTheChain(t *testing.T) { | ||
| chain := newMockChain(t, mockChainConfig{}) | ||
| cfg := &config.LoadConfig{ | ||
| ChainID: 7777, | ||
| Endpoints: []string{chain.url}, | ||
| Accounts: &config.AccountConfig{Accounts: 2}, | ||
| Scenarios: []config.Scenario{{Name: scenarios.ERC20, Weight: 1}}, | ||
| Settings: &config.Settings{GasFeeCapMultiplier: 5, GasMargin: 1.2, MaxInFlight: 10}, | ||
| } | ||
|
|
||
| _, err := generator.NewGenerator(t.Context(), newTestRng(1), cfg, types.NewAccount(false)) | ||
| require.NoError(t, err) | ||
|
|
||
| cap, ok := cfg.GetGasFeeCap() | ||
| require.True(t, ok, "startup finished without resolving a fee cap, so every transaction is priced by nothing") | ||
|
|
||
| want := new(big.Int).Mul(big.NewInt(mockGasPriceWei), big.NewInt(5)) | ||
| require.Equal(t, want.String(), cap.String(), | ||
| "the cap is not the chain's price scaled by the configured multiplier, so it carries no room for the base fee to climb") | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.