Skip to content
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

[R4R]-{estimateGas}feature: EstimateGas performance optimization #38

Merged
merged 5 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ profile.cov
/dashboard/assets/package-lock.json

**/yarn-error.log
node_modules

Choose a reason for hiding this comment

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

please delete un-releated file

Copy link
Author

Choose a reason for hiding this comment

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

resolved

7 changes: 6 additions & 1 deletion common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func (h UnprefixedHash) MarshalText() ([]byte, error) {
return []byte(hex.EncodeToString(h[:])), nil
}

/////////// Address
// ///////// Address

// Address represents the 20 byte address of an Ethereum account.
type Address [AddressLength]byte
Expand Down Expand Up @@ -309,6 +309,11 @@ func (a *Address) SetBytes(b []byte) {
copy(a[AddressLength-len(b):], b)
}

// Cmp compares two addresses.
func (a Address) Cmp(other Address) int {
return bytes.Compare(a[:], other[:])
}

// MarshalText returns the hex representation of a.
func (a Address) MarshalText() ([]byte, error) {
return hexutil.Bytes(a[:]).MarshalText()
Expand Down
38 changes: 22 additions & 16 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ var (
// ExecutionResult includes all output after executing given evm
// message no matter the execution itself is successful or not.
type ExecutionResult struct {
UsedGas uint64 // Total used gas but include the refunded gas
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)
UsedGas uint64 // Total used gas but include the refunded gas
RefundedGas uint64 // Total gas refunded after execution
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)
}

// Unwrap returns the internal evm error which allows us for further
Expand Down Expand Up @@ -585,22 +586,24 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) {
// Note for deposit tx there is no ETH refunded for unused gas, but that's taken care of by the fact that gasPrice
// is always 0 for deposit tx. So calling refundGas will ensure the gasUsed accounting is correct without actually
// changing the sender's balance
var gasRefund uint64
if !st.msg.IsDepositTx && !st.msg.IsSystemTx {
if !rules.IsLondon {
// Before EIP-3529: refunds were capped to gasUsed / 2
st.refundGas(params.RefundQuotient, tokenRatio)
gasRefund = st.refundGas(params.RefundQuotient, tokenRatio)
} else {
// After EIP-3529: refunds are capped to gasUsed / 5
st.refundGas(params.RefundQuotientEIP3529, tokenRatio)
gasRefund = st.refundGas(params.RefundQuotientEIP3529, tokenRatio)
}
}

if st.msg.IsDepositTx && rules.IsOptimismRegolith {
// Skip coinbase payments for deposit tx in Regolith
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
UsedGas: st.gasUsed(),
RefundedGas: gasRefund,
Err: vmerr,
ReturnData: ret,
}, nil
}
effectiveTip := msg.GasPrice
Expand All @@ -623,23 +626,24 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) {
if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock {
st.state.AddBalance(params.OptimismBaseFeeRecipient, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee))
// Can not collect l1 fee here again, all l1 fee has been collected by CoinBase & OptimismBaseFeeRecipient
//if cost := st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx); cost != nil {
// if cost := st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.evm.Context.Time, st.msg.RollupDataGas, st.msg.IsDepositTx); cost != nil {
// st.state.AddBalance(params.OptimismL1FeeRecipient, cost)
//}
// }
}

return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
UsedGas: st.gasUsed(),
RefundedGas: gasRefund,
Err: vmerr,
ReturnData: ret,
}, nil
}

func (st *StateTransition) refundGas(refundQuotient, tokenRatio uint64) {
func (st *StateTransition) refundGas(refundQuotient, tokenRatio uint64) uint64 {
if st.msg.RunMode == GasEstimationWithSkipCheckBalanceMode || st.msg.RunMode == EthcallMode {
st.gasRemaining = st.gasRemaining * tokenRatio
st.gp.AddGas(st.gasRemaining)
return
return 0
}
// Apply refund counter, capped to a refund quotient
refund := st.gasUsed() / refundQuotient
Expand All @@ -665,6 +669,8 @@ func (st *StateTransition) refundGas(refundQuotient, tokenRatio uint64) {
// Also return remaining gas to the block gas counter so it is
// available for the next transaction.
st.gp.AddGas(st.gasRemaining)

return refund
}

// gasUsed returns the amount of gas used up by the state transition.
Expand Down Expand Up @@ -760,7 +766,7 @@ func (st *StateTransition) generateBVMETHMintEvent(mintAddress common.Address, m
topics := make([]common.Hash, 2)
topics[0] = methodHash
topics[1] = mintAddress.Hash()
//data means the mint amount in MINT EVENT.
// data means the mint amount in MINT EVENT.
d := common.HexToHash(common.Bytes2Hex(mintValue.Bytes())).Bytes()
st.evm.StateDB.AddLog(&types.Log{
Address: BVM_ETH_ADDR,
Expand Down
167 changes: 127 additions & 40 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/consensus"

Choose a reason for hiding this comment

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

go import format

Copy link
Author

Choose a reason for hiding this comment

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

resolved

"math/big"
"strings"
"time"
Expand Down Expand Up @@ -52,8 +53,11 @@ import (
"github.com/ethereum/go-ethereum/rpc"
)

var (
gasBuffer = uint64(120)
// estimateGasErrorRatio is the amount of overestimation eth_estimateGas is
// allowed to produce in order to speed up calculations.
const (
estimateGasErrorRatio = 0.015
gasBuffer = uint64(120 / 100)
boz14676 marked this conversation as resolved.
Show resolved Hide resolved
)

// EthereumAPI provides an API to access Ethereum related information.
Expand Down Expand Up @@ -1171,27 +1175,25 @@ func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrO
func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap uint64) (hexutil.Uint64, error) {
// Binary search the gas requirement, as it may be higher than the amount used
var (
lo uint64 = params.TxGas - 1
hi uint64
cap uint64
lo = params.TxGas - 1
hi uint64
)
// Use zero address if sender unspecified.
if args.From == nil {
args.From = new(common.Address)
}

data := hexutil.Bytes(args.data())
args.Data = &data

state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return 0, err
}
// Determine the highest gas limit can be used during the estimation.
hi = header.GasLimit
if args.Gas != nil && uint64(*args.Gas) >= params.TxGas {
hi = uint64(*args.Gas)
} else {
// Retrieve the block to act as the gas ceiling
block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return 0, err
}
if block == nil {
return 0, errors.New("block not found")
}
hi = block.GasLimit()
}

// Normalize the gasPrice used for estimateGas
Expand Down Expand Up @@ -1222,11 +1224,6 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr

// Recap the highest gas limit with account's available balance.
if feeCap.BitLen() != 0 {
state, _, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return 0, err
}

balance := state.GetBalance(*args.From) // from can't be nil
metaTxParams, err := types.DecodeMetaTxParams(args.data())
if err != nil {
Expand Down Expand Up @@ -1269,7 +1266,13 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
hi = gasCap
}
cap = hi

revertErr := func(result *core.ExecutionResult) error {
if len(result.Revert()) > 0 {
return newRevertError(result)
}
return result.Err
}

// Create a helper to check if a gas allowance results in an executable transaction
executable := func(gas uint64) (bool, *core.ExecutionResult, error) {
Expand All @@ -1284,15 +1287,84 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
}
return result.Failed(), result, nil
}

// 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 args.Data == nil {
if args.To != nil && state.GetCodeSize(*args.To) == 0 {
failed, _, err := executable(params.TxGas)
if !failed && err == nil {
return hexutil.Uint64(params.TxGas), nil
}
}
}

// We first execute the transaction at the highest allowable gas limit, since if this fails we
// can return error immediately.
failed, result, err := executable(hi)
if err != nil {
return 0, err
}
if failed {
if result != nil && !errors.Is(result.Err, vm.ErrOutOfGas) {
return 0, revertErr(result)
}
return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi)
}

// For almost any transaction, the gas consumed by the unconstrained execution
// above lower-bounds the gas limit required for it to succeed. One exception
// is those that explicitly check gas remaining in order to execute within a
// given limit, but we probably don't want to return the lowest possible gas
// limit for these cases anyway.
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 = executable(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, err
}
if failed {
lo = optimisticGasLimit
} else {
hi = optimisticGasLimit
}
}

// Execute the binary search and hone in on an executable gas limit
for lo+1 < hi {
// 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) < estimateGasErrorRatio {
break
}

mid := (hi + lo) / 2
failed, _, err := executable(mid)
if mid > lo*2 {
// Most txs don't need much higher gas limit than their gas used, and most txs don't
// require near the full block limit of gas, so the selection of where to bisect the
// range here is skewed to favor the low side.
mid = lo * 2
}

failed, _, err = executable(mid)

// If the error is not nil(consensus error), it means the provided message
// call or transaction will never be accepted no matter how much gas it is
// assigned. Return the error directly, don't struggle any more.
if err != nil {
log.Error("Execution error in estimate gas", "err", err)
return 0, err
}
if failed {
Expand All @@ -1301,24 +1373,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
hi = mid
}
}
// Reject the transaction as invalid if it still fails at the highest allowance
if hi == cap {
failed, result, err := executable(hi)
if err != nil {
return 0, err
}
if failed {
if result != nil && result.Err != vm.ErrOutOfGas {
if len(result.Revert()) > 0 {
return 0, newRevertError(result)
}
return 0, result.Err
}
// Otherwise, the specified gas cap is too low
return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap)
}
}
return hexutil.Uint64(hi * gasBuffer / 100), nil
return hexutil.Uint64(hi * gasBuffer), nil
}

func calculateGasWithAllowance(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, gasPriceForEstimate *big.Int, gasCap uint64) (uint64, error) {
Expand Down Expand Up @@ -1388,6 +1443,38 @@ func (s *BlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs, b
return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap())
}

// ChainContextBackend provides methods required to implement ChainContext.
type ChainContextBackend interface {
Engine() consensus.Engine
HeaderByNumber(context.Context, rpc.BlockNumber) (*types.Header, error)
}

// ChainContext is an implementation of core.ChainContext. It's main use-case
// is instantiating a vm.BlockContext without having access to the BlockChain object.
type ChainContext struct {
b ChainContextBackend
ctx context.Context
}

func (context *ChainContext) Engine() consensus.Engine {
return context.b.Engine()
}

func (context *ChainContext) GetHeader(hash common.Hash, number uint64) *types.Header {
// This method is called to get the hash for a block number when executing the BLOCKHASH
// opcode. Hence no need to search for non-canonical blocks.
header, err := context.b.HeaderByNumber(context.ctx, rpc.BlockNumber(number))
if err != nil || header.Hash() != hash {
return nil
}
return header
}

// NewChainContext creates a new ChainContext object.
func NewChainContext(ctx context.Context, backend ChainContextBackend) *ChainContext {
return &ChainContext{ctx: ctx, b: backend}
}

// RPCMarshalHeader converts the given header to the RPC output .
func RPCMarshalHeader(head *types.Header) map[string]interface{} {
result := map[string]interface{}{
Expand Down
Loading
Loading