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 <orenyomtov@users.noreply.github.com>
This commit is contained in:
parent
e0c7ad01ab
commit
61b844f2b2
@ -32,7 +32,8 @@ import (
|
|||||||
// ExecutionResult includes all output after executing given evm
|
// ExecutionResult includes all output after executing given evm
|
||||||
// message no matter the execution itself is successful or not.
|
// message no matter the execution itself is successful or not.
|
||||||
type ExecutionResult struct {
|
type ExecutionResult struct {
|
||||||
UsedGas uint64 // Total used gas but include the refunded gas
|
UsedGas uint64 // Total used gas, not including the refunded gas
|
||||||
|
RefundedGas uint64 // Total gas refunded after execution
|
||||||
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
|
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
|
||||||
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)
|
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)
|
||||||
}
|
}
|
||||||
@ -419,12 +420,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
|
|||||||
ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
|
ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, msg.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gasRefund uint64
|
||||||
if !rules.IsLondon {
|
if !rules.IsLondon {
|
||||||
// Before EIP-3529: refunds were capped to gasUsed / 2
|
// Before EIP-3529: refunds were capped to gasUsed / 2
|
||||||
st.refundGas(params.RefundQuotient)
|
gasRefund = st.refundGas(params.RefundQuotient)
|
||||||
} else {
|
} else {
|
||||||
// After EIP-3529: refunds are capped to gasUsed / 5
|
// After EIP-3529: refunds are capped to gasUsed / 5
|
||||||
st.refundGas(params.RefundQuotientEIP3529)
|
gasRefund = st.refundGas(params.RefundQuotientEIP3529)
|
||||||
}
|
}
|
||||||
effectiveTip := msg.GasPrice
|
effectiveTip := msg.GasPrice
|
||||||
if rules.IsLondon {
|
if rules.IsLondon {
|
||||||
@ -443,12 +445,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
|
|||||||
|
|
||||||
return &ExecutionResult{
|
return &ExecutionResult{
|
||||||
UsedGas: st.gasUsed(),
|
UsedGas: st.gasUsed(),
|
||||||
|
RefundedGas: gasRefund,
|
||||||
Err: vmerr,
|
Err: vmerr,
|
||||||
ReturnData: ret,
|
ReturnData: ret,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *StateTransition) refundGas(refundQuotient uint64) {
|
func (st *StateTransition) refundGas(refundQuotient uint64) uint64 {
|
||||||
// Apply refund counter, capped to a refund quotient
|
// Apply refund counter, capped to a refund quotient
|
||||||
refund := st.gasUsed() / refundQuotient
|
refund := st.gasUsed() / refundQuotient
|
||||||
if refund > st.state.GetRefund() {
|
if refund > st.state.GetRefund() {
|
||||||
@ -463,6 +466,8 @@ func (st *StateTransition) refundGas(refundQuotient uint64) {
|
|||||||
// Also return remaining gas to the block gas counter so it is
|
// Also return remaining gas to the block gas counter so it is
|
||||||
// available for the next transaction.
|
// available for the next transaction.
|
||||||
st.gp.AddGas(st.gasRemaining)
|
st.gp.AddGas(st.gasRemaining)
|
||||||
|
|
||||||
|
return refund
|
||||||
}
|
}
|
||||||
|
|
||||||
// gasUsed returns the amount of gas used up by the state transition.
|
// gasUsed returns the amount of gas used up by the state transition.
|
||||||
|
@ -42,6 +42,8 @@ type Options struct {
|
|||||||
Chain core.ChainContext // Chain context to access past block hashes
|
Chain core.ChainContext // Chain context to access past block hashes
|
||||||
Header *types.Header // Header defining the block context to execute in
|
Header *types.Header // Header defining the block context to execute in
|
||||||
State *state.StateDB // Pre-state on top of which to estimate the gas
|
State *state.StateDB // Pre-state on top of which to estimate the gas
|
||||||
|
|
||||||
|
ErrorRatio float64 // Allowed overestimation ratio for faster estimation termination
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estimate returns the lowest possible gas limit that allows the transaction to
|
// 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
|
|||||||
if transfer == nil {
|
if transfer == nil {
|
||||||
transfer = new(big.Int)
|
transfer = new(big.Int)
|
||||||
}
|
}
|
||||||
log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance,
|
log.Debug("Gas estimation capped by limited funds", "original", hi, "balance", balance,
|
||||||
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance)
|
"sent", transfer, "maxFeePerGas", feeCap, "fundable", allowance)
|
||||||
hi = allowance.Uint64()
|
hi = allowance.Uint64()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Recap the highest gas allowance with specified gascap.
|
// Recap the highest gas allowance with specified gascap.
|
||||||
if gasCap != 0 && hi > gasCap {
|
if gasCap != 0 && hi > gasCap {
|
||||||
log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
|
log.Debug("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
|
||||||
hi = gasCap
|
hi = gasCap
|
||||||
}
|
}
|
||||||
|
// If the transaction is a plain value transfer, short circuit estimation and
|
||||||
|
// directly try 21000. Returning 21000 without any execution is dangerous as
|
||||||
|
// some tx field combos might bump the price up even for plain transfers (e.g.
|
||||||
|
// unused access list items). Ever so slightly wasteful, but safer overall.
|
||||||
|
if len(call.Data) == 0 {
|
||||||
|
if call.To != nil && opts.State.GetCodeSize(*call.To) == 0 {
|
||||||
|
failed, _, err := execute(ctx, call, opts, params.TxGas)
|
||||||
|
if !failed && err == nil {
|
||||||
|
return params.TxGas, nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// We first execute the transaction at the highest allowable gas limit, since if this fails we
|
// We first execute the transaction at the highest allowable gas limit, since if this fails we
|
||||||
// can return error immediately.
|
// can return error immediately.
|
||||||
failed, result, err := execute(ctx, call, opts, hi)
|
failed, result, err := execute(ctx, call, opts, hi)
|
||||||
@ -115,8 +129,35 @@ func Estimate(ctx context.Context, call *core.Message, opts *Options, gasCap uin
|
|||||||
// limit for these cases anyway.
|
// limit for these cases anyway.
|
||||||
lo = result.UsedGas - 1
|
lo = result.UsedGas - 1
|
||||||
|
|
||||||
|
// There's a fairly high chance for the transaction to execute successfully
|
||||||
|
// with gasLimit set to the first execution's usedGas + gasRefund. Explicitly
|
||||||
|
// check that gas amount and use as a limit for the binary search.
|
||||||
|
optimisticGasLimit := (result.UsedGas + result.RefundedGas + params.CallStipend) * 64 / 63
|
||||||
|
if optimisticGasLimit < hi {
|
||||||
|
failed, _, err = execute(ctx, call, opts, optimisticGasLimit)
|
||||||
|
if err != nil {
|
||||||
|
// This should not happen under normal conditions since if we make it this far the
|
||||||
|
// transaction had run without error at least once before.
|
||||||
|
log.Error("Execution error in estimate gas", "err", err)
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
lo = optimisticGasLimit
|
||||||
|
} else {
|
||||||
|
hi = optimisticGasLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
// Binary search for the smallest gas limit that allows the tx to execute successfully.
|
// Binary search for the smallest gas limit that allows the tx to execute successfully.
|
||||||
for lo+1 < hi {
|
for lo+1 < hi {
|
||||||
|
if opts.ErrorRatio > 0 {
|
||||||
|
// It is a bit pointless to return a perfect estimation, as changing
|
||||||
|
// network conditions require the caller to bump it up anyway. Since
|
||||||
|
// wallets tend to use 20-25% bump, allowing a small approximation
|
||||||
|
// error is fine (as long as it's upwards).
|
||||||
|
if float64(hi-lo)/float64(hi) < opts.ErrorRatio {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
mid := (hi + lo) / 2
|
mid := (hi + lo) / 2
|
||||||
if mid > lo*2 {
|
if mid > lo*2 {
|
||||||
// Most txs don't need much higher gas limit than their gas used, and most txs don't
|
// Most txs don't need much higher gas limit than their gas used, and most txs don't
|
||||||
|
@ -139,7 +139,7 @@ func TestGraphQLBlockSerialization(t *testing.T) {
|
|||||||
// should return `estimateGas` as decimal
|
// should return `estimateGas` as decimal
|
||||||
{
|
{
|
||||||
body: `{"query": "{block{ estimateGas(data:{}) }}"}`,
|
body: `{"query": "{block{ estimateGas(data:{}) }}"}`,
|
||||||
want: `{"data":{"block":{"estimateGas":"0xcf08"}}}`,
|
want: `{"data":{"block":{"estimateGas":"0xd221"}}}`,
|
||||||
code: 200,
|
code: 200,
|
||||||
},
|
},
|
||||||
// should return `status` as decimal
|
// should return `status` as decimal
|
||||||
|
@ -51,6 +51,10 @@ import (
|
|||||||
"github.com/tyler-smith/go-bip39"
|
"github.com/tyler-smith/go-bip39"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas is
|
||||||
|
// allowed to produce in order to speed up calculations.
|
||||||
|
const estimateGasErrorRatio = 0.015
|
||||||
|
|
||||||
// EthereumAPI provides an API to access Ethereum related information.
|
// EthereumAPI provides an API to access Ethereum related information.
|
||||||
type EthereumAPI struct {
|
type EthereumAPI struct {
|
||||||
b Backend
|
b Backend
|
||||||
@ -1193,6 +1197,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
|
|||||||
Chain: NewChainContext(ctx, b),
|
Chain: NewChainContext(ctx, b),
|
||||||
Header: header,
|
Header: header,
|
||||||
State: state,
|
State: state,
|
||||||
|
ErrorRatio: estimateGasErrorRatio,
|
||||||
}
|
}
|
||||||
// Run the gas estimation andwrap any revertals into a custom return
|
// Run the gas estimation andwrap any revertals into a custom return
|
||||||
call, err := args.ToMessage(gasCap, header.BaseFee)
|
call, err := args.ToMessage(gasCap, header.BaseFee)
|
||||||
|
@ -735,7 +735,7 @@ func TestEstimateGas(t *testing.T) {
|
|||||||
t.Errorf("test %d: want no error, have %v", i, err)
|
t.Errorf("test %d: want no error, have %v", i, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if uint64(result) != tc.want {
|
if float64(result) > float64(tc.want)*(1+estimateGasErrorRatio) {
|
||||||
t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, uint64(result), tc.want)
|
t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, uint64(result), tc.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user