diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index 254b1f7fb4..e875d51b65 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -19,7 +19,9 @@ package abi import ( "bytes" "encoding/json" + "errors" "fmt" + "github.com/tomochain/tomochain/crypto" "io" ) @@ -144,3 +146,26 @@ func (abi *ABI) MethodById(sigdata []byte) (*Method, error) { } return nil, fmt.Errorf("no method with id: %#x", sigdata[:4]) } + +// revertSelector is a special function selector for revert reason unpacking. +var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] + +// UnpackRevert resolves the abi-encoded revert reason. According to the solidity +// spec https://solidity.readthedocs.io/en/latest/control-structures.html#revert, +// the provided revert reason is abi-encoded as if it were a call to a function +// `Error(string)`. So it's a special tool for it. +func UnpackRevert(data []byte) (string, error) { + if len(data) < 4 { + return "", errors.New("invalid data for unpacking") + } + if !bytes.Equal(data[:4], revertSelector) { + return "", errors.New("invalid data for unpacking") + } + var reason string + // typ, _ := NewType("string", "", nil) + typ, _ := NewType("string") + if err := (Arguments{{Type: typ}}).Unpack(&reason, data[4:]); err != nil { + return "", err + } + return reason, nil +} diff --git a/accounts/abi/abi_test.go b/accounts/abi/abi_test.go index 5a128bfe54..9092b6cd81 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" @@ -619,16 +620,19 @@ func TestBareEvents(t *testing.T) { } // TestUnpackEvent is based on this contract: -// contract T { -// event received(address sender, uint amount, bytes memo); -// event receivedAddr(address sender); -// function receive(bytes memo) external payable { -// received(msg.sender, msg.value, memo); -// receivedAddr(msg.sender); -// } -// } +// +// contract T { +// event received(address sender, uint amount, bytes memo); +// event receivedAddr(address sender); +// function receive(bytes memo) external payable { +// received(msg.sender, msg.value, memo); +// receivedAddr(msg.sender); +// } +// } +// // When receive("X") is called with sender 0x00... and value 1, it produces this tx receipt: -// receipt{status=1 cgas=23949 bloom=00000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000040200000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 logs=[log: b6818c8064f645cd82d99b59a1a267d6d61117ef [75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed] 000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158 9ae378b6d4409eada347a5dc0c180f186cb62dc68fcc0f043425eb917335aa28 0 95d429d309bb9d753954195fe2d69bd140b4ae731b9b5b605c34323de162cf00 0]} +// +// receipt{status=1 cgas=23949 bloom=00000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000040200000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 logs=[log: b6818c8064f645cd82d99b59a1a267d6d61117ef [75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed] 000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158 9ae378b6d4409eada347a5dc0c180f186cb62dc68fcc0f043425eb917335aa28 0 95d429d309bb9d753954195fe2d69bd140b4ae731b9b5b605c34323de162cf00 0]} func TestUnpackEvent(t *testing.T) { const abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"receive","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"receivedAddr","type":"event"}]` abi, err := JSON(strings.NewReader(abiJSON)) @@ -713,3 +717,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 7411f492a8..436563cb54 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -20,8 +20,11 @@ import ( "context" "errors" "fmt" + "github.com/tomochain/tomochain/accounts/abi" + "github.com/tomochain/tomochain/common/hexutil" "github.com/tomochain/tomochain/consensus" "github.com/tomochain/tomochain/core/rawdb" + "github.com/tomochain/tomochain/log" "math/big" "sync" "time" @@ -82,6 +85,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() { @@ -186,6 +195,36 @@ func (b *SimulatedBackend) PendingCodeAt(ctx context.Context, contract common.Ad return b.pendingState.GetCode(contract), nil } +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) + } + return &revertError{ + error: err, + reason: hexutil.Encode(result.Revert()), + } +} + +// revertError is an API error that encompassas an EVM revertal with JSON error +// code and a binary data blob. +type revertError struct { + error + reason string // revert reason hex encoded +} + +// ErrorCode returns the JSON error code for a revertal. +// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal +func (e *revertError) ErrorCode() int { + return 3 +} + +// ErrorData returns the hex encoded revert reason. +func (e *revertError) ErrorData() interface{} { + return e.reason +} + // CallContract executes a contract call. func (b *SimulatedBackend) CallContract(ctx context.Context, call tomochain.CallMsg, blockNumber *big.Int) ([]byte, error) { b.mu.Lock() @@ -198,11 +237,15 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call tomochain.Call 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 the result contains a revert reason, try to unpack and return it. + if len(res.Revert()) > 0 { + return nil, newRevertError(res) + } + return res.Return(), res.Err } -//FIXME: please use copyState for this function +// FIXME: please use copyState for this function // CallContractWithState executes a contract call at the given state. func (b *SimulatedBackend) CallContractWithState(call tomochain.CallMsg, chain consensus.ChainContext, statedb *state.StateDB) ([]byte, error) { // Ensure message is initialized properly. @@ -228,11 +271,11 @@ func (b *SimulatedBackend) CallContractWithState(call tomochain.CallMsg, chain c vmenv := vm.NewEVM(evmContext, statedb, nil, chain.Config(), vm.Config{}) gaspool := new(core.GasPool).AddGas(1000000) owner := common.Address{} - rval, _, _, err := core.NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner) + result, err := core.NewStateTransition(vmenv, msg, gaspool).TransitionDb(owner) if err != nil { return nil, err } - return rval, err + return result.Return(), err } // PendingCallContract executes a contract call on the pending state. @@ -241,8 +284,15 @@ func (b *SimulatedBackend) PendingCallContract(ctx context.Context, call tomocha defer b.mu.Unlock() defer b.pendingState.RevertToSnapshot(b.pendingState.Snapshot()) - rval, _, _, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) - return rval, err + result, err := b.callContract(ctx, call, b.pendingBlock, b.pendingState) + if err != nil { + return nil, err + } + // If the result contains a revert reason, try to unpack and return it. + if len(result.Revert()) > 0 { + return nil, newRevertError(result) + } + return result.Return(), result.Err } // PendingNonceAt implements PendingStateReader.PendingNonceAt, retrieving @@ -277,26 +327,61 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call tomochain.CallM } else { hi = b.pendingBlock.GasLimit() } + + // Recap the highest gas allowance with account's balance. + if call.GasPrice != nil && call.GasPrice.BitLen() != 0 { + balance := b.pendingState.GetBalance(call.From) // from can't be nil + available := new(big.Int).Set(balance) + if call.Value != nil { + if call.Value.Cmp(available) >= 0 { + return 0, errors.New("insufficient funds for transfer") + } + available.Sub(available, call.Value) + } + allowance := new(big.Int).Div(available, call.GasPrice) + + // If the allowance is larger than maximum uint64, skip checking + if allowance.IsUint64() && hi > allowance.Uint64() { + transfer := call.Value + if transfer == nil { + transfer = new(big.Int) + } + log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance, + "sent", transfer, "gasprice", call.GasPrice, "fundable", allowance) + hi = allowance.Uint64() + } + } + 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) - fmt.Println("EstimateGas",err,failed) + 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 @@ -304,8 +389,19 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call tomochain.CallM } // 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 { + if len(result.Revert()) > 0 { + return 0, newRevertError(result) + } + return 0, result.Err + } + // Otherwise, the specified gas cap is too low + return 0, fmt.Errorf("gas required exceeds allowance (%d)", cap) } } return hi, nil @@ -313,7 +409,7 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call tomochain.CallM // 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 tomochain.CallMsg, block *types.Block, statedb *state.StateDB) ([]byte, uint64, bool, error) { +func (b *SimulatedBackend) callContract(ctx context.Context, call tomochain.CallMsg, block *types.Block, statedb *state.StateDB) (*core.ExecutionResult, error) { // Ensure message is initialized properly. if call.GasPrice == nil { call.GasPrice = big.NewInt(1) diff --git a/accounts/abi/bind/backends/simulated_test.go b/accounts/abi/bind/backends/simulated_test.go new file mode 100644 index 0000000000..6b403dabb6 --- /dev/null +++ b/accounts/abi/bind/backends/simulated_test.go @@ -0,0 +1,80 @@ +package backends + +import ( + "context" + "errors" + ethereum "github.com/tomochain/tomochain" + "github.com/tomochain/tomochain/common" + "github.com/tomochain/tomochain/core" + "github.com/tomochain/tomochain/crypto" + "github.com/tomochain/tomochain/params" + "math/big" + "testing" +) + +func TestSimulatedBackend_EstimateGasWithPrice(t *testing.T) { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + + sim := NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(params.Ether*2 + 2e17)}}) + defer sim.Close() + + receipant := common.HexToAddress("deadbeef") + var cases = []struct { + name string + message ethereum.CallMsg + expect uint64 + expectError error + }{ + {"EstimateWithoutPrice", ethereum.CallMsg{ + From: addr, + To: &receipant, + Gas: 0, + GasPrice: big.NewInt(0), + Value: big.NewInt(1000), + Data: nil, + }, 21000, nil}, + + {"EstimateWithPrice", ethereum.CallMsg{ + From: addr, + To: &receipant, + Gas: 0, + GasPrice: big.NewInt(1000), + Value: big.NewInt(1000), + Data: nil, + }, 21000, nil}, + + {"EstimateWithVeryHighPrice", ethereum.CallMsg{ + From: addr, + To: &receipant, + Gas: 0, + GasPrice: big.NewInt(1e14), // gascost = 2.1ether + Value: big.NewInt(1e17), // the remaining balance for fee is 2.1ether + Data: nil, + }, 21000, nil}, + + {"EstimateWithSuperhighPrice", ethereum.CallMsg{ + From: addr, + To: &receipant, + Gas: 0, + GasPrice: big.NewInt(2e14), // gascost = 4.2ether + Value: big.NewInt(1000), + Data: nil, + }, 21000, errors.New("gas required exceeds allowance (10999)")}, // 10999=(2.2ether-1000wei)/(2e14) + } + 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/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index caf1640496..cb745d597c 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -128,7 +128,7 @@ func (c *BoundContract) Call(opts *CallOpts, result interface{}, method string, return err } var ( - msg = tomochain.CallMsg{From: opts.From, To: &c.address, Data: input, GasPrice: common.MinGasPrice, Gas: uint64(4200000)} + msg = tomochain.CallMsg{From: opts.From, To: &c.address, Data: input} ctx = ensureContext(opts.Context) code []byte output []byte @@ -270,7 +270,7 @@ func (c *BoundContract) FilterLogs(opts *FilterOpts, name string, query ...[]int config.ToBlock = new(big.Int).SetUint64(*opts.End) } /* TODO(karalabe): Replace the rest of the method below with this when supported - sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) + sub, err := c.filterer.SubscribeFilterLogs(ensureContext(opts.Context), config, logs) */ buff, err := c.filterer.FilterLogs(ensureContext(opts.Context), config) if err != nil { diff --git a/core/error.go b/core/error.go index 63be6ab83d..d47530300b 100644 --- a/core/error.go +++ b/core/error.go @@ -38,4 +38,11 @@ var ( ErrNotFoundM1 = errors.New("list M1 not found ") ErrStopPreparingBlock = errors.New("stop calculating a block not verified by M2") + + // ErrGasUintOverflow is returned when calculating gas usage. + ErrGasUintOverflow = errors.New("gas uint64 overflow") + + // ErrInsufficientFundsForTransfer is returned if the transaction sender doesn't + // have enough funds for transfer(topmost call only). + ErrInsufficientFundsForTransfer = errors.New("insufficient funds for transfer") ) diff --git a/core/state_processor.go b/core/state_processor.go index 50496e4b00..cf567d1b3d 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -415,7 +415,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(vmenv, msg, gp, coinbaseOwner) + result, err := ApplyMessage(vmenv, msg, gp, coinbaseOwner) if err != nil { return nil, 0, err, false @@ -427,13 +427,13 @@ func ApplyTransaction(config *params.ChainConfig, tokensFee map[common.Address]* } else { root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes() } - *usedGas += gas + *usedGas += result.UsedGas // 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 wether the root touch-delete accounts. - receipt := types.NewReceipt(root, failed, *usedGas) + receipt := types.NewReceipt(root, result.Failed(), *usedGas) 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 { receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce()) @@ -441,10 +441,10 @@ func ApplyTransaction(config *params.ChainConfig, tokensFee map[common.Address]* // Set the receipt logs and create a bloom for filtering receipt.Logs = statedb.GetLogs(tx.Hash()) receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) - if balanceFee != nil && failed { + if balanceFee != nil && result.Failed() { state.PayFeeWithTRC21TxFail(statedb, msg.From(), *tx.To()) } - return receipt, gas, err, balanceFee != nil + return receipt, result.UsedGas, err, balanceFee != nil } func ApplySignTransaction(config *params.ChainConfig, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64) (*types.Receipt, uint64, error, bool) { diff --git a/core/state_transition.go b/core/state_transition.go index 0c844c3cf4..398da290ac 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -17,20 +17,14 @@ package core import ( - "errors" "math" "math/big" "github.com/tomochain/tomochain/common" "github.com/tomochain/tomochain/core/vm" - "github.com/tomochain/tomochain/log" "github.com/tomochain/tomochain/params" ) -var ( - errInsufficientBalanceForGas = errors.New("insufficient balance to pay for gas") -) - /* The State Transitioning Model @@ -78,6 +72,41 @@ type Message interface { BalanceTokenFee() *big.Int } +// 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) +} + +// Unwrap returns the internal evm error which allows us for further +// analysis outside. +func (result *ExecutionResult) Unwrap() error { + return result.Err +} + +// 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, homestead bool) (uint64, error) { // Set the starting gas for the raw transaction @@ -98,13 +127,13 @@ func IntrinsicGas(data []byte, contractCreation, homestead bool) (uint64, error) } // Make sure we don't exceed uint64 for all data combinations if (math.MaxUint64-gas)/params.TxDataNonZeroGas < nz { - return 0, vm.ErrOutOfGas + return 0, ErrGasUintOverflow } gas += nz * params.TxDataNonZeroGas z := uint64(len(data)) - nz if (math.MaxUint64-gas)/params.TxDataZeroGas < z { - return 0, vm.ErrOutOfGas + return 0, ErrGasUintOverflow } gas += z * params.TxDataZeroGas } @@ -131,7 +160,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) { +func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool, owner common.Address) (*ExecutionResult, error) { return NewStateTransition(evm, msg, gp).TransitionDb(owner) } @@ -163,15 +192,6 @@ func (st *StateTransition) to() vm.AccountRef { return reference } -func (st *StateTransition) useGas(amount uint64) error { - if st.gas < amount { - return vm.ErrOutOfGas - } - st.gas -= amount - - return nil -} - func (st *StateTransition) buyGas() error { var ( state = st.state @@ -179,12 +199,13 @@ func (st *StateTransition) buyGas() error { from = st.from() ) mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice) + if balanceTokenFee == nil { if state.GetBalance(from.Address()).Cmp(mgval) < 0 { - return errInsufficientBalanceForGas + return ErrInsufficientFunds } } else if balanceTokenFee.Cmp(mgval) < 0 { - return errInsufficientBalanceForGas + return ErrInsufficientFunds } if err := st.gp.SubGas(st.msg.Gas()); err != nil { return err @@ -215,11 +236,32 @@ func (st *StateTransition) preCheck() error { } // TransitionDb will transition the state by applying the current message and -// returning the result including the the used gas. It returns an error if it -// failed. An error indicates a consensus issue. -func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedGas uint64, failed bool, err error) { - if err = st.preCheck(); err != nil { - return +// returning the evm execution result with following fields. +// +// - used gas: +// total gas used (including gas being refunded) +// - returndata: +// the returned data from evm +// - concrete execution error: +// various **EVM** error which aborts the execution, +// e.g. ErrOutOfGas, ErrExecutionReverted +// +// However if any consensus issue encountered, return the error directly with +// nil evm execution result. +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 + // + // 1. the nonce of the message caller is correct + // 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) + // 3. the amount of gas required is available in the block + // 4. the purchased gas is enough to cover intrinsic usage + // 5. there is no overflow when calculating intrinsic gas + // 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, err } msg := st.msg sender := st.from() // err checked in preCheck @@ -227,44 +269,35 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber) contractCreation := msg.To() == nil - // Pay intrinsic gas + // Check clauses 4-5, subtract intrinsic gas if everything is correct gas, err := IntrinsicGas(st.data, contractCreation, homestead) if err != nil { - return nil, 0, false, err + return nil, err + } + if st.gas < gas { + return nil, ErrIntrinsicGas } - if err = st.useGas(gas); err != nil { - return nil, 0, false, err + st.gas -= gas + + // Check clause 6 + if msg.Value().Sign() > 0 && !st.evm.CanTransfer(st.state, msg.From(), msg.Value()) { + return nil, ErrInsufficientFundsForTransfer } 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/tomochain/tomochain/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 - } + ret, st.gas, vmerr = st.evm.Call(sender, st.to().Address(), st.data, st.gas, st.value) } st.refundGas() @@ -276,7 +309,11 @@ func (st *StateTransition) TransitionDb(owner common.Address) (ret []byte, usedG st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice)) } - return ret, st.gasUsed(), vmerr != nil, err + return &ExecutionResult{ + UsedGas: st.gasUsed(), + Err: vmerr, + ReturnData: ret, + }, err } func (st *StateTransition) refundGas() { diff --git a/core/token_validator.go b/core/token_validator.go index 485ff05c59..26bab72c2e 100644 --- a/core/token_validator.go +++ b/core/token_validator.go @@ -85,7 +85,7 @@ func RunContract(chain consensus.ChainContext, statedb *state.StateDB, contractA return unpackResult, nil } -//FIXME: please use copyState for this function +// FIXME: please use copyState for this function // CallContractWithState executes a contract call at the given state. func CallContractWithState(call ethereum.CallMsg, chain consensus.ChainContext, statedb *state.StateDB) ([]byte, error) { // Ensure message is initialized properly. @@ -111,11 +111,11 @@ func CallContractWithState(call ethereum.CallMsg, chain consensus.ChainContext, vmenv := vm.NewEVM(evmContext, 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/types/transaction.go b/core/types/transaction.go index 41d3d27905..d474ee12c5 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -689,9 +689,6 @@ type Message struct { } func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte, checkNonce bool, balanceTokenFee *big.Int) Message { - if balanceTokenFee != nil { - gasPrice = common.TRC21GasPrice - } return Message{ from: from, to: to, diff --git a/eth/api_backend.go b/eth/api_backend.go index 7fd7aac3b1..9efdd6a4dd 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -34,7 +34,6 @@ import ( "github.com/tomochain/tomochain/accounts" "github.com/tomochain/tomochain/common" - "github.com/tomochain/tomochain/common/math" "github.com/tomochain/tomochain/consensus" "github.com/tomochain/tomochain/contracts" "github.com/tomochain/tomochain/core" @@ -138,7 +137,6 @@ func (b *EthApiBackend) GetTd(blockHash common.Hash) *big.Int { } func (b *EthApiBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, tomoxState *tradingstate.TradingStateDB, header *types.Header, vmCfg vm.Config) (*vm.EVM, func() error, error) { - state.SetBalance(msg.From(), math.MaxBig256) vmError := func() error { return nil } context := core.NewEVMContext(msg, header, b.eth.BlockChain(), nil) diff --git a/eth/api_tracer.go b/eth/api_tracer.go index 289595cdc0..7d1c0a87cb 100644 --- a/eth/api_tracer.go +++ b/eth/api_tracer.go @@ -478,7 +478,7 @@ func (api *PrivateDebugAPI) traceBlock(ctx context.Context, block *types.Block, vmenv := vm.NewEVM(vmctx, statedb, tomoxState, 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 } @@ -635,7 +635,7 @@ func (api *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, v vmenv := vm.NewEVM(vmctx, statedb, nil, api.config, vm.Config{Debug: true, Tracer: tracer}) 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) } @@ -643,9 +643,9 @@ func (api *PrivateDebugAPI) traceTx(ctx context.Context, message core.Message, v 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/tracers_test.go b/eth/tracers/tracers_test.go index 3a6d2e5eea..216f265deb 100644 --- a/eth/tracers/tracers_test.go +++ b/eth/tracers/tracers_test.go @@ -135,13 +135,13 @@ func TestPrestateTracerCreate2(t *testing.T) { t.Fatalf("err %v", err) } /** - This comes from one of the test-vectors on the Skinny Create2 - EIP + This comes from one of the test-vectors on the Skinny Create2 - EIP - address 0x00000000000000000000000000000000deadbeef - salt 0x00000000000000000000000000000000000000000000000000000000cafebabe - init_code 0xdeadbeef - gas (assuming no mem expansion): 32006 - result: 0x60f3f640a8508fC6a86d45DF051962668E1e8AC7 + address 0x00000000000000000000000000000000deadbeef + salt 0x00000000000000000000000000000000000000000000000000000000cafebabe + init_code 0xdeadbeef + gas (assuming no mem expansion): 32006 + result: 0x60f3f640a8508fC6a86d45DF051962668E1e8AC7 */ origin, _ := signer.Sender(tx) context := vm.Context{ @@ -184,7 +184,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 @@ -259,7 +259,7 @@ func TestCallTracer(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 diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 905d7413d6..7ea9bc0174 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "github.com/tomochain/tomochain/accounts/abi" "github.com/tomochain/tomochain/tomoxlending/lendingstate" "math/big" "sort" @@ -53,7 +54,6 @@ import ( ) const ( - defaultGasPrice = 50 * params.Shannon // statuses of candidates statusMasternode = "MASTERNODE" statusSlashed = "SLASHED" @@ -1046,43 +1046,105 @@ func (s *PublicBlockChainAPI) getCandidatesFromSmartContract() ([]posv.Masternod // CallArgs represents the arguments for a call. type CallArgs struct { - From common.Address `json:"from"` + From *common.Address `json:"from"` To *common.Address `json:"to"` - Gas hexutil.Uint64 `json:"gas"` - GasPrice hexutil.Big `json:"gasPrice"` - Value hexutil.Big `json:"value"` - Data hexutil.Bytes `json:"data"` + Gas *hexutil.Uint64 `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"data"` +} + +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) + } + return &revertError{ + error: err, + reason: hexutil.Encode(result.Revert()), + } +} + +// revertError is an API error that encompassas an EVM revertal with JSON error +// code and a binary data blob. +type revertError struct { + error + reason string // revert reason hex encoded +} + +// ErrorCode returns the JSON error code for a revertal. +// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal +func (e *revertError) ErrorCode() int { + return 3 +} + +// ErrorData returns the hex encoded revert reason. +func (e *revertError) ErrorData() interface{} { + return e.reason } -func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration) ([]byte, uint64, bool, error) { +// Call executes the given transaction on the state for the given block number. +// It doesn't make and changes in the state/blockchain and is useful to execute and retrieve values. +func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { + result, err := DoCall(ctx, s.b, args, blockNr, vm.Config{}, 5*time.Second) + if err != nil { + return nil, err + } + // If the result contains a revert reason, try to unpack and return it. + if len(result.Revert()) > 0 { + return nil, newRevertError(result) + } + return result.Return(), result.Err +} + +func DoCall(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, timeout time.Duration) (*core.ExecutionResult, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) - statedb, header, err := s.b.StateAndHeaderByNumber(ctx, blockNr) - if statedb == nil || err != nil { - return nil, 0, false, err + state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) + if state == nil || err != nil { + return nil, err } + + return doCall(ctx, b, args, state, header, vmCfg, timeout) +} + +func doCall(ctx context.Context, b Backend, args CallArgs, st *state.StateDB, header *types.Header, vmCfg vm.Config, timeout time.Duration) (*core.ExecutionResult, error) { // Set sender address or use a default if none specified - addr := args.From - if addr == (common.Address{}) { - if wallets := s.b.AccountManager().Wallets(); len(wallets) > 0 { - if accounts := wallets[0].Accounts(); len(accounts) > 0 { - addr = accounts[0].Address - } - } + var addr common.Address + if args.From != nil { + addr = *args.From } // Set default gas & gas price if none were set - gas, gasPrice := uint64(args.Gas), args.GasPrice.ToInt() - if gas == 0 { - gas = math.MaxUint64 / 2 + gas := uint64(math.MaxUint64 / 2) + if args.Gas != nil { + gas = uint64(*args.Gas) } - if gasPrice.Sign() == 0 { - gasPrice = new(big.Int).SetUint64(defaultGasPrice) + gasPrice := new(big.Int) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + + var data []byte + if args.Data != nil { + data = []byte(*args.Data) + } + + var balanceTokenFee *big.Int + feeCapacity := state.GetTRC21FeeCapacityFromState(st) + if args.To != nil { + if value, ok := feeCapacity[*args.To]; ok { + balanceTokenFee = new(big.Int).Set(value) + } } - balanceTokenFee := big.NewInt(0).SetUint64(gas) - balanceTokenFee = balanceTokenFee.Mul(balanceTokenFee, gasPrice) // Create new call message - msg := types.NewMessage(addr, args.To, 0, args.Value.ToInt(), gas, gasPrice, args.Data, false, balanceTokenFee) + msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, data, false, balanceTokenFee) // Setup context so it may be cancelled the call has completed // or, in case of unmetered gas, setup a context with a timeout. @@ -1096,22 +1158,23 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr // this makes sure resources are cleaned up. defer cancel() - block, err := s.b.BlockByNumber(ctx, blockNr) + block, err := b.BlockByNumber(ctx, rpc.BlockNumber(header.Number.Int64())) if err != nil { - return nil, 0, false, err + return nil, err } - author, err := s.b.GetEngine().Author(block.Header()) + author, err := b.GetEngine().Author(block.Header()) if err != nil { - return nil, 0, false, err + return nil, err } - tomoxState, err := s.b.TomoxService().GetTradingState(block, author) + tomoxState, err := b.TomoxService().GetTradingState(block, author) if err != nil { - return nil, 0, false, err + return nil, err } + // Get a new instance of the EVM. - evm, vmError, err := s.b.GetEVM(ctx, msg, statedb, tomoxState, header, vmCfg) + evm, vmError, err := b.GetEVM(ctx, msg, st, tomoxState, header, vmCfg) if err != nil { - return nil, 0, false, err + 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) @@ -1124,73 +1187,145 @@ func (s *PublicBlockChainAPI) doCall(ctx context.Context, args CallArgs, blockNr // and apply the message. gp := new(core.GasPool).AddGas(math.MaxUint64) owner := common.Address{} - res, gas, failed, err := core.ApplyMessage(evm, msg, gp, owner) + result, err := core.ApplyMessage(evm, msg, gp, owner) if err := vmError(); err != nil { - return nil, 0, false, err + return nil, err } - return res, gas, failed, err + return result, err } -// Call executes the given transaction on the state for the given block number. -// It doesn't make and changes in the state/blockchain and is useful to execute and retrieve values. -func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Bytes, error) { - result, _, _, err := s.doCall(ctx, args, blockNr, vm.Config{}, 5*time.Second) - return (hexutil.Bytes)(result), err +// executeEstimate is a helper that executes the transaction under a given gas limit and returns +// true if the transaction fails for a reason that might be related to not enough gas. A non-nil +// error means execution failed due to reasons unrelated to the gas limit. +func executeEstimate(ctx context.Context, b Backend, args CallArgs, state *state.StateDB, header *types.Header, gasLimit uint64) (bool, *core.ExecutionResult, error) { + args.Gas = (*hexutil.Uint64)(&gasLimit) + result, err := doCall(ctx, b, args, state, header, vm.Config{}, 0) + if err != nil { + if errors.Is(err, core.ErrIntrinsicGas) { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out + } + return result.Failed(), result, nil } -// EstimateGas returns an estimate of the amount of gas needed to execute the -// given transaction against the current pending block. -func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs, blockNumber *rpc.BlockNumber) (hexutil.Uint64, error) { - bNrOrHash := rpc.LatestBlockNumber - if blockNumber != nil { - bNrOrHash = *blockNumber - } - // Binary search the gas requirement, as it may be higher than the amount used +// DoEstimateGas returns the lowest possible gas limit that allows the transaction to run +// successfully at block `blockNrOrHash`. It returns error if the transaction would revert, or if +// there are unexpected failures. The gas limit is capped by both `args.Gas` (if non-nil & +// non-zero) and `gasCap` (if non-zero). +func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNr rpc.BlockNumber) (hexutil.Uint64, error) { + // Binary search the gas limit, as it may need to be higher than the amount used var ( - lo uint64 = params.TxGas - 1 - hi uint64 - cap uint64 + lo uint64 // lowest-known gas limit where tx execution fails + hi uint64 // lowest-known gas limit where tx execution succeeds ) - if uint64(args.Gas) >= params.TxGas { - hi = uint64(args.Gas) + + // Use zero address if sender unspecified. + if args.From == nil { + args.From = new(common.Address) + } + + if args.Gas != nil && uint64(*args.Gas) >= params.TxGas { + hi = uint64(*args.Gas) } else { - // Retrieve the current pending block to act as the gas ceiling - block, err := s.b.BlockByNumber(ctx, bNrOrHash) + // Retrieve the block to act as the gas ceiling + block, err := b.BlockByNumber(ctx, blockNr) if err != nil { return 0, err } + if block == nil { + return 0, errors.New("block not found") + } hi = block.GasLimit() } - cap = hi - // Create a helper to check if a gas allowance results in an executable transaction - executable := func(gas uint64) bool { - args.Gas = hexutil.Uint64(gas) + state, header, err := b.StateAndHeaderByNumber(ctx, blockNr) + if state == nil || err != nil { + return 0, err + } + + // Recap the highest gas limit with account's available balance. + if args.GasPrice != nil && args.GasPrice.ToInt().BitLen() != 0 { + balance := state.GetBalance(*args.From) // from can't be nil + available := new(big.Int).Set(balance) + if args.Value != nil { + if args.Value.ToInt().Cmp(available) >= 0 { + return 0, core.ErrInsufficientFundsForTransfer + } + available.Sub(available, args.Value.ToInt()) + } + allowance := new(big.Int).Div(available, args.GasPrice.ToInt()) + + // If the allowance is larger than maximum uint64, skip checking + if allowance.IsUint64() && hi > allowance.Uint64() { + transfer := args.Value + if transfer == nil { + transfer = new(hexutil.Big) + } + log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance, + "sent", transfer.ToInt(), "gasprice", args.GasPrice.ToInt(), "fundable", allowance) + hi = allowance.Uint64() + } + } + + // We first execute the transaction at the highest allowable gas limit, since if this fails we + // can return error immediately. + failed, result, err := executeEstimate(ctx, b, args, state.Copy(), header, hi) - _, _, failed, err := s.doCall(ctx, args, rpc.LatestBlockNumber, vm.Config{}, 0) - if err != nil || failed { - return false + if err != nil { + return 0, err + } + if failed { + if result != nil && result.Err != vm.ErrOutOfGas { + if len(result.Revert()) > 0 { + return 0, newRevertError(result) + } + return 0, result.Err } - return true + return 0, fmt.Errorf("gas required exceeds allowance (%d)", hi) } - // Execute the binary search and hone in on an executable gas limit + // For almost any transaction, the gas consumed by the unconstrained execution above + // lower-bounds the gas limit required for it to succeed. One exception is those txs that + // explicitly check gas remaining in order to successfully execute within a given limit, but we + // probably don't want to return a lowest possible gas limit for these cases anyway. + lo = result.UsedGas - 1 + + // Binary search for the smallest gas limit that allows the tx to execute successfully. for lo+1 < hi { mid := (hi + lo) / 2 - if !executable(mid) { + if mid > lo*2 { + // Most txs don't need much higher gas limit than their gas used, and most txs don't + // require near the full block limit of gas, so the selection of where to bisect the + // range here is skewed to favor the low side. + mid = lo * 2 + } + failed, _, err = executeEstimate(ctx, b, args, state.Copy(), header, mid) + if err != nil { + // This should not happen under normal conditions since if we make it this far the + // transaction had run without error at least once before. + log.Error("execution error in estimate gas", "err", err) + return 0, err + } + if failed { lo = mid } else { hi = mid } } - // Reject the transaction as invalid if it still fails at the highest allowance - if hi == cap { - if !executable(hi) { - return 0, fmt.Errorf("gas required exceeds allowance or always failing transaction") - } - } + return hexutil.Uint64(hi), nil } +// NewEstimateGas returns an estimate of the amount of gas needed to execute the +// given transaction against the current pending block. +func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs, blockNr *rpc.BlockNumber) (hexutil.Uint64, error) { + bNr := rpc.BlockNumber(rpc.LatestBlockNumber) + if blockNr != nil { + bNr = *blockNr + } + return DoEstimateGas(ctx, s.b, args, bNr) +} + // ExecutionResult groups all structured logs emitted by the EVM // while replaying a transaction in debug mode as well as transaction // execution status, the amount of gas used and the return value diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index b90f4e9d6e..204c9e5ac5 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -1,37 +1,39 @@ package ethapi import ( - "context" - "crypto/ecdsa" - "encoding/json" - "errors" - "fmt" - "github.com/stretchr/testify/require" - "github.com/tomochain/tomochain/accounts" - "github.com/tomochain/tomochain/common" - "github.com/tomochain/tomochain/common/hexutil" - "github.com/tomochain/tomochain/consensus" - "github.com/tomochain/tomochain/consensus/ethash" - "github.com/tomochain/tomochain/core" - "github.com/tomochain/tomochain/core/state" - "github.com/tomochain/tomochain/core/types" - "github.com/tomochain/tomochain/core/vm" - "github.com/tomochain/tomochain/crypto" - "github.com/tomochain/tomochain/eth/downloader" - "github.com/tomochain/tomochain/ethclient" - "github.com/tomochain/tomochain/ethdb" - "github.com/tomochain/tomochain/event" - "github.com/tomochain/tomochain/params" - "github.com/tomochain/tomochain/rpc" - "github.com/tomochain/tomochain/tomox" - "github.com/tomochain/tomochain/tomox/tradingstate" - "github.com/tomochain/tomochain/tomoxlending" - "math/big" - "os" - "path/filepath" - "slices" - "testing" - "time" + "context" + "crypto/ecdsa" + "encoding/json" + "errors" + "fmt" + "github.com/stretchr/testify/require" + "github.com/tomochain/tomochain/accounts" + "github.com/tomochain/tomochain/accounts/abi/bind" + "github.com/tomochain/tomochain/common" + "github.com/tomochain/tomochain/common/hexutil" + "github.com/tomochain/tomochain/consensus" + "github.com/tomochain/tomochain/consensus/ethash" + "github.com/tomochain/tomochain/contracts/trc21issuer" + "github.com/tomochain/tomochain/core" + "github.com/tomochain/tomochain/core/state" + "github.com/tomochain/tomochain/core/types" + "github.com/tomochain/tomochain/core/vm" + "github.com/tomochain/tomochain/crypto" + "github.com/tomochain/tomochain/eth/downloader" + "github.com/tomochain/tomochain/ethclient" + "github.com/tomochain/tomochain/ethdb" + "github.com/tomochain/tomochain/event" + "github.com/tomochain/tomochain/params" + "github.com/tomochain/tomochain/rpc" + "github.com/tomochain/tomochain/tomox" + "github.com/tomochain/tomochain/tomox/tradingstate" + "github.com/tomochain/tomochain/tomoxlending" + "math/big" + "os" + "path/filepath" + "slices" + "testing" + "time" ) type testBackend struct { @@ -313,8 +315,9 @@ func TestEstimateGas(t *testing.T) { accounts[1].addr: {Balance: big.NewInt(params.Ether)}, }, } - genBlocks = 10 - signer = types.HomesteadSigner{} + genBlocks = 10 + signer = types.HomesteadSigner{} + randomAccounts = newAccounts(2) ) api := NewPublicBlockChainAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { // Transfer from account[0] to account[1] @@ -337,13 +340,24 @@ func TestEstimateGas(t *testing.T) { { blockNumber: rpc.LatestBlockNumber, call: CallArgs{ - From: accounts[0].addr, + From: &accounts[0].addr, To: &accounts[1].addr, - Value: (hexutil.Big)(*big.NewInt(1000)), + Value: (*hexutil.Big)(big.NewInt(1000)), }, expectErr: nil, want: 21000, }, + // simple transfer with insufficient funds on latest block + { + blockNumber: rpc.LatestBlockNumber, + call: CallArgs{ + From: &randomAccounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: core.ErrInsufficientFundsForTransfer, + want: 21000, + }, // empty create { blockNumber: rpc.LatestBlockNumber, @@ -351,6 +365,15 @@ func TestEstimateGas(t *testing.T) { expectErr: nil, want: 53000, }, + { + blockNumber: rpc.LatestBlockNumber, + call: CallArgs{ + From: &randomAccounts[0].addr, + To: &randomAccounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + expectErr: core.ErrInsufficientFundsForTransfer, + }, } for i, tc := range testSuite { result, err := api.EstimateGas(context.Background(), tc.call, &tc.blockNumber) @@ -374,6 +397,113 @@ func TestEstimateGas(t *testing.T) { } } +func TestTRC21(t *testing.T) { + // Initialize test accounts + testPriKey, _ := crypto.HexToECDSA("0d782c534042ab93092d1baaf188e041ae429ca27d28d1a0d2ded2d3dd04c717") + testAddr := crypto.PubkeyToAddress(testPriKey.PublicKey) + fmt.Println("Public key: ", testAddr.String()) // 0x5C845F19EB923eEE213b620c12cc6D1d4E6E3506 + + client, err := ethclient.Dial("http://127.0.0.1:8547") + if err != nil { + t.Fatal("Can't connect to RPC server: %", err) + } + + nonce, _ := client.NonceAt(context.Background(), testAddr, nil) + fmt.Println("Nonce", nonce) + + // Setup transactOpts + auth := bind.NewKeyedTransactor(testPriKey) + auth.Nonce = big.NewInt(int64(nonce)) + auth.Value = big.NewInt(0) // in wei + + // Deploy TRC21 + trc21Addr, trc21Instance, err := trc21issuer.DeployTRC21(auth, client, "Viction", "VIC", 18, big.NewInt(1000000000000000000), big.NewInt(0)) + if err != nil { + t.Fatal("Can't deploy TRC21: ", err) + } + fmt.Println("TRC21 address: ", trc21Addr.String()) + time.Sleep(10 * time.Second) + + // Get TRC21 name + name, err := trc21Instance.Name() + if err != nil { + t.Fatal("Can't get name of TRC21: ", err) + } + fmt.Println("TRC21 name: ", name) + + // Attach TRC21Issuer to TRC21Issuer address + trc21issuerAddr := common.TRC21IssuerSMC + trc21issuerInstance, _ := trc21issuer.NewTRC21Issuer(auth, trc21issuerAddr, client) + trc21IssuerMincap, err := trc21issuerInstance.MinCap() + if err != nil { + t.Fatal("Can't get min cap of trc21 issuer smart contract:", err) + } + fmt.Println("TRC21 Issuer min cap: ", trc21IssuerMincap) + + // Apply TRC21 issuer + trc21issuerInstance.TransactOpts.Nonce = big.NewInt(int64(nonce + 1)) + trc21issuerInstance.TransactOpts.Value = new(big.Int).SetUint64(10000000000000000000) + applyTx, err := trc21issuerInstance.Apply(trc21Addr) + if err != nil { + t.Fatal("Can't Apply free gas for token: ", err) + } + fmt.Println("Apply TRC21Issuer transaction: ", applyTx.Hash().Hex()) + time.Sleep(10 * time.Second) + applyTxReceipt, err := client.TransactionReceipt(context.Background(), applyTx.Hash()) + if err != nil { + t.Fatal("Can't get transaction receipt: ", err) + } + fmt.Println("Transaction receipt: ", applyTxReceipt) + + // Get balance token + balanceBefore, err := trc21issuerInstance.GetTokenCapacity(trc21Addr) + if err != nil { + t.Fatal("Can't get token capacity of trc21 issuer smart contract:", err) + } + fmt.Println("TRC21 Issuer token capacity: ", balanceBefore) + + // Get test account balance + testAccountBalanceBefore, err := client.BalanceAt(context.Background(), testAddr, nil) + if err != nil { + t.Fatal("Can't get balance of test account: ", err) + } + + // Transfer token to another address + trc21Instance.TransactOpts.Nonce = big.NewInt(int64(nonce + 2)) + transferTx, err := trc21Instance.Transfer(common.HexToAddress("0x8A244cfdd4777E44bedEDCD478e62AC311EC30Dc"), big.NewInt(1000000000000000000)) + if err != nil { + t.Fatal("Can't transfer token: ", err) + } + fmt.Println("Transfer token transaction: ", transferTx.Hash().Hex()) + time.Sleep(10 * time.Second) + transferTxReceipt, err := client.TransactionReceipt(context.Background(), transferTx.Hash()) + if err != nil { + t.Fatal("Can't get transaction receipt: ", err) + } + fmt.Println("Transaction receipt: ", transferTxReceipt) + + // Get test account balance after transfer + testAccountBalanceAfter, err := client.BalanceAt(context.Background(), testAddr, nil) + if err != nil { + t.Fatal("Can't get balance of test account: ", err) + } + + if testAccountBalanceBefore.Cmp(testAccountBalanceAfter) != 0 { + fmt.Println("Test failed: Test account balance before and after transfer is not equal") + } + + // Get balance token + balanceAfter, err := trc21issuerInstance.GetTokenCapacity(trc21Addr) + if err != nil { + t.Fatal("Can't get token capacity of trc21 issuer smart contract:", err) + } + fmt.Println("TRC21 Issuer token capacity: ", balanceAfter) + + if balanceBefore.Cmp(balanceAfter) <= 0 { + t.Fatal("Test failed: Token balance fee before and after transfer is not correct") + } +} + func TestRPCGetBlockReceipts(t *testing.T) { t.Parallel() diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 8f9c7af83c..54254c8501 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -456,6 +456,13 @@ web3._extend({ params: 1, inputFormatter: [web3._extend.formatters.inputTransactionFormatter] }), + new web3._extend.Method({ + name: 'estimateGas', + call: 'eth_estimateGas', + params: 2, + inputFormatter: [web3._extend.formatters.inputCallFormatter, web3._extend.formatters.inputBlockNumberFormatter], + outputFormatter: web3._extend.utils.toDecimal + }), new web3._extend.Method({ name: 'submitTransaction', call: 'eth_submitTransaction', diff --git a/les/api_backend.go b/les/api_backend.go index d8285da97d..f7f8d69acc 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -30,7 +30,6 @@ import ( "github.com/tomochain/tomochain/accounts" "github.com/tomochain/tomochain/common" - "github.com/tomochain/tomochain/common/math" "github.com/tomochain/tomochain/consensus" "github.com/tomochain/tomochain/core" "github.com/tomochain/tomochain/core/bloombits" @@ -106,7 +105,6 @@ func (b *LesApiBackend) GetTd(blockHash common.Hash) *big.Int { } func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, tomoxState *tradingstate.TradingStateDB, header *types.Header, vmCfg vm.Config) (*vm.EVM, func() error, error) { - state.SetBalance(msg.From(), math.MaxBig256) context := core.NewEVMContext(msg, header, b.eth.blockchain, nil) return vm.NewEVM(context, state, tomoxState, b.eth.chainConfig, vmCfg), state.Error, nil } diff --git a/les/odr_test.go b/les/odr_test.go index 3858e34028..0524e315a7 100644 --- a/les/odr_test.go +++ b/les/odr_test.go @@ -141,8 +141,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) @@ -158,9 +158,9 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai vmenv := vm.NewEVM(context, statedb, nil, config, vm.Config{}) 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 0c5fc78573..aa1d45690d 100644 --- a/light/odr_test.go +++ b/light/odr_test.go @@ -188,8 +188,8 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain vmenv := vm.NewEVM(context, st, nil, config, 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()...) if st.Error() != nil { return res, st.Error() } diff --git a/tests/state_test_util.go b/tests/state_test_util.go index e532aa8a46..6360457689 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -144,7 +144,7 @@ func (t *StateTest) Run(subtest StateSubtest, vmconfig vm.Config) (*state.StateD snapshot := statedb.Snapshot() 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) {