From 2b4d2bea82d48ce50ab89f50e3b109ced9c05779 Mon Sep 17 00:00:00 2001 From: Austin Abell Date: Wed, 23 Oct 2019 01:40:34 +0900 Subject: [PATCH] Implements eth_call (#127) * Fixed tx receipt error on failed transaction * Add returnData to failed transaction for logs bloom * Added simulate call option, without returning evm data * Added encoding and decoding of data from EVM execution for usability * Remove unused context parameter * Fix function comment and remove unnecessary logging on eth_call --- rpc/eth_api.go | 128 +++++++++++++++++++++++++++----- types/params.go | 8 ++ x/evm/handler.go | 6 +- x/evm/types/msg.go | 2 +- x/evm/types/state_transition.go | 25 ++++--- x/evm/types/utils.go | 27 +++++++ x/evm/types/utils_test.go | 24 ++++++ 7 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 types/params.go create mode 100644 x/evm/types/utils_test.go diff --git a/rpc/eth_api.go b/rpc/eth_api.go index 7f812ac3..82cce33a 100644 --- a/rpc/eth_api.go +++ b/rpc/eth_api.go @@ -3,12 +3,14 @@ package rpc import ( "bytes" "fmt" + "log" "math/big" "strconv" emintcrypto "github.com/cosmos/ethermint/crypto" emintkeys "github.com/cosmos/ethermint/keys" "github.com/cosmos/ethermint/rpc/args" + emint "github.com/cosmos/ethermint/types" "github.com/cosmos/ethermint/utils" "github.com/cosmos/ethermint/version" "github.com/cosmos/ethermint/x/evm" @@ -21,12 +23,15 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/spf13/viper" ) @@ -324,19 +329,115 @@ func (e *PublicEthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, erro return common.HexToHash(res.TxHash), nil } -// CallArgs represents arguments to a smart contract call as provided by RPC clients. +// CallArgs represents the arguments for a call. type CallArgs struct { - 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"` + 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"` } // Call performs a raw contract call. -func (e *PublicEthAPI) Call(args CallArgs, blockNum BlockNumber) hexutil.Bytes { - return nil +func (e *PublicEthAPI) Call(args CallArgs, blockNr rpc.BlockNumber, overrides *map[common.Address]account) (hexutil.Bytes, error) { + result, err := e.doCall(args, blockNr, vm.Config{}, big.NewInt(emint.DefaultRPCGasLimit)) + return (hexutil.Bytes)(result), err +} + +// account indicates the overriding fields of account during the execution of +// a message call. +// Note, state and stateDiff can't be specified at the same time. If state is +// set, message execution will only use the data in the given state. Otherwise +// if statDiff is set, all diff will be applied first and then execute the call +// message. +type account struct { + Nonce *hexutil.Uint64 `json:"nonce"` + Code *hexutil.Bytes `json:"code"` + Balance **hexutil.Big `json:"balance"` + State *map[common.Hash]common.Hash `json:"state"` + StateDiff *map[common.Hash]common.Hash `json:"stateDiff"` +} + +// DoCall performs a simulated call operation through the evm +func (e *PublicEthAPI) doCall(args CallArgs, blockNr rpc.BlockNumber, vmCfg vm.Config, globalGasCap *big.Int) ([]byte, error) { + // Set height for historical queries + ctx := e.cliCtx.WithHeight(blockNr.Int64()) + + // Set sender address or use a default if none specified + var addr common.Address + if args.From == nil { + if e.key != nil { + addr = common.BytesToAddress(e.key.PubKey().Address().Bytes()) + } + // No error handled here intentionally to match geth behaviour + } else { + addr = *args.From + } + + // Set default gas & gas price if none were set + // Change this to uint64(math.MaxUint64 / 2) if gas cap can be configured + gas := uint64(emint.DefaultRPCGasLimit) + if args.Gas != nil { + gas = uint64(*args.Gas) + } + if globalGasCap != nil && globalGasCap.Uint64() < gas { + log.Println("Caller gas above allowance, capping", "requested", gas, "cap", globalGasCap) + gas = globalGasCap.Uint64() + } + + // Set gas price using default or parameter if passed in + gasPrice := new(big.Int).SetUint64(emint.DefaultGasPrice) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + + // Set value for transaction + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + + // Set Data if provided + var data []byte + if args.Data != nil { + data = []byte(*args.Data) + } + + // Set destination address for call + var toAddr sdk.AccAddress + if args.To != nil { + toAddr = sdk.AccAddress(args.To.Bytes()) + } + + // Create new call message + msg := types.NewEmintMsg(0, &toAddr, sdk.NewIntFromBigInt(value), gas, + sdk.NewIntFromBigInt(gasPrice), data, sdk.AccAddress(addr.Bytes())) + + // Generate tx to be used to simulate (signature isn't needed) + tx := authtypes.NewStdTx([]sdk.Msg{msg}, authtypes.StdFee{}, []authtypes.StdSignature{authtypes.StdSignature{}}, "") + + // Encode transaction by default Tx encoder + txEncoder := authutils.GetTxEncoder(ctx.Codec) + txBytes, err := txEncoder(tx) + if err != nil { + return []byte{}, err + } + + // Transaction simulation through query + res, _, err := ctx.QueryWithData("app/simulate", txBytes) + if err != nil { + return []byte{}, err + } + + var simResult sdk.Result + if err = ctx.Codec.UnmarshalBinaryLengthPrefixed(res, &simResult); err != nil { + return nil, err + } + + _, _, ret, err := types.DecodeReturnData(simResult.Data) + + return ret, err } // EstimateGas estimates gas usage for the given smart contract call. @@ -606,13 +707,7 @@ func (e *PublicEthAPI) GetTransactionReceipt(hash common.Hash) (map[string]inter e.cliCtx.Codec.MustUnmarshalJSON(res, &logs) txData := tx.TxResult.GetData() - var bloomFilter ethtypes.Bloom - var contractAddress common.Address - if len(txData) >= 20 { - // TODO: change hard coded indexing of bytes - bloomFilter = ethtypes.BytesToBloom(txData[20:]) - contractAddress = common.BytesToAddress(txData[:20]) - } + contractAddress, bloomFilter, _, _ := types.DecodeReturnData(txData) fields := map[string]interface{}{ "blockHash": blockHash, @@ -630,7 +725,6 @@ func (e *PublicEthAPI) GetTransactionReceipt(hash common.Hash) (map[string]inter } if contractAddress != (common.Address{}) { - // TODO: change hard coded indexing of first 20 bytes fields["contractAddress"] = contractAddress } diff --git a/types/params.go b/types/params.go new file mode 100644 index 00000000..de577b1b --- /dev/null +++ b/types/params.go @@ -0,0 +1,8 @@ +package types + +const ( + // DefaultGasPrice is default gas price for evm transactions + DefaultGasPrice = 20 + // DefaultRPCGasLimit is default gas limit for RPC call operations + DefaultRPCGasLimit = 10000000 +) diff --git a/x/evm/handler.go b/x/evm/handler.go index f303235c..ebd641e4 100644 --- a/x/evm/handler.go +++ b/x/evm/handler.go @@ -20,8 +20,8 @@ func NewHandler(keeper Keeper) sdk.Handler { switch msg := msg.(type) { case types.EthereumTxMsg: return handleETHTxMsg(ctx, keeper, msg) - case types.EmintMsg: - return handleEmintMsg(ctx, keeper, msg) + case *types.EmintMsg: + return handleEmintMsg(ctx, keeper, *msg) default: errMsg := fmt.Sprintf("Unrecognized ethermint Msg type: %v", msg.Type()) return sdk.ErrUnknownRequest(errMsg).Result() @@ -67,6 +67,7 @@ func handleETHTxMsg(ctx sdk.Context, keeper Keeper, msg types.EthereumTxMsg) sdk Csdb: keeper.csdb.WithContext(ctx), ChainID: intChainID, THash: ðHash, + Simulate: ctx.IsCheckTx(), } // Prepare db for logs keeper.csdb.Prepare(ethHash, common.Hash{}, keeper.txCount.get()) @@ -97,6 +98,7 @@ func handleEmintMsg(ctx sdk.Context, keeper Keeper, msg types.EmintMsg) sdk.Resu Payload: msg.Payload, Csdb: keeper.csdb.WithContext(ctx), ChainID: intChainID, + Simulate: ctx.IsCheckTx(), } if msg.Recipient != nil { diff --git a/x/evm/types/msg.go b/x/evm/types/msg.go index 7d04186e..f5c59137 100644 --- a/x/evm/types/msg.go +++ b/x/evm/types/msg.go @@ -391,7 +391,7 @@ func GenerateFromArgs(args args.SendTxArgs, ctx context.CLIContext) (msg *Ethere if args.GasPrice == nil { // Set default gas price // TODO: Change to min gas price from context once available through server/daemon - gasPrice = big.NewInt(20) + gasPrice = big.NewInt(types.DefaultGasPrice) } if args.Nonce == nil { diff --git a/x/evm/types/state_transition.go b/x/evm/types/state_transition.go index 408e8cbe..9c35ef96 100644 --- a/x/evm/types/state_transition.go +++ b/x/evm/types/state_transition.go @@ -25,6 +25,7 @@ type StateTransition struct { Csdb *CommitStateDB ChainID *big.Int THash *common.Hash + Simulate bool } // TransitionCSDB performs an evm state transition from a transaction @@ -60,6 +61,7 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) ) var ( + ret []byte leftOverGas uint64 addr common.Address vmerr error @@ -67,11 +69,11 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) ) if contractCreation { - _, addr, leftOverGas, vmerr = vmenv.Create(senderRef, st.Payload, gasLimit, st.Amount) + ret, addr, leftOverGas, vmerr = vmenv.Create(senderRef, st.Payload, gasLimit, st.Amount) } else { // Increment the nonce for the next transaction st.Csdb.SetNonce(st.Sender, st.Csdb.GetNonce(st.Sender)+1) - _, leftOverGas, vmerr = vmenv.Call(senderRef, *st.Recipient, st.Payload, gasLimit, st.Amount) + ret, leftOverGas, vmerr = vmenv.Call(senderRef, *st.Recipient, st.Payload, gasLimit, st.Amount) } // Generate bloom filter to be saved in tx receipt data @@ -83,8 +85,8 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) bloomFilter = ethtypes.BytesToBloom(bloomInt.Bytes()) } - // TODO: coniditionally add either/ both of these to return data - returnData := append(addr.Bytes(), bloomFilter.Bytes()...) + // Encode all necessary data into slice of bytes to return in sdk result + returnData := EncodeReturnData(addr, bloomFilter, ret) // handle errors if vmerr != nil { @@ -105,12 +107,15 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int) } func (st *StateTransition) checkNonce() sdk.Result { - // Make sure this transaction's nonce is correct. - nonce := st.Csdb.GetNonce(st.Sender) - if nonce < st.AccountNonce { - return emint.ErrInvalidNonce("nonce too high").Result() - } else if nonce > st.AccountNonce { - return emint.ErrInvalidNonce("nonce too low").Result() + // If simulated transaction, don't verify nonce + if !st.Simulate { + // Make sure this transaction's nonce is correct. + nonce := st.Csdb.GetNonce(st.Sender) + if nonce < st.AccountNonce { + return emint.ErrInvalidNonce("nonce too high").Result() + } else if nonce > st.AccountNonce { + return emint.ErrInvalidNonce("nonce too low").Result() + } } return sdk.Result{} diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index de7973ba..1557b9f3 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -5,6 +5,7 @@ import ( "github.com/cosmos/ethermint/crypto" ethcmn "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" @@ -12,6 +13,11 @@ import ( "golang.org/x/crypto/sha3" ) +const ( + bloomIdx = ethcmn.AddressLength + returnIdx = bloomIdx + ethtypes.BloomByteLength +) + // GenerateEthAddress generates an Ethereum address. func GenerateEthAddress() ethcmn.Address { priv, err := crypto.GenerateKey() @@ -46,3 +52,24 @@ func rlpHash(x interface{}) (hash ethcmn.Hash) { return hash } + +// EncodeReturnData takes all of the necessary data from the EVM execution +// and returns the data as a byte slice +func EncodeReturnData(addr ethcmn.Address, bloom ethtypes.Bloom, evmRet []byte) []byte { + // Append address, bloom, evm return bytes in that order + returnData := append(addr.Bytes(), bloom.Bytes()...) + return append(returnData, evmRet...) +} + +// DecodeReturnData decodes the byte slice of values to their respective types +func DecodeReturnData(bytes []byte) (addr ethcmn.Address, bloom ethtypes.Bloom, ret []byte, err error) { + if len(bytes) >= returnIdx { + addr = ethcmn.BytesToAddress(bytes[:bloomIdx]) + bloom = ethtypes.BytesToBloom(bytes[bloomIdx:returnIdx]) + ret = bytes[returnIdx:] + } else { + err = fmt.Errorf("Invalid format for encoded data, message must be an EVM state transition") + } + + return +} diff --git a/x/evm/types/utils_test.go b/x/evm/types/utils_test.go new file mode 100644 index 00000000..9611963a --- /dev/null +++ b/x/evm/types/utils_test.go @@ -0,0 +1,24 @@ +package types + +import ( + "testing" + + ethcmn "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +func TestEvmDataEncoding(t *testing.T) { + addr := ethcmn.HexToAddress("0x12345") + bloom := ethtypes.BytesToBloom([]byte{0x1, 0x3}) + ret := []byte{0x5, 0x8} + + encoded := EncodeReturnData(addr, bloom, ret) + + decAddr, decBloom, decRet, err := DecodeReturnData(encoded) + + require.NoError(t, err) + require.Equal(t, addr, decAddr) + require.Equal(t, bloom, decBloom) + require.Equal(t, ret, decRet) +}