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

all: improve EstimateGas API #20830

Merged
merged 9 commits into from
Apr 22, 2020
Prev Previous commit
Next Next commit
all: address comments
  • Loading branch information
rjl493456442 committed Apr 20, 2020
commit 662dab7bf530a21bcd364442bf46e326cf4e4d12
14 changes: 7 additions & 7 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallM
if err != nil {
return nil, err
}
return res.Result, nil
return res.Return(), nil
}

// PendingCallContract executes a contract call on the pending state.
Expand All @@ -365,7 +365,7 @@ func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call ethereu
if err != nil {
return nil, err
}
return res.Result, nil
return res.Return(), nil
}

// PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving
Expand Down Expand Up @@ -411,7 +411,7 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
b.pendingState.RevertToSnapshot(snapshot)

if err != nil {
if err == core.ErrInsufficientIntrinsicGas {
if err == core.ErrIntrinsicGas {
return true, nil, nil // Special case, raise gas limit
}
return true, nil, err // Bail out
Expand All @@ -424,8 +424,8 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
failed, _, err := executable(mid)

// If the error is not nil(consensus error), it means the provided message
// call or transaction will never be accpeted no matter how many gas assigened.
// Return the error directly, don't struggle any more
// call or transaction will never be accepted no matter how much gas it is
// assigned. Return the error directly, don't struggle any more
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't strictly true. A transaction may have a line like require(msg.gas < 1,000,000, "Not enough gas"); or similar. We may not care about this class of failures (where the failure is dependent on gas, timestamp, block number, etc.) but there may be value in adding clarity to the comment.

Copy link
Member

Choose a reason for hiding this comment

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

@MicahZoltu It might help if we could collect a small pool of solidity code to add to tests and the desired behavior in those cases.

Code that depends on the gas counter is a bit hard to handle correctly. e.g.

require(msg.gas < 1,000,000, "Not enough gas")
require(msg.gas > 4,000,000, "Too much gas")

^ cannot be binary searched, so the estimator cannot ever properly pick a correct number. It might be a valid corner case though to support estimating these kinds of txs as long as they can be binary searched.

Copy link
Member

Choose a reason for hiding this comment

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

Feel free to open a new issue with a code that fails estimation and we can add some tweaks.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah sorry, I don't think I actually want support for these edge cases, it feels not worth the effort. I was merely stating that the comment in the code isn't strictly correct and for clarity to future readers it may be valuable to mention something like "we don't care about these edge cases at the moment" in the comment.

Copy link
Member Author

@rjl493456442 rjl493456442 Apr 30, 2020

Choose a reason for hiding this comment

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

@MicahZoltu Actually the scenario you described is handled. Since all revert will lead to an internal evm error which is wrapped by ExecutionResult. However the error here is another one refers to some consensus issues(e.g. the nonce is not correct), so in this case no matter how much gas is assigned, the transaction will never be accepted.

if err != nil {
return 0, err
}
Expand All @@ -445,8 +445,8 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
if result != nil {
if result.Err != vm.ErrOutOfGas {
errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err)
if len(result.RevertReason) > 0 {
errMsg += fmt.Sprintf(" (0x%x)", result.RevertReason)
if len(result.Revert()) > 0 {
errMsg += fmt.Sprintf(" (0x%x)", result.Revert())
}
return 0, errors.New(errMsg)
}
Expand Down
31 changes: 18 additions & 13 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ var (
ErrNoGenesis = errors.New("genesis not found in chain")
)

// State transition consensus errors, any of them encountered during
// the block processing can lead to consensus issue.
// List of evm-call-message pre-checking errors. All state transtion messages will
// be pre-checked before execution. If any invalidation detected, the corresponding
// error should be returned which is defined here.
//
// - If the pre-checking happens in the miner, then the transaction won't be packed.
// - If the pre-checking happens in the block processing procedure, then a "BAD BLOCk"
// error should be emitted.
var (
// ErrNonceTooLow is returned if the nonce of a transaction is lower than the
// one present in the local chain.
Expand All @@ -44,18 +49,18 @@ var (
// by a transaction is higher than what's left in the block.
ErrGasLimitReached = errors.New("gas limit reached")

// ErrInsufficientBalanceForTransfer is returned if the transaction sender doesn't
// have enough balance for transfer(topmost call only).
ErrInsufficientBalanceForTransfer = errors.New("insufficient balance for transfer")
// ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't
// have enough funds for transfer(topmost call only).
ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer")

// ErrInsufficientBalanceForFee is returned if transaction sender doesn't have
// enough balance to cover transaction fee.
ErrInsufficientBalanceForFee = errors.New("insufficient balance to pay fee")
// ErrInsufficientFunds is returned if the total cost of executing a transaction
// is higher than the balance of the user's account.
ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value")

// ErrGasOverflow is returned when calculating gas usage.
ErrGasOverflow = errors.New("gas overflow")
// ErrGasUintOverflow is returned when calculating gas usage.
ErrGasUintOverflow = errors.New("gas uint64 overflow")

// ErrInsufficientIntrinsicGas is returned when the gas limit speicified in transaction
// is not enought to cover intrinsic gas usage.
ErrInsufficientIntrinsicGas = errors.New("insufficient intrinsic gas")
// ErrIntrinsicGas is returned if the transaction is specified to use less gas
// than required to start the invocation.
ErrIntrinsicGas = errors.New("intrinsic gas too low")
)
58 changes: 33 additions & 25 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,12 @@ type Message interface {
Data() []byte
}

// ExecutionResult includes all output after executing given evm message
// no matter the execution itself is successful or not.
// ExecutionResult includes all output after executing given evm
Copy link
Member

@karalabe karalabe Apr 20, 2020

Choose a reason for hiding this comment

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

Wondering if we should move this to vm? Seems a bit clearer to type vm.ExecutionResult vs. core.EvexutionResult. Is there some dependency loop that would make this undoable?

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason to put this definition here is: after raw evm execution(we get the ret and vmerr), we still need to apply some additional rules(e.g. refund gas), so the usedGas field has to be updated in the core scope.

Btw now all my VPN nodes are blocked :P. Now I can't login my discord account(also the reason for missing standup). Sorry about it.

// 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 exection(listed in core/vm/errors.go)
Result []byte // Returned value of the calling function
RevertReason []byte // Reason to perform revert thrown by solidity code
UsedGas uint64 // Total used gas but include the refunded gas
Err error // Any error encountered during the exection(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 All @@ -86,6 +85,24 @@ func (result *ExecutionResult) Unwrap() error {
// Failed returns the indicator whether the execution is successful or not
func (result *ExecutionResult) Failed() bool { return result.Err != nil }

// Return is a helper function to help caller distinguish between revert reason
// and function return. Return returns the data after execution if no error occurs.
func (result *ExecutionResult) Return() []byte {
if result.Err != nil {
return nil
}
return common.CopyBytes(result.ReturnData)
}

// Revert returns the concrete revert reason if the execution is aborted by `REVERT`
// opcode. Note the reason can be nil if no data supplied with revert opcode.
func (result *ExecutionResult) Revert() []byte {
if result.Err != vm.ErrExecutionReverted {
return nil
}
return common.CopyBytes(result.ReturnData)
}

// IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
func IntrinsicGas(data []byte, contractCreation, isHomestead bool, isEIP2028 bool) (uint64, error) {
// Set the starting gas for the raw transaction
Expand All @@ -110,13 +127,13 @@ func IntrinsicGas(data []byte, contractCreation, isHomestead bool, isEIP2028 boo
nonZeroGas = params.TxDataNonZeroGasEIP2028
}
if (math.MaxUint64-gas)/nonZeroGas < nz {
return 0, ErrGasOverflow
return 0, ErrGasUintOverflow
}
gas += nz * nonZeroGas

z := uint64(len(data)) - nz
if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
return 0, ErrGasOverflow
return 0, ErrGasUintOverflow
}
gas += z * params.TxDataZeroGas
}
Expand Down Expand Up @@ -158,7 +175,7 @@ func (st *StateTransition) to() common.Address {
func (st *StateTransition) buyGas() error {
mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
if st.state.GetBalance(st.msg.From()).Cmp(mgval) < 0 {
return ErrInsufficientBalanceForFee
return ErrInsufficientFunds
}
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
return err
Expand Down Expand Up @@ -188,10 +205,8 @@ func (st *StateTransition) preCheck() error {
//
// - used gas:
// total gas used (including gas being refunded)
// - execution result:
// the return value of the calling function
// - revert reason:
// reason to perform revert thrown by solidity code
// - returndata:
// the returned data from evm
// - concrete execution error:
// various **EVM** error which aborts the execution,
// e.g. ErrOutOfGas, ErrExecutionReverted
Expand Down Expand Up @@ -225,13 +240,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
return nil, err
}
if st.gas < gas {
return nil, ErrInsufficientIntrinsicGas
return nil, ErrIntrinsicGas
}
st.gas -= gas

// Check clause 6
if msg.Value().Sign() > 0 && !st.evm.CanTransfer(st.state, msg.From(), msg.Value()) {
return nil, ErrInsufficientBalanceForTransfer
return nil, ErrInsufficientFundsForTransfer
}
var (
ret []byte
Expand All @@ -247,17 +262,10 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) {
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))

var revert, result []byte
if vmerr == vm.ErrExecutionReverted {
revert = ret // Revert reason will be returned in ret iif the vmerr is ErrExecutionReverted
} else {
result = ret // Otherwise the ret represents the execution result, may nil
}
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
Result: result,
RevertReason: revert,
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
}, nil
}

Expand Down
8 changes: 0 additions & 8 deletions core/tx_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ var (
// with a different one without the required price bump.
ErrReplaceUnderpriced = errors.New("replacement transaction underpriced")

// ErrInsufficientFunds is returned if the total cost of executing a transaction
// is higher than the balance of the user's account.
ErrInsufficientFunds = errors.New("insufficient funds for gas * price + value")

// ErrIntrinsicGas is returned if the transaction is specified to use less gas
// than required to start the invocation.
ErrIntrinsicGas = errors.New("intrinsic gas too low")

// ErrGasLimit is returned if a transaction's requested gas limit exceeds the
// maximum allowance of the current block.
ErrGasLimit = errors.New("exceeds block gas limit")
Expand Down
10 changes: 5 additions & 5 deletions core/vm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,17 @@ func (e *ErrStackUnderflow) Error() string {
// ErrStackOverflow wraps an evm error when the items on the stack exceeds
// the maximum allowance.
type ErrStackOverflow struct {
stackLen int
allowance int
stackLen int
limit int
}

func (e *ErrStackOverflow) Error() string {
return fmt.Sprintf("stack limit reached %d (%d)", e.stackLen, e.allowance)
return fmt.Sprintf("stack limit reached %d (%d)", e.stackLen, e.limit)
}

// ErrInvalidOpCode wraps an evm error when an invalid opcode is encountered.
type ErrInvalidOpCode struct {
opcode int
opcode OpCode
}

func (e *ErrInvalidOpCode) Error() string { return fmt.Sprintf("invalid opcode 0x%x", e.opcode) }
func (e *ErrInvalidOpCode) Error() string { return fmt.Sprintf("invalid opcode 0x%x", byte(e.opcode)) }
4 changes: 2 additions & 2 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
}
// Fail if we're trying to transfer more than the available balance
// Note although it's noop to transfer X ether to caller itself. But
// if caller doesn't have enough balance, it can lead to an evm error.
// So the check here is necessary.
// if caller doesn't have enough balance, it would be an error to allow
// over-charging itself. So the check here is necessary.
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, gas, ErrInsufficientBalance
}
Expand Down
4 changes: 2 additions & 2 deletions core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,13 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
op = contract.GetOp(pc)
operation := in.cfg.JumpTable[op]
if !operation.valid {
return nil, &ErrInvalidOpCode{opcode: int(op)}
return nil, &ErrInvalidOpCode{opcode: op}
}
// Validate stack
if sLen := stack.len(); sLen < operation.minStack {
return nil, &ErrStackUnderflow{stackLen: sLen, required: operation.minStack}
} else if sLen > operation.maxStack {
return nil, &ErrStackOverflow{stackLen: sLen, allowance: operation.maxStack}
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// If the operation is valid, enforce and write restrictions
if in.readOnly && in.evm.chainRules.IsByzantium {
Expand Down
2 changes: 1 addition & 1 deletion eth/api_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ func (api *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, v
return &ethapi.ExecutionResult{
Gas: result.UsedGas,
Failed: result.Failed(),
ReturnValue: fmt.Sprintf("%x", result.Result),
ReturnValue: fmt.Sprintf("%x", result.Return()),
StructLogs: ethapi.FormatLogs(tracer.StructLogs()),
}, nil

Expand Down
4 changes: 2 additions & 2 deletions graphql/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@ func (b *Block) Call(ctx context.Context, args struct {
status = 0
}
return &CallResult{
data: result.Result,
data: result.Return(),
gasUsed: hexutil.Uint64(result.UsedGas),
status: status,
}, nil
Expand Down Expand Up @@ -881,7 +881,7 @@ func (p *Pending) Call(ctx context.Context, args struct {
status = 0
}
return &CallResult{
data: result.Result,
data: result.Return(),
gasUsed: hexutil.Uint64(result.UsedGas),
status: status,
}, nil
Expand Down
12 changes: 6 additions & 6 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr
if err != nil {
return nil, err
}
return result.Result, nil
return result.Return(), nil
}

func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) {
Expand Down Expand Up @@ -915,7 +915,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash

result, err := DoCall(ctx, b, args, blockNrOrHash, nil, vm.Config{}, 0, gasCap)
if err != nil {
if err == core.ErrInsufficientIntrinsicGas {
if err == core.ErrIntrinsicGas {
return true, nil, nil // Special case, raise gas limit
}
return true, nil, err // Bail out
Expand All @@ -928,8 +928,8 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
failed, _, err := executable(mid)

// If the error is not nil(consensus error), it means the provided message
// call or transaction will never be accpeted no matter how many gas assigened.
// Return the error directly, don't struggle any more
// 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
}
Expand All @@ -949,8 +949,8 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
if result != nil {
if result.Err != vm.ErrOutOfGas {
errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err)
if len(result.RevertReason) > 0 {
errMsg += fmt.Sprintf(" (0x%x)", result.RevertReason)
if len(result.Revert()) > 0 {
errMsg += fmt.Sprintf(" (0x%x)", result.Revert())
}
return 0, errors.New(errMsg)
}
Expand Down
4 changes: 2 additions & 2 deletions les/odr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
//vmenv := core.NewEnv(statedb, config, bc, msg, header, vm.Config{})
gp := new(core.GasPool).AddGas(math.MaxUint64)
result, _ := core.ApplyMessage(vmenv, msg, gp)
res = append(res, result.Result...)
res = append(res, result.Return()...)
}
} else {
header := lc.GetHeaderByHash(bhash)
Expand All @@ -148,7 +148,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
gp := new(core.GasPool).AddGas(math.MaxUint64)
result, _ := core.ApplyMessage(vmenv, msg, gp)
if state.Error() == nil {
res = append(res, result.Result...)
res = append(res, result.Return()...)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion light/odr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain
vmenv := vm.NewEVM(context, st, config, vm.Config{})
gp := new(core.GasPool).AddGas(math.MaxUint64)
result, _ := core.ApplyMessage(vmenv, msg, gp)
res = append(res, result.Result...)
res = append(res, result.Return()...)
if st.Error() != nil {
return res, st.Error()
}
Expand Down