erigon-pulse/cmd/rpcdaemon/commands/eth_call.go
hexoscott 12ed9d22cf
eth_estimateGas performance tweaks (#6001)
Will gain more perf improvements when the change for kvcache goes in
from erigon-lib.

Hopefully can be validated with hive? My main concern is the re-use of
the state reader, I couldn't find any manipulation of it so it looks
safe to re-use.
2022-11-08 21:48:11 +00:00

480 lines
14 KiB
Go

package commands
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/holiman/uint256"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
txpool_proto "github.com/ledgerwatch/erigon-lib/gointerfaces/txpool"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/log/v3"
"google.golang.org/grpc"
ethapi2 "github.com/ledgerwatch/erigon/turbo/adapter/ethapi"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/common/hexutil"
"github.com/ledgerwatch/erigon/core"
"github.com/ledgerwatch/erigon/core/state"
"github.com/ledgerwatch/erigon/core/types"
"github.com/ledgerwatch/erigon/core/vm"
"github.com/ledgerwatch/erigon/crypto"
"github.com/ledgerwatch/erigon/eth/tracers/logger"
"github.com/ledgerwatch/erigon/params"
"github.com/ledgerwatch/erigon/rpc"
"github.com/ledgerwatch/erigon/turbo/rpchelper"
"github.com/ledgerwatch/erigon/turbo/transactions"
)
var (
latestNumOrHash = rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
)
// Call implements eth_call. Executes a new message call immediately without creating a transaction on the block chain.
func (api *APIImpl) Call(ctx context.Context, args ethapi2.CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *ethapi2.StateOverrides) (hexutil.Bytes, error) {
tx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
chainConfig, err := api.chainConfig(tx)
if err != nil {
return nil, err
}
if args.Gas == nil || uint64(*args.Gas) == 0 {
args.Gas = (*hexutil.Uint64)(&api.GasCap)
}
blockNumber, hash, _, err := rpchelper.GetCanonicalBlockNumber(blockNrOrHash, tx, api.filters) // DoCall cannot be executed on non-canonical blocks
if err != nil {
return nil, err
}
block, err := api.BaseAPI.blockWithSenders(tx, hash, blockNumber)
if err != nil {
return nil, err
}
if block == nil {
return nil, nil
}
stateReader, err := rpchelper.CreateStateReader(ctx, tx, blockNrOrHash, 0, api.filters, api.stateCache, api.historyV3(tx), api._agg)
if err != nil {
return nil, err
}
result, err := transactions.DoCall(ctx, args, tx, blockNrOrHash, block, overrides, api.GasCap, chainConfig, stateReader, api._blockReader, api.evmCallTimeout)
if err != nil {
return nil, err
}
// If the result contains a revert reason, try to unpack and return it.
if len(result.Revert()) > 0 {
return nil, ethapi2.NewRevertError(result)
}
return result.Return(), result.Err
}
// headerByNumberOrHash - intent to read recent headers only, tries from the lru cache before reading from the db
func headerByNumberOrHash(ctx context.Context, tx kv.Tx, blockNrOrHash rpc.BlockNumberOrHash, api *APIImpl) (*types.Header, error) {
_, bNrOrHashHash, _, err := rpchelper.GetCanonicalBlockNumber(blockNrOrHash, tx, api.filters)
if err != nil {
return nil, err
}
block := api.tryBlockFromLru(bNrOrHashHash)
if block != nil {
return block.Header(), nil
}
blockNum, _, _, err := rpchelper.GetBlockNumber(blockNrOrHash, tx, api.filters)
if err != nil {
return nil, err
}
header, err := api._blockReader.HeaderByNumber(ctx, tx, blockNum)
if err != nil {
return nil, err
}
// header can be nil
return header, nil
}
// EstimateGas implements eth_estimateGas. Returns an estimate of how much gas is necessary to allow the transaction to complete. The transaction will not be added to the blockchain.
func (api *APIImpl) EstimateGas(ctx context.Context, argsOrNil *ethapi2.CallArgs, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Uint64, error) {
var args ethapi2.CallArgs
// if we actually get CallArgs here, we use them
if argsOrNil != nil {
args = *argsOrNil
}
dbtx, err := api.db.BeginRo(ctx)
if err != nil {
return 0, err
}
defer dbtx.Rollback()
// Binary search the gas requirement, as it may be higher than the amount used
var (
lo = params.TxGas - 1
hi uint64
cap uint64
)
// Use zero address if sender unspecified.
if args.From == nil {
args.From = new(common.Address)
}
bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
if blockNrOrHash != nil {
bNrOrHash = *blockNrOrHash
}
// Determine the highest gas limit can be used during the estimation.
if args.Gas != nil && uint64(*args.Gas) >= params.TxGas {
hi = uint64(*args.Gas)
} else {
// Retrieve the block to act as the gas ceiling
h, err := headerByNumberOrHash(ctx, dbtx, bNrOrHash, api)
if err != nil {
return 0, err
}
if h == nil {
// if a block number was supplied and there is no header return 0
if blockNrOrHash != nil {
return 0, nil
}
// block number not supplied, so we haven't found a pending block, read the latest block instead
h, err = headerByNumberOrHash(ctx, dbtx, latestNumOrHash, api)
if err != nil {
return 0, err
}
if h == nil {
return 0, nil
}
}
hi = h.GasLimit
}
var feeCap *big.Int
if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) {
return 0, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
} else if args.GasPrice != nil {
feeCap = args.GasPrice.ToInt()
} else if args.MaxFeePerGas != nil {
feeCap = args.MaxFeePerGas.ToInt()
} else {
feeCap = common.Big0
}
// Recap the highest gas limit with account's available balance.
if feeCap.Sign() != 0 {
cacheView, err := api.stateCache.View(ctx, dbtx)
if err != nil {
return 0, err
}
stateReader := state.NewCachedReader2(cacheView, dbtx)
state := state.New(stateReader)
if state == nil {
return 0, fmt.Errorf("can't get the current state")
}
balance := state.GetBalance(*args.From) // from can't be nil
available := balance.ToBig()
if args.Value != nil {
if args.Value.ToInt().Cmp(available) >= 0 {
return 0, errors.New("insufficient funds for transfer")
}
available.Sub(available, args.Value.ToInt())
}
allowance := new(big.Int).Div(available, feeCap)
// If the allowance is larger than maximum uint64, skip checking
if allowance.IsUint64() && hi > allowance.Uint64() {
transfer := args.Value
if transfer == nil {
transfer = new(hexutil.Big)
}
log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance,
"sent", transfer.ToInt(), "maxFeePerGas", feeCap, "fundable", allowance)
hi = allowance.Uint64()
}
}
// Recap the highest gas allowance with specified gascap.
if hi > api.GasCap {
log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", api.GasCap)
hi = api.GasCap
}
cap = hi
chainConfig, err := api.chainConfig(dbtx)
if err != nil {
return 0, err
}
latestCanBlockNumber, latestCanHash, isLatest, err := rpchelper.GetCanonicalBlockNumber(latestNumOrHash, dbtx, api.filters) // DoCall cannot be executed on non-canonical blocks
if err != nil {
return 0, err
}
// try and get the block from the lru cache first then try DB before failing
block := api.tryBlockFromLru(latestCanHash)
if block == nil {
block, err = api.BaseAPI.blockWithSenders(dbtx, latestCanHash, latestCanBlockNumber)
if err != nil {
return 0, err
}
}
if block == nil {
return 0, fmt.Errorf("could not find latest block in cache or db")
}
stateReader, err := rpchelper.CreateStateReaderFromBlockNumber(ctx, dbtx, latestCanBlockNumber, isLatest, 0, api.stateCache, api.historyV3(dbtx), api._agg)
if err != nil {
return 0, err
}
// Create a helper to check if a gas allowance results in an executable transaction
executable := func(gas uint64) (bool, *core.ExecutionResult, error) {
args.Gas = (*hexutil.Uint64)(&gas)
result, err := transactions.DoCall(ctx, args, dbtx, latestNumOrHash, block, nil, api.GasCap, chainConfig, stateReader, api._blockReader, api.evmCallTimeout)
if err != nil {
if errors.Is(err, core.ErrIntrinsicGas) {
// Special case, raise gas limit
return true, nil, nil
}
// Bail out
return true, nil, err
}
return result.Failed(), result, nil
}
// Execute the binary search and hone in on an executable gas limit
for lo+1 < hi {
mid := (hi + 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
// assigened. Return the error directly, don't struggle any more.
if err != nil {
return 0, err
}
if failed {
lo = mid
} else {
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 && !errors.Is(result.Err, vm.ErrOutOfGas) {
if len(result.Revert()) > 0 {
return 0, ethapi2.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), nil
}
// GetProof not implemented
func (api *APIImpl) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNr rpc.BlockNumber) (*interface{}, error) {
var stub interface{}
return &stub, fmt.Errorf(NotImplemented, "eth_getProof")
}
func (api *APIImpl) tryBlockFromLru(hash common.Hash) *types.Block {
var block *types.Block
if api.blocksLRU != nil {
if it, ok := api.blocksLRU.Get(hash); ok && it != nil {
block = it.(*types.Block)
}
}
return block
}
// accessListResult returns an optional accesslist
// Its the result of the `eth_createAccessList` RPC call.
// It contains an error if the transaction itself failed.
type accessListResult struct {
Accesslist *types.AccessList `json:"accessList"`
Error string `json:"error,omitempty"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
}
// CreateAccessList implements eth_createAccessList. It creates an access list for the given transaction.
// If the accesslist creation fails an error is returned.
// If the transaction itself fails, an vmErr is returned.
func (api *APIImpl) CreateAccessList(ctx context.Context, args ethapi2.CallArgs, blockNrOrHash *rpc.BlockNumberOrHash, optimizeGas *bool) (*accessListResult, error) {
bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
if blockNrOrHash != nil {
bNrOrHash = *blockNrOrHash
}
tx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()
chainConfig, err := api.chainConfig(tx)
if err != nil {
return nil, err
}
blockNumber, hash, latest, err := rpchelper.GetCanonicalBlockNumber(bNrOrHash, tx, api.filters) // DoCall cannot be executed on non-canonical blocks
if err != nil {
return nil, err
}
block, err := api.BaseAPI.blockWithSenders(tx, hash, blockNumber)
if err != nil {
return nil, err
}
if block == nil {
return nil, nil
}
var stateReader state.StateReader
if latest {
cacheView, err := api.stateCache.View(ctx, tx)
if err != nil {
return nil, err
}
stateReader = state.NewCachedReader2(cacheView, tx)
} else {
stateReader, err = rpchelper.CreateHistoryStateReader(tx, blockNumber+1, 0, api._agg, api.historyV3(tx))
if err != nil {
return nil, err
}
}
header := block.Header()
// If the gas amount is not set, extract this as it will depend on access
// lists and we'll need to reestimate every time
nogas := args.Gas == nil
var to common.Address
if args.To != nil {
to = *args.To
} else {
// Require nonce to calculate address of created contract
if args.Nonce == nil {
var nonce uint64
reply, err := api.txPool.Nonce(ctx, &txpool_proto.NonceRequest{
Address: gointerfaces.ConvertAddressToH160(*args.From),
}, &grpc.EmptyCallOption{})
if err != nil {
return nil, err
}
if reply.Found {
nonce = reply.Nonce + 1
}
args.Nonce = (*hexutil.Uint64)(&nonce)
}
to = crypto.CreateAddress(*args.From, uint64(*args.Nonce))
}
// Retrieve the precompiles since they don't need to be added to the access list
precompiles := vm.ActivePrecompiles(chainConfig.Rules(blockNumber))
// Create an initial tracer
prevTracer := logger.NewAccessListTracer(nil, *args.From, to, precompiles)
if args.AccessList != nil {
prevTracer = logger.NewAccessListTracer(*args.AccessList, *args.From, to, precompiles)
}
for {
state := state.New(stateReader)
// Retrieve the current access list to expand
accessList := prevTracer.AccessList()
log.Trace("Creating access list", "input", accessList)
// If no gas amount was specified, each unique access list needs it's own
// gas calculation. This is quite expensive, but we need to be accurate
// and it's convered by the sender only anyway.
if nogas {
args.Gas = nil
}
// Set the accesslist to the last al
args.AccessList = &accessList
var msg types.Message
var baseFee *uint256.Int = nil
// check if EIP-1559
if header.BaseFee != nil {
baseFee, _ = uint256.FromBig(header.BaseFee)
}
msg, err = args.ToMessage(api.GasCap, baseFee)
if err != nil {
return nil, err
}
// Apply the transaction with the access list tracer
tracer := logger.NewAccessListTracer(accessList, *args.From, to, precompiles)
config := vm.Config{Tracer: tracer, Debug: true, NoBaseFee: true}
blockCtx, txCtx := transactions.GetEvmContext(msg, header, bNrOrHash.RequireCanonical, tx, api._blockReader)
evm := vm.NewEVM(blockCtx, txCtx, state, chainConfig, config)
gp := new(core.GasPool).AddGas(msg.Gas())
res, err := core.ApplyMessage(evm, msg, gp, true /* refunds */, false /* gasBailout */)
if err != nil {
return nil, err
}
if tracer.Equal(prevTracer) {
var errString string
if res.Err != nil {
errString = res.Err.Error()
}
accessList := &accessListResult{Accesslist: &accessList, Error: errString, GasUsed: hexutil.Uint64(res.UsedGas)}
if optimizeGas != nil && *optimizeGas {
optimizeToInAccessList(accessList, to)
}
return accessList, nil
}
prevTracer = tracer
}
}
// to address is warm already, so we can save by adding it to the access list
// only if we are adding a lot of its storage slots as well
func optimizeToInAccessList(accessList *accessListResult, to common.Address) {
indexToRemove := -1
for i := 0; i < len(*accessList.Accesslist); i++ {
entry := (*accessList.Accesslist)[i]
if entry.Address != to {
continue
}
// https://eips.ethereum.org/EIPS/eip-2930#charging-less-for-accesses-in-the-access-list
accessListSavingPerSlot := params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929 - params.TxAccessListStorageKeyGas
numSlots := uint64(len(entry.StorageKeys))
if numSlots*accessListSavingPerSlot <= params.TxAccessListAddressGas {
indexToRemove = i
}
}
if indexToRemove >= 0 {
*accessList.Accesslist = removeIndex(*accessList.Accesslist, indexToRemove)
}
}
func removeIndex(s types.AccessList, index int) types.AccessList {
return append(s[:index], s[index+1:]...)
}