[core] Return EVM errors from ApplyMessage for external use (#3375)

* [rpc] Return EVM errors as RPC errors for smart contract Calls

	* [core] Return ExecutionResult from TransitionDb & ApplyMessage
	* [core] Update ApplyTransation to use ExecutionResult
	* [rpc] Update doEstimateGas to use ExecutionResult

* [rpc] Remove redundant VMError error message wrap

* [rpc] Remove debug prints

* [rpc] Revert hmy_call RPC behavior to keep backwards compatibility

* [core] Return ExecutionResult struct instead of pointer to struct
pull/3384/head
Janet Liang 4 years ago committed by GitHub
parent 5d1517cdf7
commit 2bd4083b61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      core/state_processor.go
  2. 55
      core/state_transition.go
  3. 5
      hmy/hmy.go
  4. 33
      rpc/contract.go
  5. 4
      rpc/transaction.go

@ -195,7 +195,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
// about the transaction and calling mechanisms. // about the transaction and calling mechanisms.
vmenv := vm.NewEVM(context, statedb, config, cfg) vmenv := vm.NewEVM(context, statedb, config, cfg)
// Apply the transaction to the current state (included in the env) // Apply the transaction to the current state (included in the env)
_, gas, failed, err := ApplyMessage(vmenv, msg, gp) result, err := ApplyMessage(vmenv, msg, gp)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
@ -206,13 +206,14 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
} else { } else {
root = statedb.IntermediateRoot(config.IsS3(header.Epoch())).Bytes() root = statedb.IntermediateRoot(config.IsS3(header.Epoch())).Bytes()
} }
*usedGas += gas *usedGas += result.UsedGas
failedExe := result.VMErr != nil
// Create a new receipt for the transaction, storing the intermediate root and gas used by the tx // Create a new receipt for the transaction, storing the intermediate root and gas used by the tx
// based on the eip phase, we're passing whether the root touch-delete accounts. // based on the eip phase, we're passing whether the root touch-delete accounts.
receipt := types.NewReceipt(root, failed, *usedGas) receipt := types.NewReceipt(root, failedExe, *usedGas)
receipt.TxHash = tx.Hash() receipt.TxHash = tx.Hash()
receipt.GasUsed = gas receipt.GasUsed = result.UsedGas
// if the transaction created a contract, store the creation address in the receipt. // if the transaction created a contract, store the creation address in the receipt.
if msg.To() == nil { if msg.To() == nil {
receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce()) receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
@ -226,13 +227,13 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
var cxReceipt *types.CXReceipt var cxReceipt *types.CXReceipt
// Do not create cxReceipt if EVM call failed // Do not create cxReceipt if EVM call failed
if txType == types.SubtractionOnly && !failed { if txType == types.SubtractionOnly && !failedExe {
cxReceipt = &types.CXReceipt{tx.Hash(), msg.From(), msg.To(), tx.ShardID(), tx.ToShardID(), msg.Value()} cxReceipt = &types.CXReceipt{tx.Hash(), msg.From(), msg.To(), tx.ShardID(), tx.ToShardID(), msg.Value()}
} else { } else {
cxReceipt = nil cxReceipt = nil
} }
return receipt, cxReceipt, gas, err return receipt, cxReceipt, result.UsedGas, err
} }
// ApplyStakingTransaction attempts to apply a staking transaction to the given state database // ApplyStakingTransaction attempts to apply a staking transaction to the given state database

@ -22,15 +22,14 @@ import (
"math/big" "math/big"
"sort" "sort"
staking2 "github.com/harmony-one/harmony/staking"
"github.com/harmony-one/harmony/staking/network"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/internal/utils"
staking2 "github.com/harmony-one/harmony/staking"
"github.com/harmony-one/harmony/staking/network"
staking "github.com/harmony-one/harmony/staking/types" staking "github.com/harmony-one/harmony/staking/types"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -97,6 +96,13 @@ type Message interface {
BlockNum() *big.Int BlockNum() *big.Int
} }
// ExecutionResult is the return value from a transaction committed to the DB
type ExecutionResult struct {
ReturnData []byte
UsedGas uint64
VMErr error
}
// IntrinsicGas computes the 'intrinsic gas' for a message with the given data. // IntrinsicGas computes the 'intrinsic gas' for a message with the given data.
func IntrinsicGas(data []byte, contractCreation, homestead, istanbul, isValidatorCreation bool) (uint64, error) { func IntrinsicGas(data []byte, contractCreation, homestead, istanbul, isValidatorCreation bool) (uint64, error) {
// Set the starting gas for the raw transaction // Set the starting gas for the raw transaction
@ -157,7 +163,7 @@ func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool, bc ChainContext)
// the gas used (which includes gas refunds) and an error if it failed. An error always // the gas used (which includes gas refunds) and an error if it failed. An error always
// indicates a core error meaning that the message would always fail for that particular // indicates a core error meaning that the message would always fail for that particular
// state and would never be accepted within a block. // state and would never be accepted within a block.
func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) ([]byte, uint64, bool, error) { func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) (ExecutionResult, error) {
return NewStateTransition(evm, msg, gp, nil).TransitionDb() return NewStateTransition(evm, msg, gp, nil).TransitionDb()
} }
@ -218,9 +224,9 @@ func (st *StateTransition) preCheck() error {
// TransitionDb will transition the state by applying the current message and // TransitionDb will transition the state by applying the current message and
// returning the result including the used gas. It returns an error if failed. // returning the result including the used gas. It returns an error if failed.
// An error indicates a consensus issue. // An error indicates a consensus issue.
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) { func (st *StateTransition) TransitionDb() (ExecutionResult, error) {
if err = st.preCheck(); err != nil { if err := st.preCheck(); err != nil {
return return ExecutionResult{}, err
} }
msg := st.msg msg := st.msg
sender := vm.AccountRef(msg.From()) sender := vm.AccountRef(msg.From())
@ -231,34 +237,33 @@ func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bo
// Pay intrinsic gas // Pay intrinsic gas
gas, err := IntrinsicGas(st.data, contractCreation, homestead, istanbul, false) gas, err := IntrinsicGas(st.data, contractCreation, homestead, istanbul, false)
if err != nil { if err != nil {
return nil, 0, false, err return ExecutionResult{}, err
} }
if err = st.useGas(gas); err != nil { if err = st.useGas(gas); err != nil {
return nil, 0, false, err return ExecutionResult{}, err
} }
var ( evm := st.evm
evm = st.evm
// vm errors do not effect consensus and are therefor var ret []byte
// not assigned to err, except for insufficient balance // All VM errors are valid except for insufficient balance, therefore returned separately
// error. var vmErr error
vmerr error
)
if contractCreation { if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value) ret, _, st.gas, vmErr = evm.Create(sender, st.data, st.gas, st.value)
} else { } else {
// Increment the nonce for the next transaction // Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value) ret, st.gas, vmErr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
} }
if vmerr != nil { if vmErr != nil {
utils.Logger().Info().Err(vmerr).Msg("VM returned with error") utils.Logger().Info().Err(vmErr).Msg("VM returned with error")
// The only possible consensus-error would be if there wasn't // The only possible consensus-error would be if there wasn't
// sufficient balance to make the transfer happen. The first // sufficient balance to make the transfer happen. The first
// balance transfer may never fail. // balance transfer may never fail.
if vmerr == vm.ErrInsufficientBalance { if vmErr == vm.ErrInsufficientBalance {
return nil, 0, false, vmerr return ExecutionResult{}, vmErr
} }
} }
st.refundGas() st.refundGas()
@ -269,7 +274,11 @@ func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bo
st.state.AddBalance(st.evm.Coinbase, txFee) st.state.AddBalance(st.evm.Coinbase, txFee)
} }
return ret, st.gasUsed(), vmerr != nil, err return ExecutionResult{
ReturnData: ret,
UsedGas: st.gasUsed(),
VMErr: vmErr,
}, err
} }
func (st *StateTransition) refundGas() { func (st *StateTransition) refundGas() {

@ -200,11 +200,10 @@ func (hmy *Harmony) GetNodeMetadata() commonRPC.NodeMetadata {
} }
// GetEVM returns a new EVM entity // GetEVM returns a new EVM entity
func (hmy *Harmony) GetEVM(ctx context.Context, msg core.Message, state *state.DB, header *block.Header) (*vm.EVM, func() error, error) { func (hmy *Harmony) GetEVM(ctx context.Context, msg core.Message, state *state.DB, header *block.Header) (*vm.EVM, error) {
state.SetBalance(msg.From(), math.MaxBig256) state.SetBalance(msg.From(), math.MaxBig256)
vmError := func() error { return nil }
vmCtx := core.NewEVMContext(msg, header, hmy.BlockChain, nil) vmCtx := core.NewEVMContext(msg, header, hmy.BlockChain, nil)
return vm.NewEVM(vmCtx, state, hmy.BlockChain.Config(), *hmy.BlockChain.GetVMConfig()), vmError, nil return vm.NewEVM(vmCtx, state, hmy.BlockChain.Config(), *hmy.BlockChain.GetVMConfig()), nil
} }
// ChainDb .. // ChainDb ..

@ -15,7 +15,7 @@ import (
"github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/hmy" "github.com/harmony-one/harmony/hmy"
internal_common "github.com/harmony-one/harmony/internal/common" hmyCommon "github.com/harmony-one/harmony/internal/common"
"github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/internal/utils"
) )
@ -50,10 +50,13 @@ func (s *PublicContractService) Call(
blockNum := blockNumber.EthBlockNumber() blockNum := blockNumber.EthBlockNumber()
// Execute call // Execute call
result, _, _, err := doCall(ctx, s.hmy, args, blockNum, vm.Config{}, CallTimeout, s.hmy.RPCGasCap) result, err := doCall(ctx, s.hmy, args, blockNum, vm.Config{}, CallTimeout, s.hmy.RPCGasCap)
if err != nil {
return nil, err
}
// Response output is the same for all versions // If VM returns error, still return the ReturnData, which is the contract error message
return result, err return result.ReturnData, nil
} }
// GetCode returns the code stored at the given address in the state for the given block number. // GetCode returns the code stored at the given address in the state for the given block number.
@ -64,7 +67,7 @@ func (s *PublicContractService) GetCode(
blockNum := blockNumber.EthBlockNumber() blockNum := blockNumber.EthBlockNumber()
// Fetch state // Fetch state
address := internal_common.ParseAddr(addr) address := hmyCommon.ParseAddr(addr)
state, _, err := s.hmy.StateAndHeaderByNumber(ctx, blockNum) state, _, err := s.hmy.StateAndHeaderByNumber(ctx, blockNum)
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
@ -89,7 +92,7 @@ func (s *PublicContractService) GetStorageAt(
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }
address := internal_common.ParseAddr(addr) address := hmyCommon.ParseAddr(addr)
res := state.GetState(address, common.HexToHash(key)) res := state.GetState(address, common.HexToHash(key))
// Response output is the same for all versions // Response output is the same for all versions
@ -100,7 +103,7 @@ func (s *PublicContractService) GetStorageAt(
func doCall( func doCall(
ctx context.Context, hmy *hmy.Harmony, args CallArgs, blockNum rpc.BlockNumber, ctx context.Context, hmy *hmy.Harmony, args CallArgs, blockNum rpc.BlockNumber,
vmCfg vm.Config, timeout time.Duration, globalGasCap *big.Int, vmCfg vm.Config, timeout time.Duration, globalGasCap *big.Int,
) ([]byte, uint64, bool, error) { ) (core.ExecutionResult, error) {
defer func(start time.Time) { defer func(start time.Time) {
utils.Logger().Debug(). utils.Logger().Debug().
Dur("runtime", time.Since(start)). Dur("runtime", time.Since(start)).
@ -110,7 +113,7 @@ func doCall(
// Fetch state // Fetch state
state, header, err := hmy.StateAndHeaderByNumber(ctx, blockNum) state, header, err := hmy.StateAndHeaderByNumber(ctx, blockNum)
if state == nil || err != nil { if state == nil || err != nil {
return nil, 0, false, err return core.ExecutionResult{}, err
} }
// Set sender address or use a default if none specified // Set sender address or use a default if none specified
@ -166,9 +169,9 @@ func doCall(
defer cancel() defer cancel()
// Get a new instance of the EVM. // Get a new instance of the EVM.
evm, vmError, err := hmy.GetEVM(ctx, msg, state, header) evm, err := hmy.GetEVM(ctx, msg, state, header)
if err != nil { if err != nil {
return nil, 0, false, err return core.ExecutionResult{}, 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
@ -181,16 +184,16 @@ func doCall(
// Setup the gas pool (also for unmetered requests) // Setup the gas pool (also for unmetered requests)
// and apply the message. // and apply the message.
gp := new(core.GasPool).AddGas(math.MaxUint64) gp := new(core.GasPool).AddGas(math.MaxUint64)
res, gas, failed, err := core.ApplyMessage(evm, msg, gp) result, err := core.ApplyMessage(evm, msg, gp)
if err := vmError(); err != nil { if err != nil {
return nil, 0, false, err return core.ExecutionResult{}, 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 core.ExecutionResult{}, fmt.Errorf("execution aborted (timeout = %v)", timeout)
} }
// Response output is the same for all versions // Response output is the same for all versions
return res, gas, failed, err return result, nil
} }

@ -724,8 +724,8 @@ func doEstimateGas(
executable := func(gas uint64) bool { executable := func(gas uint64) bool {
args.Gas = (*hexutil.Uint64)(&gas) args.Gas = (*hexutil.Uint64)(&gas)
_, _, failed, err := doCall(ctx, hmy, args, blockNum, vm.Config{}, 0, big.NewInt(int64(max))) result, err := doCall(ctx, hmy, args, blockNum, vm.Config{}, 0, big.NewInt(int64(max)))
if err != nil || failed { if err != nil || result.VMErr == nil {
return false return false
} }
return true return true

Loading…
Cancel
Save