Skip to content

Commit 61b844f

Browse files
karalabeorenyomtov
andauthored
eth/gasestimator: allow slight estimation error in favor of less iterations (#28618)
* eth/gasestimator: early exit for plain transfer and error allowance * core, eth/gasestimator: hard guess at a possible required gas * internal/ethapi: update estimation tests with the error ratio * eth/gasestimator: I hate you linter * graphql: fix gas estimation test --------- Co-authored-by: Oren <[email protected]>
1 parent e0c7ad0 commit 61b844f

File tree

5 files changed

+68
-17
lines changed

5 files changed

+68
-17
lines changed

core/state_transition.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ import (
3232
// ExecutionResult includes all output after executing given evm
3333
// message no matter the execution itself is successful or not.
3434
type ExecutionResult struct {
35-
UsedGas uint64 // Total used gas but include the refunded gas
36-
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
37-
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)
35+
UsedGas uint64 // Total used gas, not including the refunded gas
36+
RefundedGas uint64 // Total gas refunded after execution
37+
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
38+
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)
3839
}
3940

4041
// Unwrap returns the internal evm error which allows us for further
@@ -419,12 +420,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
419420
ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
420421
}
421422

423+
var gasRefund uint64
422424
if !rules.IsLondon {
423425
// Before EIP-3529: refunds were capped to gasUsed / 2
424-
st.refundGas(params.RefundQuotient)
426+
gasRefund = st.refundGas(params.RefundQuotient)
425427
} else {
426428
// After EIP-3529: refunds are capped to gasUsed / 5
427-
st.refundGas(params.RefundQuotientEIP3529)
429+
gasRefund = st.refundGas(params.RefundQuotientEIP3529)
428430
}
429431
effectiveTip := msg.GasPrice
430432
if rules.IsLondon {
@@ -442,13 +444,14 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
442444
}
443445

444446
return &ExecutionResult{
445-
UsedGas: st.gasUsed(),
446-
Err: vmerr,
447-
ReturnData: ret,
447+
UsedGas: st.gasUsed(),
448+
RefundedGas: gasRefund,
449+
Err: vmerr,
450+
ReturnData: ret,
448451
}, nil
449452
}
450453

451-
func (st *StateTransition) refundGas(refundQuotient uint64) {
454+
func (st *StateTransition) refundGas(refundQuotient uint64) uint64 {
452455
// Apply refund counter, capped to a refund quotient
453456
refund := st.gasUsed() / refundQuotient
454457
if refund > st.state.GetRefund() {
@@ -463,6 +466,8 @@ func (st *StateTransition) refundGas(refundQuotient uint64) {
463466
// Also return remaining gas to the block gas counter so it is
464467
// available for the next transaction.
465468
st.gp.AddGas(st.gasRemaining)
469+
470+
return refund
466471
}
467472

468473
// gasUsed returns the amount of gas used up by the state transition.

eth/gasestimator/gasestimator.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type Options struct {
4242
Chain core.ChainContext // Chain context to access past block hashes
4343
Header *types.Header // Header defining the block context to execute in
4444
State *state.StateDB // Pre-state on top of which to estimate the gas
45+
46+
ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination
4547
}
4648

4749
// Estimate returns the lowest possible gas limit that allows the transaction to
@@ -86,16 +88,28 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
8688
if transfer == nil {
8789
transfer = new(big.Int)
8890
}
89-
log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance,
91+
log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance,
9092
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance)
9193
hi = allowance.Uint64()
9294
}
9395
}
9496
// Recap the highest gas allowance with specified gascap.
9597
if gasCap != 0 && hi > gasCap {
96-
log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
98+
log.Debug("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
9799
hi = gasCap
98100
}
101+
// If the transaction is a plain value transfer, short circuit estimation and
102+
// directly try 21000. Returning 21000 without any execution is dangerous as
103+
// some tx field combos might bump the price up even for plain transfers (e.g.
104+
// unused access list items). Ever so slightly wasteful, but safer overall.
105+
if len(call.Data) == 0 {
106+
if call.To != nil && opts.State.GetCodeSize(*call.To) == 0 {
107+
failed, _, err := execute(ctx, call, opts, params.TxGas)
108+
if !failed && err == nil {
109+
return params.TxGas, nil, nil
110+
}
111+
}
112+
}
99113
// We first execute the transaction at the highest allowable gas limit, since if this fails we
100114
// can return error immediately.
101115
failed, result, err := execute(ctx, call, opts, hi)
@@ -115,8 +129,35 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
115129
// limit for these cases anyway.
116130
lo = result.UsedGas - 1
117131

132+
// There's a fairly high chance for the transaction to execute successfully
133+
// with gasLimit set to the first execution's usedGas + gasRefund. Explicitly
134+
// check that gas amount and use as a limit for the binary search.
135+
optimisticGasLimit := (result.UsedGas + result.RefundedGas + params.CallStipend) * 64 / 63
136+
if optimisticGasLimit < hi {
137+
failed, _, err = execute(ctx, call, opts, optimisticGasLimit)
138+
if err != nil {
139+
// This should not happen under normal conditions since if we make it this far the
140+
// transaction had run without error at least once before.
141+
log.Error("Execution error in estimate gas", "err", err)
142+
return 0, nil, err
143+
}
144+
if failed {
145+
lo = optimisticGasLimit
146+
} else {
147+
hi = optimisticGasLimit
148+
}
149+
}
118150
// Binary search for the smallest gas limit that allows the tx to execute successfully.
119151
for lo+1 < hi {
152+
if opts.ErrorRatio > 0 {
153+
// It is a bit pointless to return a perfect estimation, as changing
154+
// network conditions require the caller to bump it up anyway. Since
155+
// wallets tend to use 20-25% bump, allowing a small approximation
156+
// error is fine (as long as it's upwards).
157+
if float64(hi-lo)/float64(hi) < opts.ErrorRatio {
158+
break
159+
}
160+
}
120161
mid := (hi + lo) / 2
121162
if mid > lo*2 {
122163
// Most txs don't need much higher gas limit than their gas used, and most txs don't

graphql/graphql_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func TestGraphQLBlockSerialization(t *testing.T) {
139139
// should return `estimateGas` as decimal
140140
{
141141
body: `{"query": "{block{ estimateGas(data:{}) }}"}`,
142-
want: `{"data":{"block":{"estimateGas":"0xcf08"}}}`,
142+
want: `{"data":{"block":{"estimateGas":"0xd221"}}}`,
143143
code: 200,
144144
},
145145
// should return `status` as decimal

internal/ethapi/api.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ import (
5151
"github.com/tyler-smith/go-bip39"
5252
)
5353

54+
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas is
55+
// allowed to produce in order to speed up calculations.
56+
const estimateGasErrorRatio = 0.015
57+
5458
// EthereumAPI provides an API to access Ethereum related information.
5559
type EthereumAPI struct {
5660
b Backend
@@ -1189,10 +1193,11 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
11891193
}
11901194
// Construct the gas estimator option from the user input
11911195
opts := &gasestimator.Options{
1192-
Config: b.ChainConfig(),
1193-
Chain: NewChainContext(ctx, b),
1194-
Header: header,
1195-
State: state,
1196+
Config: b.ChainConfig(),
1197+
Chain: NewChainContext(ctx, b),
1198+
Header: header,
1199+
State: state,
1200+
ErrorRatio: estimateGasErrorRatio,
11961201
}
11971202
// Run the gas estimation andwrap any revertals into a custom return
11981203
call, err := args.ToMessage(gasCap, header.BaseFee)

internal/ethapi/api_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ func TestEstimateGas(t *testing.T) {
735735
t.Errorf("test %d: want no error, have %v", i, err)
736736
continue
737737
}
738-
if uint64(result) != tc.want {
738+
if float64(result) > float64(tc.want)*(1+estimateGasErrorRatio) {
739739
t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, uint64(result), tc.want)
740740
}
741741
}

0 commit comments

Comments
 (0)