Update eth_call code. #105

Merged
arijitAD merged 3 commits from update-eth-call into master 2021-09-30 14:44:24 +00:00
5 changed files with 210 additions and 131 deletions

View File

@ -27,10 +27,12 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/eth/filters"
@ -819,50 +821,59 @@ func (pea *PublicEthAPI) localGetProof(ctx context.Context, address common.Addre
}, state.Error() }, state.Error()
} }
// Call executes the given transaction on the state for the given block number. // revertError is an API error that encompassas an EVM revertal with JSON error
// // code and a binary data blob.
// Additionally, the caller can specify a batch of contract for fields overriding. type revertError struct {
// error
// Note, this function doesn't make and changes in the state/blockchain and is reason string // revert reason hex encoded
// useful to execute and retrieve values.
func (pea *PublicEthAPI) Call(ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *map[common.Address]account) (hexutil.Bytes, error) {
var accounts map[common.Address]account
if overrides != nil {
accounts = *overrides
}
res, _, failed, err := DoCall(ctx, pea.B, args, blockNrOrHash, accounts, 5*time.Second, pea.B.Config.RPCGasCap)
if (failed || err != nil) && pea.rpc != nil {
var hex hexutil.Bytes
if err := pea.rpc.CallContext(ctx, &hex, "eth_call", args, blockNrOrHash, overrides); hex != nil && err == nil {
go pea.writeStateDiffAtOrFor(blockNrOrHash)
return hex, nil
}
}
if failed && err == nil {
return nil, errors.New("eth_call failed without error")
}
return (hexutil.Bytes)(res), err
} }
func DoCall(ctx context.Context, b *Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, timeout time.Duration, globalGasCap *big.Int) ([]byte, uint64, bool, error) { // ErrorCode returns the JSON error code for a revertal.
defer func(start time.Time) { // See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal
logrus.Debugf("Executing EVM call finished %s runtime %s", time.Now().String(), time.Since(start).String()) func (e *revertError) ErrorCode() int {
}(time.Now()) return 3
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) }
if state == nil || err != nil {
return nil, 0, false, err // ErrorData returns the hex encoded revert reason.
func (e *revertError) ErrorData() interface{} {
return e.reason
}
func newRevertError(result *core.ExecutionResult) *revertError {
reason, errUnpack := abi.UnpackRevert(result.Revert())
err := errors.New("execution reverted")
if errUnpack == nil {
err = fmt.Errorf("execution reverted: %v", reason)
} }
// Set sender address or use a default if none specified return &revertError{
var addr common.Address error: err,
if args.From == nil { reason: hexutil.Encode(result.Revert()),
if b.Config.DefaultSender != nil {
addr = *b.Config.DefaultSender
}
} else {
addr = *args.From
} }
// Override the fields of specified contracts before execution. }
for addr, account := range overrides {
// OverrideAccount indicates the overriding fields of account during the execution
// of a message call.
// Note, state and stateDiff can't be specified at the same time. If state is
// set, message execution will only use the data in the given state. Otherwise
// if statDiff is set, all diff will be applied first and then execute the call
// message.
type OverrideAccount struct {
Nonce *hexutil.Uint64 `json:"nonce"`
Code *hexutil.Bytes `json:"code"`
Balance **hexutil.Big `json:"balance"`
State *map[common.Hash]common.Hash `json:"state"`
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
}
// StateOverride is the collection of overridden accounts.
type StateOverride map[common.Address]OverrideAccount
// Apply overrides the fields of specified accounts into the given state.
func (diff *StateOverride) Apply(state *state.StateDB) error {
if diff == nil {
return nil
}
for addr, account := range *diff {
// Override account nonce. // Override account nonce.
if account.Nonce != nil { if account.Nonce != nil {
state.SetNonce(addr, uint64(*account.Nonce)) state.SetNonce(addr, uint64(*account.Nonce))
@ -876,7 +887,7 @@ func DoCall(ctx context.Context, b *Backend, args CallArgs, blockNrOrHash rpc.Bl
state.SetBalance(addr, (*big.Int)(*account.Balance)) state.SetBalance(addr, (*big.Int)(*account.Balance))
} }
if account.State != nil && account.StateDiff != nil { if account.State != nil && account.StateDiff != nil {
return nil, 0, false, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex()) return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
} }
// Replace entire state if caller requires. // Replace entire state if caller requires.
if account.State != nil { if account.State != nil {
@ -889,74 +900,51 @@ func DoCall(ctx context.Context, b *Backend, args CallArgs, blockNrOrHash rpc.Bl
} }
} }
} }
// Set default gas & gas price if none were set return nil
gas := uint64(math.MaxUint64 / 2) }
if args.Gas != nil {
gas = uint64(*args.Gas)
}
if globalGasCap != nil && globalGasCap.Uint64() < gas { // Call executes the given transaction on the state for the given block number.
logrus.Warnf("Caller gas above allowance, capping; requested: %d, cap: %d", gas, globalGasCap) //
gas = globalGasCap.Uint64() // Additionally, the caller can specify a batch of contract for fields overriding.
} //
// Note, this function doesn't make and changes in the state/blockchain and is
// useful to execute and retrieve values.
func (pea *PublicEthAPI) Call(ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) {
Review

For now this should be left as err :=, if we hit an error on the proxy call we'd prefer to return the original error that caused us to call out to the proxy rather than the proxy error.

What we really should do, but we can make this a separate issue, is track all the errors in an []error and then when we return from this method we concatenate all those error messages into a single error.

For now this should be left as `err :=`, if we hit an error on the proxy call we'd prefer to return the original error that caused us to call out to the proxy rather than the proxy error. What we really should do, but we can make this a separate issue, is track all the errors in an `[]error` and then when we return from this method we concatenate all those error messages into a single error.
arijitAD commented 2021-09-30 13:45:37 +00:00 (Migrated from github.com)
Review
Done Created an issue https://github.com/vulcanize/ipld-eth-server/issues/109
Review

Thanks! Sorry, I should have mentioned I already created an issue so I closed yours.

Thanks! Sorry, I should have mentioned I already created an issue so I closed yours.
result, err := DoCall(ctx, pea.B, args, blockNrOrHash, overrides, 5*time.Second, pea.B.Config.RPCGasCap.Uint64())
var ( // If the result contains a revert reason, try to unpack and return it.
gasPrice *big.Int if err == nil {
gasFeeCap *big.Int if len(result.Revert()) > 0 {
gasTipCap *big.Int err = newRevertError(result)
) } else if result.Err != nil {
err = result.Err
if header.BaseFee == nil {
// If there's no basefee, then it must be a non-1559 execution
gasPrice = new(big.Int)
if args.GasPrice != nil {
gasPrice = args.GasPrice.ToInt()
}
gasFeeCap, gasTipCap = gasPrice, gasPrice
} else {
// A basefee is provided, necessitating 1559-type execution
if args.GasPrice != nil {
// User specified the legacy gas field, convert to 1559 gas typing
gasPrice = args.GasPrice.ToInt()
gasFeeCap, gasTipCap = gasPrice, gasPrice
} else {
// User specified 1559 gas feilds (or none), use those
gasFeeCap = new(big.Int)
if args.MaxFeePerGas != nil {
gasFeeCap = args.MaxFeePerGas.ToInt()
}
gasTipCap = new(big.Int)
if args.MaxPriorityFeePerGas != nil {
gasTipCap = args.MaxPriorityFeePerGas.ToInt()
}
// Backfill the legacy gasPrice for EVM execution, unless we're all zeroes
gasPrice = new(big.Int)
if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 {
gasPrice = math.BigMin(new(big.Int).Add(gasTipCap, header.BaseFee), gasFeeCap)
}
} }
} }
value := new(big.Int) if err != nil && pea.rpc != nil {
if args.Value != nil { var hex hexutil.Bytes
value = args.Value.ToInt() if err := pea.rpc.CallContext(ctx, &hex, "eth_call", args, blockNrOrHash, overrides); hex != nil && err == nil {
go pea.writeStateDiffAtOrFor(blockNrOrHash)
return hex, nil
}
}
return result.Return(), err
}
func DoCall(ctx context.Context, b *Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
defer func(start time.Time) {
logrus.Debugf("Executing EVM call finished %s runtime %s", time.Now().String(), time.Since(start).String())
}(time.Now())
state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
} }
var data []byte if err := overrides.Apply(state); err != nil {
if args.Input != nil { return nil, err
data = *args.Input
} else if args.Data != nil {
data = *args.Data
} }
var accessList types.AccessList
if args.AccessList != nil {
accessList = *args.AccessList
}
// Create new call message
msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, gasFeeCap, gasTipCap, data, accessList, false)
// Setup context so it may be cancelled the call has completed // Setup context so it may be cancelled the call has completed
// or, in case of unmetered gas, setup a context with a timeout. // or, in case of unmetered gas, setup a context with a timeout.
var cancel context.CancelFunc var cancel context.CancelFunc
@ -970,10 +958,16 @@ func DoCall(ctx context.Context, b *Backend, args CallArgs, blockNrOrHash rpc.Bl
defer cancel() defer cancel()
// Get a new instance of the EVM. // Get a new instance of the EVM.
evm, err := b.GetEVM(ctx, msg, state, header) msg, err := args.ToMessage(globalGasCap, header.BaseFee)
if err != nil { if err != nil {
return nil, 0, false, err return nil, err
} }
evm, vmError, err := b.GetEVM(ctx, msg, state, header)
if err != nil {
return nil, err
}
// Wait for the context to be done and cancel the evm. Even if the // Wait for the context to be done and cancel the evm. Even if the
// EVM has finished, cancelling may be done (repeatedly) // EVM has finished, cancelling may be done (repeatedly)
go func() { go func() {
@ -981,18 +975,21 @@ func DoCall(ctx context.Context, b *Backend, args CallArgs, blockNrOrHash rpc.Bl
evm.Cancel() evm.Cancel()
}() }()
// Setup the gas pool (also for unmetered requests) // Execute the message.
// and apply the message.
gp := new(core.GasPool).AddGas(math.MaxUint64) gp := new(core.GasPool).AddGas(math.MaxUint64)
result, err := core.ApplyMessage(evm, msg, gp) result, err := core.ApplyMessage(evm, msg, gp)
if err != nil { if err := vmError(); err != nil {
return nil, 0, false, fmt.Errorf("execution failed: %v", err) return nil, err
} }
// If the timer caused an abort, return an appropriate error message // If the timer caused an abort, return an appropriate error message
if evm.Cancelled() { if evm.Cancelled() {
return nil, 0, false, fmt.Errorf("execution aborted (timeout = %v)", timeout) return nil, fmt.Errorf("execution aborted (timeout = %v)", timeout)
} }
return result.Return(), result.UsedGas, result.Failed(), err if err != nil {
return result, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas())
}
return result, nil
} }
// writeStateDiffAtOrFor calls out to the proxy statediffing geth client to fill in a gap in the index // writeStateDiffAtOrFor calls out to the proxy statediffing geth client to fill in a gap in the index

View File

@ -26,7 +26,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
@ -620,11 +619,11 @@ func (b *Backend) GetCanonicalHeader(number uint64) (string, []byte, error) {
} }
// GetEVM constructs and returns a vm.EVM // GetEVM constructs and returns a vm.EVM
func (b *Backend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header) (*vm.EVM, error) { func (b *Backend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, header *types.Header) (*vm.EVM, func() error, error) {
state.SetBalance(msg.From(), math.MaxBig256) vmError := func() error { return nil }
vmctx := core.NewEVMBlockContext(header, b, nil)
txContext := core.NewEVMTxContext(msg) txContext := core.NewEVMTxContext(msg)
return vm.NewEVM(vmctx, txContext, state, b.Config.ChainConfig, b.Config.VMConfig), nil context := core.NewEVMBlockContext(header, b, nil)
return vm.NewEVM(context, txContext, state, b.Config.ChainConfig, b.Config.VMConfig), vmError, nil
} }
// GetAccountByNumberOrHash returns the account object for the provided address at the block corresponding to the provided number or hash // GetAccountByNumberOrHash returns the account object for the provided address at the block corresponding to the provided number or hash

View File

@ -17,14 +17,17 @@
package eth package eth
import ( import (
"errors"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/statediff/indexer/ipfs" "github.com/ethereum/go-ethereum/statediff/indexer/ipfs"
"github.com/ethereum/go-ethereum/statediff/indexer/models" "github.com/ethereum/go-ethereum/statediff/indexer/models"
sdtypes "github.com/ethereum/go-ethereum/statediff/types" sdtypes "github.com/ethereum/go-ethereum/statediff/types"
"github.com/sirupsen/logrus"
) )
// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction // RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
@ -99,18 +102,94 @@ type CallArgs struct {
Input *hexutil.Bytes `json:"input"` Input *hexutil.Bytes `json:"input"`
} }
// account indicates the overriding fields of account during the execution of // from retrieves the transaction sender address.
// a message call. func (arg *CallArgs) from() common.Address {
// Note, state and stateDiff can't be specified at the same time. If state is if arg.From == nil {
// set, message execution will only use the data in the given state. Otherwise return common.Address{}
// if statDiff is set, all diff will be applied first and then execute the call }
// message. return *arg.From
type account struct { }
Nonce *hexutil.Uint64 `json:"nonce"`
Code *hexutil.Bytes `json:"code"` // data retrieves the transaction calldata. Input field is preferred.
Balance **hexutil.Big `json:"balance"` func (arg *CallArgs) data() []byte {
State *map[common.Hash]common.Hash `json:"state"` if arg.Input != nil {
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` return *arg.Input
}
if arg.Data != nil {
return *arg.Data
}
return nil
}
// ToMessage converts the transaction arguments to the Message type used by the
// core evm. This method is used in calls and traces that do not require a real
// live transaction.
func (arg *CallArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (types.Message, error) {
// Reject invalid combinations of pre- and post-1559 fee styles
if arg.GasPrice != nil && (arg.MaxFeePerGas != nil || arg.MaxPriorityFeePerGas != nil) {
return types.Message{}, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
}
// Set sender address or use zero address if none specified.
addr := arg.from()
// Set default gas & gas price if none were set
gas := globalGasCap
if gas == 0 {
gas = uint64(math.MaxUint64 / 2)
}
if arg.Gas != nil {
gas = uint64(*arg.Gas)
}
if globalGasCap != 0 && globalGasCap < gas {
logrus.Warn("Caller gas above allowance, capping", "requested", gas, "cap", globalGasCap)
gas = globalGasCap
}
var (
gasPrice *big.Int
gasFeeCap *big.Int
gasTipCap *big.Int
)
if baseFee == nil {
// If there's no basefee, then it must be a non-1559 execution
gasPrice = new(big.Int)
if arg.GasPrice != nil {
gasPrice = arg.GasPrice.ToInt()
}
gasFeeCap, gasTipCap = gasPrice, gasPrice
} else {
// A basefee is provided, necessitating 1559-type execution
if arg.GasPrice != nil {
// User specified the legacy gas field, convert to 1559 gas typing
gasPrice = arg.GasPrice.ToInt()
gasFeeCap, gasTipCap = gasPrice, gasPrice
} else {
// User specified 1559 gas feilds (or none), use those
gasFeeCap = new(big.Int)
if arg.MaxFeePerGas != nil {
gasFeeCap = arg.MaxFeePerGas.ToInt()
}
gasTipCap = new(big.Int)
if arg.MaxPriorityFeePerGas != nil {
gasTipCap = arg.MaxPriorityFeePerGas.ToInt()
}
// Backfill the legacy gasPrice for EVM execution, unless we're all zeroes
gasPrice = new(big.Int)
if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 {
gasPrice = math.BigMin(new(big.Int).Add(gasTipCap, baseFee), gasFeeCap)
}
}
}
value := new(big.Int)
if arg.Value != nil {
value = arg.Value.ToInt()
}
data := arg.data()
var accessList types.AccessList
if arg.AccessList != nil {
accessList = *arg.AccessList
}
msg := types.NewMessage(addr, arg.To, 0, value, gas, gasPrice, gasFeeCap, gasTipCap, data, accessList, true)
return msg, nil
} }
// IPLDs is used to package raw IPLD block data fetched from IPFS and returned by the server // IPLDs is used to package raw IPLD block data fetched from IPFS and returned by the server

View File

@ -835,16 +835,20 @@ func (b *Block) Call(ctx context.Context, args struct {
return nil, err return nil, err
} }
} }
result, gas, failed, err := eth.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, 5*time.Second, b.backend.RPCGasCap()) result, err := eth.DoCall(ctx, b.backend, args.Data, *b.numberOrHash, nil, 5*time.Second, b.backend.RPCGasCap().Uint64())
if err != nil {
return nil, err
}
status := hexutil.Uint64(1) status := hexutil.Uint64(1)
if failed { if result.Failed() {
status = 0 status = 0
} }
return &CallResult{ return &CallResult{
data: hexutil.Bytes(result), data: result.ReturnData,
gasUsed: hexutil.Uint64(gas), gasUsed: hexutil.Uint64(result.UsedGas),
status: status, status: status,
}, err }, nil
} }
// Resolver is the top-level object in the GraphQL hierarchy. // Resolver is the top-level object in the GraphQL hierarchy.

View File

@ -100,7 +100,7 @@ func NewServer(settings *Config) (Server, error) {
var err error var err error
sap.backend, err = eth.NewEthBackend(sap.db, &eth.Config{ sap.backend, err = eth.NewEthBackend(sap.db, &eth.Config{
ChainConfig: settings.ChainConfig, ChainConfig: settings.ChainConfig,
VMConfig: vm.Config{}, VMConfig: vm.Config{NoBaseFee: true},
DefaultSender: settings.DefaultSender, DefaultSender: settings.DefaultSender,
RPCGasCap: settings.RPCGasCap, RPCGasCap: settings.RPCGasCap,
GroupCacheConfig: settings.GroupCache, GroupCacheConfig: settings.GroupCache,