Skip to content

Commit

Permalink
rpc: refactor backend package (evmos#418)
Browse files Browse the repository at this point in the history
* backend refractor

* Revert init file changes

* fix linter issues

* Update ethereum/rpc/namespaces/personal/api.go

Co-authored-by: Federico Kunze Küllmer <[email protected]>
  • Loading branch information
crypto-facs and fedekunze authored Aug 10, 2021
1 parent 9227e78 commit 640684c
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 296 deletions.
8 changes: 3 additions & 5 deletions ethereum/rpc/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ const (
func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpcclient.WSClient, selectedAPIs []string) []rpc.API {
nonceLock := new(types.AddrLocker)
evmBackend := backend.NewEVMBackend(ctx.Logger, clientCtx)
ethAPI := eth.NewPublicAPI(ctx.Logger, clientCtx, evmBackend, nonceLock)

var apis []rpc.API

// remove duplicates
selectedAPIs = unique(selectedAPIs)

Expand All @@ -51,7 +49,7 @@ func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpccl
rpc.API{
Namespace: EthNamespace,
Version: apiVersion,
Service: ethAPI,
Service: eth.NewPublicAPI(ctx.Logger, clientCtx, evmBackend, nonceLock),
Public: true,
},
rpc.API{
Expand Down Expand Up @@ -84,7 +82,7 @@ func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpccl
rpc.API{
Namespace: PersonalNamespace,
Version: apiVersion,
Service: personal.NewAPI(ctx.Logger, ethAPI),
Service: personal.NewAPI(ctx.Logger, clientCtx, evmBackend),
Public: true,
},
)
Expand All @@ -111,7 +109,7 @@ func GetRPCAPIs(ctx *server.Context, clientCtx client.Context, tmWSClient *rpccl
rpc.API{
Namespace: MinerNamespace,
Version: apiVersion,
Service: miner.NewMinerAPI(ctx, ethAPI, evmBackend),
Service: miner.NewMinerAPI(ctx, clientCtx, evmBackend),
Public: true,
},
)
Expand Down
161 changes: 149 additions & 12 deletions ethereum/rpc/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package backend

import (
"context"
"encoding/json"
"fmt"
"math/big"
"regexp"
"strconv"

"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
"github.com/ethereum/go-ethereum/accounts/keystore"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"

Expand All @@ -26,27 +32,22 @@ import (
evmtypes "github.com/tharsis/ethermint/x/evm/types"
)

// Backend implements the functionality needed to filter changes.
// Backend implements the functionality shared within namespaces.
// Implemented by EVMBackend.
type Backend interface {
// Used by block filter; also used for polling
BlockNumber() (hexutil.Uint64, error)
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
GetBlockByNumber(blockNum types.BlockNumber, fullTx bool) (map[string]interface{}, error)
GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error)

// returns the logs of a given block
GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error)

// Used by pending transaction filter
HeaderByNumber(blockNum types.BlockNumber) (*ethtypes.Header, error)
HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error)
PendingTransactions() ([]*sdk.Tx, error)

// Used by log filter
GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error)
GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error)
SendTransaction(args types.SendTxArgs) (common.Hash, error)
GetLogs(blockHash common.Hash) ([][]*ethtypes.Log, error)
BloomStatus() (uint64, uint64)

GetCoinbase() (sdk.AccAddress, error)
EstimateGas(args evmtypes.CallArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error)
}

var _ Backend = (*EVMBackend)(nil)
Expand Down Expand Up @@ -444,3 +445,139 @@ func (e *EVMBackend) GetCoinbase() (sdk.AccAddress, error) {
address, _ := sdk.AccAddressFromBech32(res.AccountAddress)
return address, nil
}

func (e *EVMBackend) SendTransaction(args types.SendTxArgs) (common.Hash, error) {
// Look up the wallet containing the requested signer
_, err := e.clientCtx.Keyring.KeyByAddress(sdk.AccAddress(args.From.Bytes()))
if err != nil {
e.logger.Error("failed to find key in keyring", "address", args.From, "error", err.Error())
return common.Hash{}, fmt.Errorf("%s; %s", keystore.ErrNoMatch, err.Error())
}

args, err = e.setTxDefaults(args)
if err != nil {
return common.Hash{}, err
}

msg := args.ToTransaction()

if err := msg.ValidateBasic(); err != nil {
e.logger.Debug("tx failed basic validation", "error", err.Error())
return common.Hash{}, err
}

// TODO: get from chain config
signer := ethtypes.LatestSignerForChainID(args.ChainID.ToInt())

// Sign transaction
if err := msg.Sign(signer, e.clientCtx.Keyring); err != nil {
e.logger.Debug("failed to sign tx", "error", err.Error())
return common.Hash{}, err
}

// Assemble transaction from fields
builder, ok := e.clientCtx.TxConfig.NewTxBuilder().(authtx.ExtensionOptionsTxBuilder)
if !ok {
e.logger.Error("clientCtx.TxConfig.NewTxBuilder returns unsupported builder", "error", err.Error())
}

option, err := codectypes.NewAnyWithValue(&evmtypes.ExtensionOptionsEthereumTx{})
if err != nil {
e.logger.Error("codectypes.NewAnyWithValue failed to pack an obvious value", "error", err.Error())
return common.Hash{}, err
}

builder.SetExtensionOptions(option)
err = builder.SetMsgs(msg)
if err != nil {
e.logger.Error("builder.SetMsgs failed", "error", err.Error())
}

// Query params to use the EVM denomination
res, err := e.queryClient.QueryClient.Params(e.ctx, &evmtypes.QueryParamsRequest{})
if err != nil {
e.logger.Error("failed to query evm params", "error", err.Error())
return common.Hash{}, err
}

txData, err := evmtypes.UnpackTxData(msg.Data)
if err != nil {
e.logger.Error("failed to unpack tx data", "error", err.Error())
return common.Hash{}, err
}

fees := sdk.Coins{sdk.NewCoin(res.Params.EvmDenom, sdk.NewIntFromBigInt(txData.Fee()))}
builder.SetFeeAmount(fees)
builder.SetGasLimit(msg.GetGas())

// Encode transaction by default Tx encoder
txEncoder := e.clientCtx.TxConfig.TxEncoder()
txBytes, err := txEncoder(builder.GetTx())
if err != nil {
e.logger.Error("failed to encode eth tx using default encoder", "error", err.Error())
return common.Hash{}, err
}

txHash := msg.AsTransaction().Hash()

// Broadcast transaction in sync mode (default)
// NOTE: If error is encountered on the node, the broadcast will not return an error
syncCtx := e.clientCtx.WithBroadcastMode(flags.BroadcastSync)
rsp, err := syncCtx.BroadcastTx(txBytes)
if err != nil || rsp.Code != 0 {
if err == nil {
err = errors.New(rsp.RawLog)
}
e.logger.Error("failed to broadcast tx", "error", err.Error())
return txHash, err
}

// Return transaction hash
return txHash, nil
}

// EstimateGas returns an estimate of gas usage for the given smart contract call.
func (e *EVMBackend) EstimateGas(args evmtypes.CallArgs, blockNrOptional *types.BlockNumber) (hexutil.Uint64, error) {
blockNr := types.EthPendingBlockNumber
if blockNrOptional != nil {
blockNr = *blockNrOptional
}

bz, err := json.Marshal(&args)
if err != nil {
return 0, err
}
req := evmtypes.EthCallRequest{Args: bz, GasCap: ethermint.DefaultRPCGasLimit}

// From ContextWithHeight: if the provided height is 0,
// it will return an empty context and the gRPC query will use
// the latest block height for querying.
res, err := e.queryClient.EstimateGas(types.ContextWithHeight(blockNr.Int64()), &req)
if err != nil {
return 0, err
}
return hexutil.Uint64(res.Gas), nil
}

// GetTransactionCount returns the number of transactions at the given address up to the given block number.
func (e *EVMBackend) GetTransactionCount(address common.Address, blockNum types.BlockNumber) (*hexutil.Uint64, error) {
// Get nonce (sequence) from account
from := sdk.AccAddress(address.Bytes())
accRet := e.clientCtx.AccountRetriever

err := accRet.EnsureExists(e.clientCtx, from)
if err != nil {
// account doesn't exist yet, return 0
n := hexutil.Uint64(0)
return &n, nil
}

includePending := blockNum == types.EthPendingBlockNumber
nonce, err := e.getAccountNonce(address, includePending, blockNum.Int64(), e.logger)
if err != nil {
return nil, err
}

n := hexutil.Uint64(nonce)
return &n, nil
}
136 changes: 136 additions & 0 deletions ethereum/rpc/backend/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package backend

import (
"bytes"
"errors"
"math/big"

sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/ethereum/go-ethereum/common"
"github.com/tendermint/tendermint/libs/log"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/tharsis/ethermint/ethereum/rpc/types"
ethermint "github.com/tharsis/ethermint/types"
evmtypes "github.com/tharsis/ethermint/x/evm/types"
)

// setTxDefaults populates tx message with default values in case they are not
// provided on the args
func (e *EVMBackend) setTxDefaults(args types.SendTxArgs) (types.SendTxArgs, error) {

if args.GasPrice == nil {
// TODO: Change to either:
// - min gas price from context once available through server/daemon, or
// - suggest a gas price based on the previous included txs
args.GasPrice = (*hexutil.Big)(big.NewInt(ethermint.DefaultGasPrice))
}

if args.Nonce == nil {
// get the nonce from the account retriever
// ignore error in case tge account doesn't exist yet
nonce, _ := e.getAccountNonce(args.From, true, 0, e.logger)
args.Nonce = (*hexutil.Uint64)(&nonce)
}

if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
return args, errors.New("both 'data' and 'input' are set and not equal. Please use 'input' to pass transaction call data")
}

if args.To == nil {
// Contract creation
var input []byte
if args.Data != nil {
input = *args.Data
} else if args.Input != nil {
input = *args.Input
}

if len(input) == 0 {
return args, errors.New(`contract creation without any data provided`)
}
}

if args.Gas == nil {
// For backwards-compatibility reason, we try both input and data
// but input is preferred.
input := args.Input
if input == nil {
input = args.Data
}

callArgs := evmtypes.CallArgs{
From: &args.From, // From shouldn't be nil
To: args.To,
Gas: args.Gas,
GasPrice: args.GasPrice,
Value: args.Value,
Data: input,
AccessList: args.AccessList,
}
blockNr := types.NewBlockNumber(big.NewInt(0))
estimated, err := e.EstimateGas(callArgs, &blockNr)
if err != nil {
return args, err
}
args.Gas = &estimated
e.logger.Debug("estimate gas usage automatically", "gas", args.Gas)
}

if args.ChainID == nil {
args.ChainID = (*hexutil.Big)(e.chainID)
}

return args, nil
}

// getAccountNonce returns the account nonce for the given account address.
// If the pending value is true, it will iterate over the mempool (pending)
// txs in order to compute and return the pending tx sequence.
// Todo: include the ability to specify a blockNumber
func (e *EVMBackend) getAccountNonce(accAddr common.Address, pending bool, height int64, logger log.Logger) (uint64, error) {
queryClient := authtypes.NewQueryClient(e.clientCtx)
res, err := queryClient.Account(types.ContextWithHeight(height), &authtypes.QueryAccountRequest{Address: sdk.AccAddress(accAddr.Bytes()).String()})
if err != nil {
return 0, err
}
var acc authtypes.AccountI
if err := e.clientCtx.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil {
return 0, err
}

nonce := acc.GetSequence()

if !pending {
return nonce, nil
}

// the account retriever doesn't include the uncommitted transactions on the nonce so we need to
// to manually add them.
pendingTxs, err := e.PendingTransactions()
if err != nil {
logger.Error("failed to fetch pending transactions", "error", err.Error())
return nonce, nil
}

// add the uncommitted txs to the nonce counter
// only supports `MsgEthereumTx` style tx
for _, tx := range pendingTxs {
msg, err := evmtypes.UnwrapEthereumMsg(tx)
if err != nil {
// not ethereum tx
continue
}

sender, err := msg.GetSender(e.chainID)
if err != nil {
continue
}
if sender == accAddr {
nonce++
}
}

return nonce, nil
}
Loading

0 comments on commit 640684c

Please sign in to comment.