From 2d5b73f46d845ae3c1ef16166d23783beadbda45 Mon Sep 17 00:00:00 2001 From: JukLee0ira Date: Tue, 26 Nov 2024 12:16:29 +0800 Subject: [PATCH] all: improve EstimateGas API (#20830) --- accounts/abi/abi_test.go | 32 ++++ accounts/abi/bind/backends/simulated.go | 73 +++++++-- accounts/abi/bind/backends/simulated_test.go | 154 +++++++++++++++++++ core/blockchain.go | 1 - core/error.go | 16 ++ core/state_processor.go | 12 +- core/state_transition.go | 92 ++++++----- core/token_validator.go | 4 +- core/vm/instructions.go | 2 +- eth/api_tracer.go | 10 +- eth/tracers/testing/calltrace_test.go | 4 +- eth/tracers/tracers_test.go | 6 +- internal/ethapi/api.go | 103 ++++++++----- les/odr_test.go | 8 +- light/odr_test.go | 4 +- tests/state_test_util.go | 2 +- 16 files changed, 403 insertions(+), 120 deletions(-) create mode 100644 accounts/abi/bind/backends/simulated_test.go diff --git a/accounts/abi/abi_test.go b/accounts/abi/abi_test.go index 4cf5668ff641..790505b8c043 100644 --- a/accounts/abi/abi_test.go +++ b/accounts/abi/abi_test.go @@ -19,6 +19,7 @@ package abi import ( "bytes" "encoding/hex" + "errors" "fmt" "log" "math/big" @@ -713,3 +714,34 @@ func TestABI_MethodById(t *testing.T) { } } + +func TestUnpackRevert(t *testing.T) { + t.Parallel() + + var cases = []struct { + input string + expect string + expectErr error + }{ + {"", "", errors.New("invalid data for unpacking")}, + {"08c379a1", "", errors.New("invalid data for unpacking")}, + {"08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d72657665727420726561736f6e00000000000000000000000000000000000000", "revert reason", nil}, + } + for index, c := range cases { + t.Run(fmt.Sprintf("case %d", index), func(t *testing.T) { + got, err := UnpackRevert(common.Hex2Bytes(c.input)) + if c.expectErr != nil { + if err == nil { + t.Fatalf("Expected non-nil error") + } + if err.Error() != c.expectErr.Error() { + t.Fatalf("Expected error mismatch, want %v, got %v", c.expectErr, err) + } + return + } + if c.expect != got { + t.Fatalf("Output mismatch, want %v, got %v", c.expect, got) + } + }) + } +} diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 4de5a89885dd..16d9b2ce364e 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -29,6 +29,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/XDCx" "github.com/XinFinOrg/XDPoSChain/XDCxlending" "github.com/XinFinOrg/XDPoSChain/accounts" + "github.com/XinFinOrg/XDPoSChain/accounts/abi" "github.com/XinFinOrg/XDPoSChain/accounts/abi/bind" "github.com/XinFinOrg/XDPoSChain/accounts/keystore" "github.com/XinFinOrg/XDPoSChain/common" @@ -53,7 +54,6 @@ import ( var _ bind.ContractBackend = (*SimulatedBackend)(nil) var errBlockNumberUnsupported = errors.New("SimulatedBackend cannot access blocks other than the latest block") -var errGasEstimationFailed = errors.New("gas required exceeds allowance or always failing transaction") // SimulatedBackend implements bind.ContractBackend, simulating a blockchain in // the background. Its main purpose is to allow easily testing contract bindings. @@ -160,6 +160,12 @@ func NewSimulatedBackend(alloc core.GenesisAlloc) *SimulatedBackend { return backend } +// Close terminates the underlying blockchain's update loop. +func (b *SimulatedBackend) Close() error { + b.blockchain.Stop() + return nil +} + // Commit imports all the pending transactions as a single block and starts a // fresh new state. func (b *SimulatedBackend) Commit() { @@ -289,8 +295,11 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call XDPoSChain.Cal if err != nil { return nil, err } - rval, _, _, err := b.callContract(ctx, call, b.blockchain.CurrentBlock(), state) - return rval, err + res, err := b.callContract(ctx, call, b.blockchain.CurrentBlock(), state) + if err != nil { + return nil, err + } + return res.Return(), nil } // PendingCallContract executes a contract call on the pending state. @@ -299,8 +308,11 @@ func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call XDPoSCh defer b.mu.Unlock() defer b.pendingState.RevertToSnapshot(b.pendingState.Snapshot()) - rval, _, _, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) - return rval, err + res, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) + if err != nil { + return nil, err + } + return res.Return(), nil } // PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving @@ -347,22 +359,33 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call XDPoSChain.Call cap = hi // Create a helper to check if a gas allowance results in an executable transaction - executable := func(gas uint64) bool { + executable := func(gas uint64) (bool, *core.ExecutionResult, error) { call.Gas = gas snapshot := b.pendingState.Snapshot() - _, _, failed, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) + res, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) b.pendingState.RevertToSnapshot(snapshot) - if err != nil || failed { - return false + if err != nil { + if err == core.ErrIntrinsicGas { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out } - return true + return res.Failed(), res, nil } // Execute the binary search and hone in on an executable gas limit for lo+1 < hi { mid := (hi + lo) / 2 - if !executable(mid) { + 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 { + return 0, err + } + if failed { lo = mid } else { hi = mid @@ -370,8 +393,25 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call XDPoSChain.Call } // Reject the transaction as invalid if it still fails at the highest allowance if hi == cap { - if !executable(hi) { - return 0, errGasEstimationFailed + failed, result, err := executable(hi) + if err != nil { + return 0, err + } + if failed { + if result != nil && result.Err != vm.ErrOutOfGas { + errMsg := fmt.Sprintf("always failing transaction (%v)", result.Err) + if len(result.Revert()) > 0 { + ret, err := abi.UnpackRevert(result.Revert()) + if err != nil { + errMsg += fmt.Sprintf(" (%#x)", result.Revert()) + } else { + errMsg += fmt.Sprintf(" (%s)", ret) + } + } + return 0, errors.New(errMsg) + } + // Otherwise, the specified gas cap is too low + return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) } } return hi, nil @@ -379,10 +419,10 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call XDPoSChain.Call // callContract implements common code between normal and pending contract calls. // state is modified during execution, make sure to copy it if necessary. -func (b *SimulatedBackend) callContract(ctx context.Context, call XDPoSChain.CallMsg, block *types.Block, statedb *state.StateDB) (ret []byte, usedGas uint64, failed bool, err error) { +func (b *SimulatedBackend) callContract(ctx context.Context, call XDPoSChain.CallMsg, block *types.Block, statedb *state.StateDB) (*core.ExecutionResult, error) { // Gas prices post 1559 need to be initialized if call.GasPrice != nil && (call.GasFeeCap != nil || call.GasTipCap != nil) { - return nil, 0, false, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") } head := b.blockchain.CurrentHeader() if !b.blockchain.Config().IsEIP1559(head.Number) { @@ -437,8 +477,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call XDPoSChain.Cal vmenv := vm.NewEVM(evmContext, txContext, statedb, nil, b.config, vm.Config{NoBaseFee: true}) gaspool := new(core.GasPool).AddGas(math.MaxUint64) owner := common.Address{} - ret, usedGas, failed, err, _ = core.NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner) - return + return core.NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner) } // SendTransaction updates the pending block to include the given transaction. diff --git a/accounts/abi/bind/backends/simulated_test.go b/accounts/abi/bind/backends/simulated_test.go new file mode 100644 index 000000000000..568189852310 --- /dev/null +++ b/accounts/abi/bind/backends/simulated_test.go @@ -0,0 +1,154 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package backends + +import ( + "context" + "errors" + "math/big" + "strings" + "testing" + + "github.com/XinFinOrg/XDPoSChain" + "github.com/XinFinOrg/XDPoSChain/accounts/abi" + "github.com/XinFinOrg/XDPoSChain/accounts/abi/bind" + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/params" +) + +func TestSimulatedBackend_EstimateGas(t *testing.T) { + /* + pragma solidity ^0.6.4; + contract GasEstimation { + function PureRevert() public { revert(); } + function Revert() public { revert("revert reason");} + function OOG() public { for (uint i = 0; ; i++) {}} + function Assert() public { assert(false);} + function Valid() public {} + }*/ + const contractAbi = "[{\"inputs\":[],\"name\":\"Assert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"OOG\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"PureRevert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"Revert\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"Valid\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" + const contractBin = "0x60806040523480156100115760006000fd5b50610017565b61016e806100266000396000f3fe60806040523480156100115760006000fd5b506004361061005c5760003560e01c806350f6fe3414610062578063aa8b1d301461006c578063b9b046f914610076578063d8b9839114610080578063e09fface1461008a5761005c565b60006000fd5b61006a610094565b005b6100746100ad565b005b61007e6100b5565b005b6100886100c2565b005b610092610135565b005b6000600090505b5b808060010191505061009b565b505b565b60006000fd5b565b600015156100bf57fe5b5b565b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252600d8152602001807f72657665727420726561736f6e0000000000000000000000000000000000000081526020015060200191505060405180910390fd5b565b5b56fea2646970667358221220345bbcbb1a5ecf22b53a78eaebf95f8ee0eceff6d10d4b9643495084d2ec934a64736f6c63430006040033" + + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + opts := bind.NewKeyedTransactor(key) + + sim := NewXDCSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(params.Ether)}}, 10000000, ¶ms.ChainConfig{ + ConstantinopleBlock: big.NewInt(0), + XDPoS: ¶ms.XDPoSConfig{ + Epoch: 900, + SkipV1Validation: true, + V2: ¶ms.V2{ + SwitchBlock: big.NewInt(900), + CurrentConfig: params.UnitTestV2Configs[0], + }, + }, + }) + + defer sim.Close() + + parsed, _ := abi.JSON(strings.NewReader(contractAbi)) + contractAddr, _, _, _ := bind.DeployContract(opts, parsed, common.FromHex(contractBin), sim) + sim.Commit() + + var cases = []struct { + name string + message XDPoSChain.CallMsg + expect uint64 + expectError error + }{ + {"plain transfer(valid)", XDPoSChain.CallMsg{ + From: addr, + To: &addr, + Gas: 0, + GasPrice: big.NewInt(0), + Value: big.NewInt(1), + Data: nil, + }, params.TxGas, nil}, + + {"plain transfer(invalid)", XDPoSChain.CallMsg{ + From: addr, + To: &contractAddr, + Gas: 0, + GasPrice: big.NewInt(0), + Value: big.NewInt(1), + Data: nil, + }, 0, errors.New("always failing transaction (execution reverted)")}, + + {"Revert", XDPoSChain.CallMsg{ + From: addr, + To: &contractAddr, + Gas: 0, + GasPrice: big.NewInt(0), + Value: nil, + Data: common.Hex2Bytes("d8b98391"), + }, 0, errors.New("always failing transaction (execution reverted) (revert reason)")}, + + {"PureRevert", XDPoSChain.CallMsg{ + From: addr, + To: &contractAddr, + Gas: 0, + GasPrice: big.NewInt(0), + Value: nil, + Data: common.Hex2Bytes("aa8b1d30"), + }, 0, errors.New("always failing transaction (execution reverted)")}, + + {"OOG", XDPoSChain.CallMsg{ + From: addr, + To: &contractAddr, + Gas: 100000, + GasPrice: big.NewInt(0), + Value: nil, + Data: common.Hex2Bytes("50f6fe34"), + }, 0, errors.New("gas required exceeds allowance (100000)")}, + + {"Assert", XDPoSChain.CallMsg{ + From: addr, + To: &contractAddr, + Gas: 100000, + GasPrice: big.NewInt(0), + Value: nil, + Data: common.Hex2Bytes("b9b046f9"), + }, 0, errors.New("always failing transaction (invalid opcode: INVALID)")}, + + {"Valid", XDPoSChain.CallMsg{ + From: addr, + To: &contractAddr, + Gas: 100000, + GasPrice: big.NewInt(0), + Value: nil, + Data: common.Hex2Bytes("e09fface"), + }, 21483, nil}, + } + for _, c := range cases { + got, err := sim.EstimateGas(context.Background(), c.message) + if c.expectError != nil { + if err == nil { + t.Fatalf("Expect error, got nil") + } + if c.expectError.Error() != err.Error() { + t.Fatalf("Expect error, want %v, got %v", c.expectError, err) + } + continue + } + if got != c.expect { + t.Fatalf("Gas estimation mismatch, want %d, got %d", c.expect, got) + } + } +} diff --git a/core/blockchain.go b/core/blockchain.go index 2f731331decc..e938e00c6985 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -58,7 +58,6 @@ import ( var ( blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) CheckpointCh = make(chan int) - ErrNoGenesis = errors.New("Genesis not found in chain") blockReorgMeter = metrics.NewRegisteredMeter("chain/reorg/executes", nil) blockReorgAddMeter = metrics.NewRegisteredMeter("chain/reorg/add", nil) diff --git a/core/error.go b/core/error.go index 4eb7eebf0ec9..67bb75730e84 100644 --- a/core/error.go +++ b/core/error.go @@ -29,6 +29,18 @@ var ( // ErrBlacklistedHash is returned if a block to import is on the blacklist. ErrBlacklistedHash = errors.New("blacklisted hash") + // ErrNoGenesis is returned when there is no Genesis Block. + ErrNoGenesis = errors.New("genesis not found in chain") +) + +// 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. ErrNonceTooLow = errors.New("nonce too low") @@ -45,6 +57,10 @@ var ( // by a transaction is higher than what's left in the block. ErrGasLimitReached = errors.New("gas limit reached") + // ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't + // have enough funds for transfer(topmost call only). + ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") + // ErrMaxInitCodeSizeExceeded is returned if creation transaction provides the init code bigger // than init code size limit. ErrMaxInitCodeSizeExceeded = errors.New("max initcode size exceeded") diff --git a/core/state_processor.go b/core/state_processor.go index 8234e36c8f7d..2f7980db6326 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -403,7 +403,7 @@ func applyTransaction(config *params.ChainConfig, tokensFee map[common.Address]* // End Bypass blacklist address // Apply the transaction to the current state (included in the env) - _, gas, failed, err, _ := ApplyMessage(evm, msg, gp, coinbaseOwner) + result, err := ApplyMessage(evm, msg, gp, coinbaseOwner) if err != nil { return nil, 0, err, false @@ -416,18 +416,18 @@ func applyTransaction(config *params.ChainConfig, tokensFee map[common.Address]* } else { root = statedb.IntermediateRoot(config.IsEIP158(blockNumber)).Bytes() } - *usedGas += gas + *usedGas += result.UsedGas // Create a new receipt for the transaction, storing the intermediate root and gas used // by the tx. receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} - if failed { + if result.Failed() { receipt.Status = types.ReceiptStatusFailed } else { receipt.Status = types.ReceiptStatusSuccessful } 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 msg.To() == nil { @@ -440,10 +440,10 @@ func applyTransaction(config *params.ChainConfig, tokensFee map[common.Address]* receipt.BlockHash = blockHash receipt.BlockNumber = blockNumber receipt.TransactionIndex = uint(statedb.TxIndex()) - if balanceFee != nil && failed { + if balanceFee != nil && result.Failed() { state.PayFeeWithTRC21TxFail(statedb, msg.From(), *to) } - return receipt, gas, err, balanceFee != nil + return receipt, result.UsedGas, err, balanceFee != nil } func getCoinbaseOwner(bc *BlockChain, statedb *state.StateDB, header *types.Header, author *common.Address) common.Address { diff --git a/core/state_transition.go b/core/state_transition.go index 90401eb817a5..4c9ee3fca09d 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -27,8 +27,8 @@ import ( "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/core/vm" "github.com/XinFinOrg/XDPoSChain/crypto" - "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/params" + "github.com/holiman/uint256" ) var emptyCodeHash = crypto.Keccak256Hash(nil) @@ -73,7 +73,6 @@ type StateTransition struct { // Message represents a message sent to a contract. type Message interface { From() common.Address - //FromFrontier() (common.Address, error) To() *common.Address GasPrice() *big.Int @@ -89,6 +88,35 @@ type Message interface { AccessList() types.AccessList } +// 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) +} + +// 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, accessList types.AccessList, isContractCreation, isHomestead bool, isEIP3860 bool) (uint64, error) { // Set the starting gas for the raw transaction @@ -166,7 +194,7 @@ func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition // 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 // state and would never be accepted within a block. -func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool, owner common.Address) ([]byte, uint64, bool, error, error) { +func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool, owner common.Address) (*ExecutionResult, error) { return NewStateTransition(evm, msg, gp).TransitionDb(owner) } @@ -213,7 +241,7 @@ func (st *StateTransition) buyGas() error { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From().Hex(), have, want) } } else if balanceTokenFee.Cmp(mgval) < 0 { - return errInsufficientBalanceForGas + return ErrInsufficientFunds } if err := st.gp.SubGas(st.msg.Gas()); err != nil { return err @@ -289,7 +317,7 @@ func (st *StateTransition) preCheck() error { // // However if any consensus issue encountered, return the error directly with // nil evm execution result. -func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedGas uint64, failed bool, err error, vmErr error) { +func (st *StateTransition) TransitionDb(owner common.Address) (*ExecutionResult, error) { // First check this message satisfies all consensus rules before // applying the message. The rules include these clauses // @@ -301,8 +329,8 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG // 6. caller has enough balance to cover asset transfer for **topmost** call // Check clauses 1-3, buy gas if everything is correct - if err = st.preCheck(); err != nil { - return nil, 0, false, err, nil + if err := st.preCheck(); err != nil { + return nil, err } var ( @@ -317,16 +345,16 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG // Check clauses 4-5, subtract intrinsic gas if everything is correct gas, err := IntrinsicGas(st.data, st.msg.AccessList(), contractCreation, homestead, rules.IsEIP1559) if err != nil { - return nil, 0, false, err, nil + return nil, err } if st.gas < gas { - return nil, 0, false, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas), nil + return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas) } st.gas -= gas // Check whether the init code size has been exceeded. if rules.IsEIP1559 && contractCreation && len(st.data) > params.MaxInitCodeSize { - return nil, 0, false, fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, len(st.data), params.MaxInitCodeSize), nil + return nil, fmt.Errorf("%w: code size %v limit %v", ErrMaxInitCodeSizeExceeded, len(st.data), params.MaxInitCodeSize) } // Execute the preparatory steps for state transition which includes: @@ -334,35 +362,25 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG // - reset transient storage(eip 1153) st.state.Prepare(rules, msg.From(), msg.To(), vm.ActivePrecompiles(rules), msg.AccessList()) + // Check clause 6 + value, overflow := uint256.FromBig(msg.Value()) + if overflow { + return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From().Hex()) + } + if !value.IsZero() && !st.evm.Context.CanTransfer(st.state, msg.From(), msg.Value()) { + return nil, fmt.Errorf("%w: address %v", ErrInsufficientFundsForTransfer, msg.From().Hex()) + } + var ( - evm = st.evm - // vm errors do not effect consensus and are therefor - // not assigned to err, except for insufficient balance - // error. - vmerr error + ret []byte + vmerr error // vm errors do not effect consensus and are therefore not assigned to err ) - // for debugging purpose - // TODO: clean it after fixing the issue https://github.com/XinFinOrg/XDPoSChain/issues/401 - var contractAction string - nonce := uint64(1) if contractCreation { - ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value) - contractAction = "contract creation" + ret, _, st.gas, vmerr = st.evm.Create(sender, st.data, st.gas, st.value) } else { // Increment the nonce for the next transaction - nonce = st.state.GetNonce(sender.Address()) + 1 - st.state.SetNonce(sender.Address(), nonce) - ret, st.gas, vmerr = evm.Call(sender, st.to().Address(), st.data, st.gas, st.value) - contractAction = "contract call" - } - if vmerr != nil { - log.Debug("VM returned with error", "action", contractAction, "contract address", st.to().Address(), "gas", st.gas, "gasPrice", st.gasPrice, "nonce", nonce, "err", vmerr) - // The only possible consensus-error would be if there wasn't - // sufficient balance to make the transfer happen. The first - // balance transfer may never fail. - if vmerr == vm.ErrInsufficientBalance { - return nil, 0, false, vmerr, nil - } + st.state.SetNonce(sender.Address(), st.state.GetNonce(sender.Address())+1) + ret, st.gas, vmerr = st.evm.Call(sender, st.to().Address(), st.data, st.gas, st.value) } if !eip3529 { // Before EIP-3529: refunds were capped to gasUsed / 2 @@ -384,7 +402,11 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG st.state.AddBalance(st.evm.Context.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip)) } - return ret, st.gasUsed(), vmerr != nil, nil, vmerr + return &ExecutionResult{ + UsedGas: st.gasUsed(), + Err: vmerr, + ReturnData: ret, + }, nil } func (st *StateTransition) refundGas(refundQuotient uint64) { diff --git a/core/token_validator.go b/core/token_validator.go index 628db3c98a98..070a3342ce8e 100644 --- a/core/token_validator.go +++ b/core/token_validator.go @@ -117,11 +117,11 @@ func CallContractWithState(call ethereum.CallMsg, chain consensus.ChainContext, vmenv := vm.NewEVM(evmContext, txContext, statedb, nil, chain.Config(), vm.Config{}) gaspool := new(GasPool).AddGas(1000000) owner := common.Address{} - rval, _, _, err, _ := NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner) + result, err := NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner) if err != nil { return nil, err } - return rval, err + return result.Return(), err } // make sure that balance of token is at slot 0 diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 8914bf94badb..43b7900535b5 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -21,8 +21,8 @@ import ( "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core/types" - "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/params" "github.com/holiman/uint256" ) diff --git a/eth/api_tracer.go b/eth/api_tracer.go index 88d2cf4881b4..18c0fc010945 100644 --- a/eth/api_tracer.go +++ b/eth/api_tracer.go @@ -526,7 +526,7 @@ func (api *PrivateDebugAPI) traceBlock(ctx context.Context, block *types.Block, vmenv := vm.NewEVM(blockCtx, txContext, statedb, XDCxState, api.config, vm.Config{}) owner := common.Address{} - if _, _, _, err, _ := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), owner); err != nil { + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), owner); err != nil { failed = err break } @@ -750,7 +750,7 @@ func (api *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, t statedb.SetTxContext(txctx.TxHash, txctx.TxIndex) owner := common.Address{} - ret, gas, failed, err, _ := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()), owner) + result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()), owner) if err != nil { return nil, fmt.Errorf("tracing failed: %v", err) } @@ -758,9 +758,9 @@ func (api *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, t switch tracer := tracer.(type) { case *vm.StructLogger: return ðapi.ExecutionResult{ - Gas: gas, - Failed: failed, - ReturnValue: fmt.Sprintf("%x", ret), + Gas: result.UsedGas, + Failed: result.Failed(), + ReturnValue: fmt.Sprintf("%x", result.Return()), StructLogs: ethapi.FormatLogs(tracer.StructLogs()), }, nil diff --git a/eth/tracers/testing/calltrace_test.go b/eth/tracers/testing/calltrace_test.go index aaea4f375d41..102e6f7416cc 100644 --- a/eth/tracers/testing/calltrace_test.go +++ b/eth/tracers/testing/calltrace_test.go @@ -120,7 +120,7 @@ func testCallTracer(tracerName string, dirPath string, t *testing.T) { t.Fatalf("failed to prepare transaction for tracing: %v", err) } st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) - if _, _, _, err, _ = st.TransitionDb(common.Address{}); err != nil { + if _, err = st.TransitionDb(common.Address{}); err != nil { t.Fatalf("failed to execute transaction: %v", err) } // Retrieve the trace result and compare against the etalon @@ -231,7 +231,7 @@ func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { evm := vm.NewEVM(context, txContext, statedb, nil, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer}) snap := statedb.Snapshot() st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) - if _, _, _, err, _ = st.TransitionDb(common.Address{}); err != nil { + if _, err = st.TransitionDb(common.Address{}); err != nil { b.Fatalf("failed to execute transaction: %v", err) } if _, err = tracer.GetResult(); err != nil { diff --git a/eth/tracers/tracers_test.go b/eth/tracers/tracers_test.go index 9505387fce35..af01acda6741 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -158,7 +158,7 @@ func TestZeroValueToNotExitCall(t *testing.T) { t.Fatalf("failed to prepare transaction for tracing: %v", err) } st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) - if _, _, _, err, _ = st.TransitionDb(common.Address{}); err != nil { + if _, err = st.TransitionDb(common.Address{}); err != nil { t.Fatalf("failed to execute transaction: %v", err) } // Retrieve the trace result and compare against the etalon @@ -244,7 +244,7 @@ func TestPrestateTracerCreate2(t *testing.T) { t.Fatalf("failed to prepare transaction for tracing: %v", err) } st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) - if _, _, _, err, _ = st.TransitionDb(common.Address{}); err != nil { + if _, err = st.TransitionDb(common.Address{}); err != nil { t.Fatalf("failed to execute transaction: %v", err) } // Retrieve the trace result and compare against the etalon @@ -346,7 +346,7 @@ func BenchmarkTransactionTrace(b *testing.B) { for i := 0; i < b.N; i++ { snap := statedb.Snapshot() st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) - _, _, _, err, _ = st.TransitionDb(common.Address{}) + _, err = st.TransitionDb(common.Address{}) if err != nil { b.Fatal(err) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index ac7ac6739b3e..ad6c7279148f 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1322,18 +1322,18 @@ func (s *PublicBlockChainAPI) getCandidatesFromSmartContract() ([]utils.Masterno return candidatesWithStakeInfo, nil } -func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) ([]byte, uint64, bool, error, error) { +func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) statedb, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if statedb == nil || err != nil { - return nil, 0, false, err, nil + return nil, err } if header == nil { - return nil, 0, false, errors.New("nil header in DoCall"), nil + return nil, errors.New("nil header in DoCall") } if err := overrides.Apply(statedb); err != nil { - return nil, 0, false, err, nil + return nil, err } // Setup context so it may be cancelled the call has completed @@ -1350,32 +1350,32 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash) if err != nil { - return nil, 0, false, err, nil + return nil, err } if block == nil { - return nil, 0, false, fmt.Errorf("nil block in DoCall: number=%d, hash=%s", header.Number.Uint64(), header.Hash().Hex()), nil + return nil, fmt.Errorf("nil block in DoCall: number=%d, hash=%s", header.Number.Uint64(), header.Hash().Hex()) } author, err := b.GetEngine().Author(block.Header()) if err != nil { - return nil, 0, false, err, nil + return nil, err } XDCxState, err := b.XDCxService().GetTradingState(block, author) if err != nil { - return nil, 0, false, err, nil + return nil, err } // TODO: replace header.BaseFee with blockCtx.BaseFee // reference: https://github.com/ethereum/go-ethereum/pull/29051 msg, err := args.ToMessage(b, header.Number, globalGasCap, header.BaseFee) if err != nil { - return nil, 0, false, err, nil + return nil, err } msg.SetBalanceTokenFeeForCall() // Get a new instance of the EVM. evm, vmError, err := b.GetEVM(ctx, msg, statedb, XDCxState, header, &vm.Config{NoBaseFee: true}) if err != nil { - return nil, 0, false, err, nil + return nil, err } // Wait for the context to be done and cancel the evm. Even if the // EVM has finished, cancelling may be done (repeatedly) @@ -1387,19 +1387,19 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash // Execute the message. gp := new(core.GasPool).AddGas(math.MaxUint64) owner := common.Address{} - res, gas, failed, err, vmErr := core.ApplyMessage(evm, msg, gp, owner) + result, err := core.ApplyMessage(evm, msg, gp, owner) if err := vmError(); err != nil { - return nil, 0, false, err, nil + return nil, err } // If the timer caused an abort, return an appropriate error message if evm.Cancelled() { - return nil, 0, false, fmt.Errorf("execution aborted (timeout = %v)", timeout), nil + return nil, fmt.Errorf("execution aborted (timeout = %v)", timeout) } if err != nil { - return res, 0, false, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas()), nil + return result, fmt.Errorf("err: %w (supplied gas %d)", err, msg.Gas()) } - return res, gas, failed, err, vmErr + return result, err } func newRevertError(res []byte) *revertError { @@ -1443,16 +1443,32 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args TransactionArgs, bl if args.To != nil && *args.To == common.MasternodeVotingSMCBinary { timeout = 0 } - result, _, failed, err, vmErr := DoCall(ctx, s.b, args, *blockNrOrHash, overrides, timeout, s.b.RPCGasCap()) + result, err := DoCall(ctx, s.b, args, *blockNrOrHash, overrides, timeout, s.b.RPCGasCap()) if err != nil { return nil, err } // If the result contains a revert reason, try to unpack and return it. - if failed && len(result) > 0 { - return nil, newRevertError(result) + if result.Failed() && len(result.Return()) > 0 { + return nil, newRevertError(result.Return()) } + return result.Return(), nil +} + +type estimateGasError struct { + error string // Concrete error type if it's failed to estimate gas usage + vmerr error // Additional field, it's non-nil if the given transaction is invalid + revert string // Additional field, it's non-empty if the transaction is reverted and reason is provided +} - return (hexutil.Bytes)(result), vmErr +func (e estimateGasError) Error() string { + errMsg := e.error + if e.vmerr != nil { + errMsg += fmt.Sprintf(" (%v)", e.vmerr) + } + if e.revert != "" { + errMsg += fmt.Sprintf(" (%s)", e.revert) + } + return errMsg } func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, gasCap uint64) (hexutil.Uint64, error) { @@ -1496,21 +1512,17 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr cap = hi // Create a helper to check if a gas allowance results in an executable transaction - executable := func(gas uint64) (bool, []byte, error, error) { + executable := func(gas uint64) (bool, *core.ExecutionResult, error) { args.Gas = (*hexutil.Uint64)(&gas) - res, _, failed, err, vmErr := DoCall(ctx, b, args, blockNrOrHash, nil, 0, gasCap) + result, err := DoCall(ctx, b, args, blockNrOrHash, nil, 0, gasCap) if err != nil { if errors.Is(err, vm.ErrOutOfGas) || errors.Is(err, core.ErrIntrinsicGas) { - return false, nil, nil, nil // Special case, raise gas limit + return true, nil, nil // Special case, raise gas limit } - return false, nil, err, nil // Bail out - } - if failed { - return false, res, nil, vmErr + return true, nil, err // Bail out } - - return true, nil, nil, nil + return result.Failed(), result, nil } // If the transaction is a plain value transfer, short circuit estimation and @@ -1519,7 +1531,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr // unused access list items). Ever so slightly wasteful, but safer overall. if args.Data == nil || len(*args.Data) == 0 { if args.To != nil && state.GetCodeSize(*args.To) == 0 { - ok, _, err, _ := executable(params.TxGas) + ok, _, err := executable(params.TxGas) if ok && err == nil { return hexutil.Uint64(params.TxGas), nil } @@ -1529,7 +1541,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr // Execute the binary search and hone in on an executable gas limit for lo+1 < hi { mid := (hi + lo) / 2 - ok, _, err, _ := executable(mid) + 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 @@ -1538,7 +1550,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr return 0, err } - if !ok { + if failed { lo = mid } else { hi = mid @@ -1547,21 +1559,30 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr // Reject the transaction as invalid if it still fails at the highest allowance if hi == cap { - ok, res, err, vmErr := executable(hi) + failed, result, err := executable(hi) if err != nil { return 0, err } - if !ok { - if vmErr != vm.ErrOutOfGas { - if len(res) > 0 { - return 0, newRevertError(res) + if failed { + if result != nil && result.Err != vm.ErrOutOfGas { + var revert string + if len(result.Revert()) > 0 { + ret, err := abi.UnpackRevert(result.Revert()) + if err != nil { + revert = hexutil.Encode(result.Revert()) + } else { + revert = ret + } + } + return 0, estimateGasError{ + error: "always failing transaction", + vmerr: result.Err, + revert: revert, } - return 0, vmErr } - // Otherwise, the specified gas cap is too low - return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) + return 0, estimateGasError{error: fmt.Sprintf("gas required exceeds allowance (%d)", cap)} } } return hexutil.Uint64(hi), nil @@ -2075,12 +2096,12 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH return nil, 0, nil, err } // TODO: determine the value of owner - _, UsedGas, _, err, vmErr := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), owner) + res, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), owner) if err != nil { return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", args.toTransaction().Hash(), err) } if tracer.Equal(prevTracer) { - return accessList, UsedGas, vmErr, nil + return accessList, res.UsedGas, res.Err, nil } prevTracer = tracer } diff --git a/les/odr_test.go b/les/odr_test.go index b4aa8cf21d0f..706853d504b2 100644 --- a/les/odr_test.go +++ b/les/odr_test.go @@ -142,8 +142,8 @@ 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) owner := common.Address{} - ret, _, _, _, _ := core.ApplyMessage(vmenv, msg, gp, owner) - res = append(res, ret...) + result, _ := core.ApplyMessage(vmenv, msg, gp, owner) + res = append(res, result.Return()...) } } else { header := lc.GetHeaderByHash(bhash) @@ -160,9 +160,9 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai vmenv := vm.NewEVM(context, txContext, statedb, nil, config, vm.Config{NoBaseFee: true}) gp := new(core.GasPool).AddGas(math.MaxUint64) owner := common.Address{} - ret, _, _, _, _ := core.ApplyMessage(vmenv, msg, gp, owner) + result, _ := core.ApplyMessage(vmenv, msg, gp, owner) if statedb.Error() == nil { - res = append(res, ret...) + res = append(res, result.Return()...) } } } diff --git a/light/odr_test.go b/light/odr_test.go index 94c1246065ad..79d3241c2048 100644 --- a/light/odr_test.go +++ b/light/odr_test.go @@ -191,8 +191,8 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain vmenv := vm.NewEVM(context, txContext, st, nil, config, vm.Config{NoBaseFee: true}) gp := new(core.GasPool).AddGas(math.MaxUint64) owner := common.Address{} - ret, _, _, _, _ := core.ApplyMessage(vmenv, msg, gp, owner) - res = append(res, ret...) + result, _ := core.ApplyMessage(vmenv, msg, gp, owner) + res = append(res, result.Return()...) if st.Error() != nil { return res, st.Error() } diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 30750b973c73..e95e45155589 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -166,7 +166,7 @@ func (t *StateTest) Run(subtest StateSubtest, vmconfig vm.Config) (*state.StateD gaspool.AddGas(block.GasLimit()) coinbase := &t.json.Env.Coinbase - if _, _, _, err, _ := core.ApplyMessage(evm, msg, gaspool, *coinbase); err != nil { + if _, err := core.ApplyMessage(evm, msg, gaspool, *coinbase); err != nil { statedb.RevertToSnapshot(snapshot) } if logs := rlpHash(statedb.Logs()); logs != common.Hash(post.Logs) {