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
This commit is contained in:
Austin Abell 2019-10-23 01:40:34 +09:00 committed by Dustin Brickwood
parent 475919274e
commit 2b4d2bea82
7 changed files with 190 additions and 30 deletions

View File

@ -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
}

8
types/params.go Normal file
View File

@ -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
)

View File

@ -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: &ethHash,
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 {

View File

@ -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 {

View File

@ -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{}

View File

@ -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
}

24
x/evm/types/utils_test.go Normal file
View File

@ -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)
}