-
Notifications
You must be signed in to change notification settings - Fork 3
feat(gas): ask the chain what a call costs instead of hard-coding it #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
base: main
Are you sure you want to change the base?
Changes from all commits
33d1584
6c7e41d
137675a
7ce00e6
6ce1027
fa93aa7
010bd04
511fc8f
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,161 @@ | ||
| package generator | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum" | ||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/ethereum/go-ethereum/core" | ||
| "github.com/ethereum/go-ethereum/ethclient" | ||
|
|
||
| "github.com/sei-protocol/sei-load/generator/scenarios" | ||
| "github.com/sei-protocol/sei-load/types" | ||
| loadutils "github.com/sei-protocol/sei-load/utils" | ||
| ) | ||
|
|
||
| // gasMeasureTimeout bounds the whole pricing step. ethclient over HTTP sets no | ||
| // deadline of its own, so an endpoint that accepts the connection and never | ||
| // answers would hold startup open with nothing logged. | ||
| const gasMeasureTimeout = 60 * time.Second | ||
|
|
||
| // gasEstimateCap is the largest limit a node will estimate. Sei caps | ||
| // eth_estimateGas at its simulation gas limit, so a shape needing more than this | ||
| // cannot be priced at all and the error names the allowance rather than the | ||
| // shape. | ||
| const gasEstimateCap = 10_000_000 | ||
|
|
||
| // measureGasLimits asks the chain what each scenario's calls cost, and stores | ||
| // the answer for the run. | ||
| // | ||
| // It runs after every contract is bound, because a call is priced against the | ||
| // deployment the run will actually drive. It runs before funding, because | ||
| // pricing needs no funded account: the estimate carries no fee cap, so the node | ||
| // does not check the caller's balance. | ||
| // | ||
| // A failure here stops the run. The alternative is a hard-coded limit, and a | ||
| // limit below what a call needs does not fail visibly: the transaction reaches a | ||
| // block, burns the whole limit, and is reported as sent. Refusing to start says | ||
| // so once, at startup, instead of publishing a throughput number that is a | ||
| // fabrication. | ||
| func (g *generatorBuilder) measureGasLimits(ctx context.Context, client *ethclient.Client, bindings []*binding) error { | ||
| type priced struct { | ||
| name string | ||
| address common.Address | ||
| price scenarios.GasEstimateCaller | ||
| } | ||
| var work []priced | ||
| for _, b := range bindings { | ||
| for _, instance := range b.instances { | ||
| if price := instance.Scenario.GasEstimateCaller(); price != nil { | ||
| work = append(work, priced{instance.Name, b.address, price}) | ||
| } | ||
| } | ||
| } | ||
| // A profile of native transfers alone prices nothing, so it reads no header | ||
| // and issues no estimate. Startup costs what the profile asks for. | ||
| if len(work) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| return loadutils.WithinBudget(ctx, gasMeasureTimeout, "gas measurement", func(ctx context.Context) error { | ||
| blockGasLimit, err := blockGasLimit(ctx, client) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| margin := g.config.GetGasMargin() | ||
| for _, w := range work { | ||
| estimate := gasEstimator(client, w.address, w.name, margin, blockGasLimit) | ||
| if err := w.price(ctx, estimate); err != nil { | ||
| return fmt.Errorf("price %s: %w", w.name, err) | ||
| } | ||
| } | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| // gasEstimator returns the estimator one scenario's calls are priced through. | ||
| func gasEstimator(client *ethclient.Client, address common.Address, name string, | ||
| margin float64, blockGasLimit uint64) scenarios.GasEstimator { | ||
| return func(ctx context.Context, call scenarios.GasEstimateCall) (scenarios.GasModel, error) { | ||
| // The three fee fields stay unset. A call carrying one makes the node | ||
| // check the caller's balance, and this caller has none by design: it is a | ||
| // fresh address chosen so every slot the call writes is still zero. | ||
| required, err := client.EstimateGas(ctx, ethereum.CallMsg{ | ||
| From: types.NewAccount(false).Address, | ||
| To: &address, | ||
| Data: call.Data, | ||
| Value: call.Value, | ||
|
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. Unfunded estimate From rejects payable callsHigh Severity The estimate now forwards Additional Locations (1)Reviewed by Cursor Bugbot for commit 511fc8f. Configure here. |
||
| }) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| if err != nil { | ||
| return scenarios.GasModel{}, err | ||
| } | ||
|
|
||
| intrinsic, err := core.IntrinsicGas(call.Data, nil, nil, false, true, true, true) | ||
| if err != nil { | ||
| return scenarios.GasModel{}, fmt.Errorf("intrinsic gas: %w", err) | ||
| } | ||
| if required <= intrinsic { | ||
| return scenarios.GasModel{}, fmt.Errorf( | ||
| "the chain quoted %d, at or under this call's own intrinsic cost of %d", required, intrinsic) | ||
| } | ||
|
|
||
| model := scenarios.GasModel{Exec: required - intrinsic, Margin: margin} | ||
| limit, err := model.Limit(call.Data) | ||
| if err != nil { | ||
| return scenarios.GasModel{}, err | ||
| } | ||
| // Against the priced call's own bytes. A scenario whose calldata varies | ||
| // recomposes per transaction and can exceed this without the check seeing | ||
| // it: StorageRW at its largest pad wants several times what its empty-pad | ||
| // probe does. Covering that needs the size distribution's maximum visible | ||
| // here, which it is not. | ||
| if limit > blockGasLimit { | ||
| return scenarios.GasModel{}, fmt.Errorf( | ||
| "needs %d gas for the call priced here, past the %d this run will admit, so no limit carries it", | ||
| limit, blockGasLimit) | ||
| } | ||
| log.Printf("⛽ %s/%s: chain quoted %d, limit %d (margin %.2f)", name, call.Operation, required, limit, margin) | ||
| return model, nil | ||
| } | ||
| } | ||
|
|
||
| // blockGasLimit reads what one block admits, so a priced call that cannot fit in | ||
| // any block fails at startup rather than on every send. | ||
| func blockGasLimit(ctx context.Context, client *ethclient.Client) (uint64, error) { | ||
| header, err := client.HeaderByNumber(ctx, nil) | ||
| if err != nil { | ||
| return 0, fmt.Errorf("read the latest header for the block gas limit: %w", err) | ||
| } | ||
| // The smaller of what a block admits and what a node will estimate, so the | ||
| // error above names the bound that actually applies. | ||
| return min(header.GasLimit, gasEstimateCap), nil | ||
| } | ||
|
|
||
| // mockGasLimitExec is what a dry run reports as the execution term. A dry run | ||
| // reaches no chain, so nothing here is a measurement. One shared value keeps | ||
| // that obvious: a per-scenario number would read like one. | ||
| const mockGasLimitExec = 200_000 | ||
|
|
||
| // mockGasLimits gives every scenario a limit without asking a chain, so a dry | ||
| // run can preview a profile. It is not a measurement and the log says so. | ||
| func (g *generatorBuilder) mockGasLimits(ctx context.Context, bindings []*binding) error { | ||
| log.Printf("⛽ dry run: gas limits are not measured") | ||
| for _, b := range bindings { | ||
| for _, instance := range b.instances { | ||
| price := instance.Scenario.GasEstimateCaller() | ||
| if price == nil { | ||
| continue | ||
| } | ||
| estimate := func(context.Context, scenarios.GasEstimateCall) (scenarios.GasModel, error) { | ||
| return scenarios.GasModel{Exec: mockGasLimitExec, Margin: g.config.GetGasMargin()}, nil | ||
| } | ||
| if err := price(ctx, estimate); err != nil { | ||
| return fmt.Errorf("price %s: %w", instance.Name, err) | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.