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:
parent
475919274e
commit
2b4d2bea82
128
rpc/eth_api.go
128
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
|
||||
}
|
||||
|
||||
|
8
types/params.go
Normal file
8
types/params.go
Normal 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
|
||||
)
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,6 +107,8 @@ func (st StateTransition) TransitionCSDB(ctx sdk.Context) (sdk.Result, *big.Int)
|
||||
}
|
||||
|
||||
func (st *StateTransition) checkNonce() sdk.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 {
|
||||
@ -112,6 +116,7 @@ func (st *StateTransition) checkNonce() sdk.Result {
|
||||
} else if nonce > st.AccountNonce {
|
||||
return emint.ErrInvalidNonce("nonce too low").Result()
|
||||
}
|
||||
}
|
||||
|
||||
return sdk.Result{}
|
||||
}
|
||||
|
@ -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
24
x/evm/types/utils_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user