From 28aaba069530176567443cb72af616a057e9a7fc Mon Sep 17 00:00:00 2001 From: Austin Abell Date: Fri, 20 Sep 2019 09:30:20 -0400 Subject: [PATCH] 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 --- keys/show.go | 14 +++++++-- keys/utils.go | 9 ++++++ rpc/addrlock.go | 38 +++++++++++++++++++++++ rpc/apis.go | 5 +-- rpc/args/send_tx.go | 22 ++++++++++++++ rpc/eth_api.go | 74 +++++++++++++++++++++++++++++++++++++++------ rpc/personal_api.go | 8 +++-- x/evm/types/msg.go | 61 +++++++++++++++++++++++++++++++++++++ 8 files changed, 213 insertions(+), 18 deletions(-) create mode 100644 rpc/addrlock.go create mode 100644 rpc/args/send_tx.go diff --git a/keys/show.go b/keys/show.go index ddccf56b..bd2f8065 100644 --- a/keys/show.go +++ b/keys/show.go @@ -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: diff --git a/keys/utils.go b/keys/utils.go index b10ecc42..e8579104 100644 --- a/keys/utils.go +++ b/keys/utils.go @@ -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 { diff --git a/rpc/addrlock.go b/rpc/addrlock.go new file mode 100644 index 00000000..51f6b9c3 --- /dev/null +++ b/rpc/addrlock.go @@ -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() +} diff --git a/rpc/apis.go b/rpc/apis.go index 7dc73222..c3aa2281 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -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, }, } diff --git a/rpc/args/send_tx.go b/rpc/args/send_tx.go new file mode 100644 index 00000000..61d8a225 --- /dev/null +++ b/rpc/args/send_tx.go @@ -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"` +} \ No newline at end of file diff --git a/rpc/eth_api.go b/rpc/eth_api.go index 4acc00e2..335f1f8c 100644 --- a/rpc/eth_api.go +++ b/rpc/eth_api.go @@ -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 + 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, + 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. diff --git a/rpc/personal_api.go b/rpc/personal_api.go index 1aecd9b8..a97220b1 100644 --- a/rpc/personal_api.go +++ b/rpc/personal_api.go @@ -10,13 +10,15 @@ import ( // PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. type PersonalEthAPI struct { - cliCtx sdkcontext.CLIContext + 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, + cliCtx: cliCtx, + nonceLock: nonceLock, } } diff --git a/x/evm/types/msg.go b/x/evm/types/msg.go index b9ecd9a3..6135f9f9 100644 --- a/x/evm/types/msg.go +++ b/x/evm/types/msg.go @@ -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 +}