Implement eth_sendTransaction (#104)

* Set up framework for sending transaction with correct args and nonce mutex locking

* Set up printing ethereum address through emintkeys and getting chainid from flags

* Implemented defaults for eth_sendTransaction

* Fix bug with no data provided

* Updated comments and error, as well as RLP encoded tx bytes for return instead of amino encoded
This commit is contained in:
Austin Abell 2019-09-20 09:30:20 -04:00 committed by GitHub
parent 2ca42cc155
commit 28aaba0695
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 213 additions and 18 deletions

View File

@ -19,6 +19,8 @@ import (
const (
// FlagAddress is the flag for the user's address on the command line.
FlagAddress = "address"
// FlagAddress is the flag for the user's address on the command line.
FlagETHAddress = "ethwallet"
// FlagPublicKey represents the user's public key on the command line.
FlagPublicKey = "pubkey"
// FlagBechPrefix defines a desired Bech32 prefix encoding for a key.
@ -38,6 +40,7 @@ func showKeysCmd() *cobra.Command {
cmd.Flags().String(FlagBechPrefix, sdk.PrefixAccount, "The Bech32 prefix encoding for a key (acc|val|cons)")
cmd.Flags().BoolP(FlagAddress, "a", false, "Output the address only (overrides --output)")
cmd.Flags().BoolP(FlagETHAddress, "w", false, "Output the Ethereum address only (overrides --output)")
cmd.Flags().BoolP(FlagPublicKey, "p", false, "Output the public key only (overrides --output)")
cmd.Flags().BoolP(FlagDevice, "d", false, "Output the address in a ledger device")
cmd.Flags().Bool(flags.FlagIndentResponse, false, "Add indent to JSON response")
@ -58,6 +61,7 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
}
isShowAddr := viper.GetBool(FlagAddress)
isShowEthAddr := viper.GetBool(FlagETHAddress)
isShowPubKey := viper.GetBool(FlagPublicKey)
isShowDevice := viper.GetBool(FlagDevice)
@ -67,11 +71,13 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
isOutputSet = tmp.Changed
}
if isShowAddr && isShowPubKey {
return errors.New("cannot use both --address and --pubkey at once")
isShowEitherAddr := isShowAddr || isShowEthAddr
if isShowEitherAddr && isShowPubKey {
return errors.New("cannot get address, with --address or --ethwallet, and --pubkey at once")
}
if isOutputSet && (isShowAddr || isShowPubKey) {
if isOutputSet && (isShowEitherAddr || isShowPubKey) {
return errors.New("cannot use --output with --address or --pubkey")
}
@ -80,6 +86,8 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
switch {
case isShowAddr:
printKeyAddress(info, keyOutputFunction)
case isShowEthAddr:
printKeyEthAddress(info, keyOutputFunction)
case isShowPubKey:
printPubKey(info, keyOutputFunction)
default:

View File

@ -148,6 +148,15 @@ func printKeyAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
fmt.Println(ko.Address)
}
func printKeyEthAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
ko, err := bechKeyOut(info)
if err != nil {
panic(err)
}
fmt.Println(ko.ETHAddress)
}
func printPubKey(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
ko, err := bechKeyOut(info)
if err != nil {

38
rpc/addrlock.go Normal file
View File

@ -0,0 +1,38 @@
package rpc
import (
"sync"
"github.com/ethereum/go-ethereum/common"
)
// AddrLocker is a mutex structure used to avoid querying outdated account data
type AddrLocker struct {
mu sync.Mutex
locks map[common.Address]*sync.Mutex
}
// lock returns the lock of the given address.
func (l *AddrLocker) lock(address common.Address) *sync.Mutex {
l.mu.Lock()
defer l.mu.Unlock()
if l.locks == nil {
l.locks = make(map[common.Address]*sync.Mutex)
}
if _, ok := l.locks[address]; !ok {
l.locks[address] = new(sync.Mutex)
}
return l.locks[address]
}
// LockAddr locks an account's mutex. This is used to prevent another tx getting the
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from
// being read again during the time that the first transaction is being signed.
func (l *AddrLocker) LockAddr(address common.Address) {
l.lock(address).Lock()
}
// UnlockAddr unlocks the mutex of the given account.
func (l *AddrLocker) UnlockAddr(address common.Address) {
l.lock(address).Unlock()
}

View File

@ -10,6 +10,7 @@ import (
// GetRPCAPIs returns the list of all APIs
func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []rpc.API {
nonceLock := new(AddrLocker)
return []rpc.API{
{
Namespace: "web3",
@ -20,13 +21,13 @@ func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []r
{
Namespace: "eth",
Version: "1.0",
Service: NewPublicEthAPI(cliCtx, key),
Service: NewPublicEthAPI(cliCtx, nonceLock, key),
Public: true,
},
{
Namespace: "personal",
Version: "1.0",
Service: NewPersonalEthAPI(cliCtx),
Service: NewPersonalEthAPI(cliCtx, nonceLock),
Public: false,
},
}

22
rpc/args/send_tx.go Normal file
View File

@ -0,0 +1,22 @@
package args
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool.
// Duplicate struct definition since geth struct is in internal package
// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.9/internal/ethapi/api.go#L1346
type SendTxArgs 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"`
Nonce *hexutil.Uint64 `json:"nonce"`
// We accept "data" and "input" for backwards-compatibility reasons. "input" is the
// newer name and should be preferred by clients.
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input"`
}

View File

@ -9,6 +9,7 @@ import (
authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
emintcrypto "github.com/cosmos/ethermint/crypto"
emintkeys "github.com/cosmos/ethermint/keys"
"github.com/cosmos/ethermint/rpc/args"
"github.com/cosmos/ethermint/version"
"github.com/cosmos/ethermint/x/evm/types"
@ -17,20 +18,26 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/signer/core"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/spf13/viper"
)
// PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
type PublicEthAPI struct {
cliCtx context.CLIContext
key emintcrypto.PrivKeySecp256k1
nonceLock *AddrLocker
}
// NewPublicEthAPI creates an instance of the public ETH Web3 API.
func NewPublicEthAPI(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) *PublicEthAPI {
func NewPublicEthAPI(cliCtx context.CLIContext, nonceLock *AddrLocker,
key emintcrypto.PrivKeySecp256k1) *PublicEthAPI {
return &PublicEthAPI{
cliCtx: cliCtx,
key: key,
nonceLock: nonceLock,
}
}
@ -200,9 +207,54 @@ func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil
}
// SendTransaction sends an Ethereum transaction.
func (e *PublicEthAPI) SendTransaction(args core.SendTxArgs) common.Hash {
var h common.Hash
return h
func (e *PublicEthAPI) SendTransaction(args args.SendTxArgs) (common.Hash, error) {
// TODO: Change this functionality to find an unlocked account by address
if e.key == nil || !bytes.Equal(e.key.PubKey().Address().Bytes(), args.From.Bytes()) {
return common.Hash{}, keystore.ErrLocked
}
// Mutex lock the address' nonce to avoid assigning it to multiple requests
if args.Nonce == nil {
e.nonceLock.LockAddr(args.From)
defer e.nonceLock.UnlockAddr(args.From)
}
// Assemble transaction from fields
tx, err := types.GenerateFromArgs(args, e.cliCtx)
if err != nil {
return common.Hash{}, err
}
// ChainID must be set as flag to send transaction
chainID := viper.GetString(flags.FlagChainID)
// parse the chainID from a string to a base-10 integer
intChainID, ok := new(big.Int).SetString(chainID, 10)
if !ok {
return common.Hash{}, fmt.Errorf(
fmt.Sprintf("Invalid chainID: %s, must be integer format", chainID))
}
// Sign transaction
tx.Sign(intChainID, e.key.ToECDSA())
// Encode transaction by default Tx encoder
txEncoder := authutils.GetTxEncoder(e.cliCtx.Codec)
txBytes, err := txEncoder(tx)
if err != nil {
return common.Hash{}, err
}
// Broadcast transaction
res, err := e.cliCtx.BroadcastTx(txBytes)
// If error is encountered on the node, the broadcast will not return an error
// TODO: Remove res log
fmt.Println(res.RawLog)
if err != nil {
return common.Hash{}, err
}
// Return RLP encoded bytes
return tx.Hash(), nil
}
// SendRawTransaction send a raw Ethereum transaction.
@ -225,12 +277,14 @@ func (e *PublicEthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, erro
// TODO: Possibly log the contract creation address (if recipient address is nil) or tx data
res, err := e.cliCtx.BroadcastTx(txBytes)
// If error is encountered on the node, the broadcast will not return an error
// TODO: Remove res log
fmt.Println(res.RawLog)
if err != nil {
return common.Hash{}, err
}
return common.HexToHash(res.TxHash), nil
// Return RLP encoded bytes
return tx.Hash(), nil
}
// CallArgs represents arguments to a smart contract call as provided by RPC clients.

View File

@ -11,12 +11,14 @@ import (
// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
type PersonalEthAPI struct {
cliCtx sdkcontext.CLIContext
nonceLock *AddrLocker
}
// NewPersonalEthAPI creates an instance of the public ETH Web3 API.
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext) *PersonalEthAPI {
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext, nonceLock *AddrLocker) *PersonalEthAPI {
return &PersonalEthAPI{
cliCtx: cliCtx,
nonceLock: nonceLock,
}
}

View File

@ -1,6 +1,7 @@
package types
import (
"bytes"
"crypto/ecdsa"
"errors"
"fmt"
@ -10,8 +11,11 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/ethermint/rpc/args"
"github.com/cosmos/ethermint/types"
"github.com/cosmos/cosmos-sdk/client/context"
ethcmn "github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
@ -355,3 +359,60 @@ func recoverEthSig(R, S, Vb *big.Int, sigHash ethcmn.Hash) (ethcmn.Address, erro
return addr, nil
}
// PopulateFromArgs populates tx message with args (used in RPC API)
func GenerateFromArgs(args args.SendTxArgs, ctx context.CLIContext) (msg *EthereumTxMsg, err error) {
var nonce uint64
var gasLimit uint64
amount := (*big.Int)(args.Value)
gasPrice := (*big.Int)(args.GasPrice)
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)
}
if args.Nonce == nil {
// Get nonce (sequence) from account
from := sdk.AccAddress(args.From.Bytes())
_, nonce, err = authtypes.NewAccountRetriever(ctx).GetAccountNumberSequence(from)
if err != nil {
return nil, err
}
} else {
nonce = (uint64)(*args.Nonce)
}
if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
return nil, fmt.Errorf(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`)
}
// Sets input to either Input or Data, if both are set and not equal error above returns
var input []byte
if args.Input != nil {
input = *args.Input
} else if args.Data != nil {
input = *args.Data
}
if args.To == nil {
// Contract creation
if len(input) == 0 {
return nil, fmt.Errorf("contract creation without any data provided")
}
}
if args.Gas == nil {
// Estimate the gas usage if necessary.
// TODO: Set gas based on estimate when simulating txs are setup
gasLimit = 22000
} else {
gasLimit = (uint64)(*args.Gas)
}
return newEthereumTxMsg(nonce, args.To, amount, gasLimit, gasPrice, input), nil
}