From 6c72a79035a441e8082778eb366c3cc942787948 Mon Sep 17 00:00:00 2001 From: Austin Abell Date: Wed, 18 Sep 2019 14:45:21 -0400 Subject: [PATCH] RPC unlock Ethermint key and eth_sign (#99) * Set up personal account api for personal sign * Added unlocking key functionality and attach to eth rpc * Implemented eth_sign * Transform V in sig based on yp and fix bug * Fix lint issue * Remove escape character from comment * Switch error handling to panic on invalid unlocked key --- rpc/apis.go | 13 ++++++++-- rpc/config.go | 63 +++++++++++++++++++++++++++++++++++---------- rpc/eth_api.go | 23 ++++++++++++++--- rpc/personal_api.go | 34 ++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 rpc/personal_api.go diff --git a/rpc/apis.go b/rpc/apis.go index 1570bce5..7dc73222 100644 --- a/rpc/apis.go +++ b/rpc/apis.go @@ -4,21 +4,30 @@ package rpc import ( "github.com/cosmos/cosmos-sdk/client/context" + emintcrypto "github.com/cosmos/ethermint/crypto" "github.com/ethereum/go-ethereum/rpc" ) // GetRPCAPIs returns the list of all APIs -func GetRPCAPIs(cliCtx context.CLIContext) []rpc.API { +func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []rpc.API { return []rpc.API{ { Namespace: "web3", Version: "1.0", Service: NewPublicWeb3API(), + Public: true, }, { Namespace: "eth", Version: "1.0", - Service: NewPublicEthAPI(cliCtx), + Service: NewPublicEthAPI(cliCtx, key), + Public: true, + }, + { + Namespace: "personal", + Version: "1.0", + Service: NewPersonalEthAPI(cliCtx), + Public: false, }, } } diff --git a/rpc/config.go b/rpc/config.go index 43997155..790b1a5d 100644 --- a/rpc/config.go +++ b/rpc/config.go @@ -1,17 +1,21 @@ package rpc import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client/lcd" "github.com/cosmos/cosmos-sdk/codec" + emintcrypto "github.com/cosmos/ethermint/crypto" + emintkeys "github.com/cosmos/ethermint/keys" "github.com/ethereum/go-ethereum/rpc" + "github.com/spf13/cobra" - "log" + "github.com/spf13/viper" ) -// defaultModules returns all available modules -func defaultModules() []string { - return []string{"web3", "eth"} -} +const ( + flagUnlockKey = "unlock-key" +) // Config contains configuration fields that determine the behavior of the RPC HTTP server. // TODO: These may become irrelevant if HTTP config is handled by the SDK @@ -30,31 +34,64 @@ type Config struct { // Web3RpcCmd creates a CLI command to start RPC server func Web3RpcCmd(cdc *codec.Codec) *cobra.Command { - return lcd.ServeCommand(cdc, registerRoutes) + cmd := lcd.ServeCommand(cdc, registerRoutes) + // Attach flag to cmd output to be handled in registerRoutes + cmd.Flags().String(flagUnlockKey, "", "Select a key to unlock on the RPC server") + return cmd } // registerRoutes creates a new server and registers the `/rpc` endpoint. // Rpc calls are enabled based on their associated module (eg. "eth"). func registerRoutes(rs *lcd.RestServer) { s := rpc.NewServer() - apis := GetRPCAPIs(rs.CliCtx) + accountName := viper.GetString(flagUnlockKey) + + var emintKey emintcrypto.PrivKeySecp256k1 + if len(accountName) > 0 { + passphrase, err := emintkeys.GetPassphrase(accountName) + if err != nil { + panic(err) + } + + emintKey, err = unlockKeyFromNameAndPassphrase(accountName, passphrase) + if err != nil { + panic(err) + } + } + + apis := GetRPCAPIs(rs.CliCtx, emintKey) // TODO: Allow cli to configure modules https://github.com/ChainSafe/ethermint/issues/74 - modules := defaultModules() whitelist := make(map[string]bool) - for _, module := range modules { - whitelist[module] = true - } // Register all the APIs exposed by the services for _, api := range apis { if whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) { if err := s.RegisterName(api.Namespace, api.Service); err != nil { - log.Println(err) - return + panic(err) } } } rs.Mux.HandleFunc("/rpc", s.ServeHTTP).Methods("POST") } + +func unlockKeyFromNameAndPassphrase(accountName, passphrase string) (emintKey emintcrypto.PrivKeySecp256k1, err error) { + keybase, err := emintkeys.NewKeyBaseFromHomeFlag() + if err != nil { + return + } + + privKey, err := keybase.ExportPrivateKeyObject(accountName, passphrase) + if err != nil { + return + } + + var ok bool + emintKey, ok = privKey.(emintcrypto.PrivKeySecp256k1) + if !ok { + panic(fmt.Sprintf("invalid private key type: %T", privKey)) + } + + return +} diff --git a/rpc/eth_api.go b/rpc/eth_api.go index 1cd73129..f01997a5 100644 --- a/rpc/eth_api.go +++ b/rpc/eth_api.go @@ -1,13 +1,17 @@ package rpc import ( + "bytes" "fmt" "math/big" "github.com/cosmos/cosmos-sdk/client/context" + emintcrypto "github.com/cosmos/ethermint/crypto" emintkeys "github.com/cosmos/ethermint/keys" "github.com/cosmos/ethermint/version" "github.com/cosmos/ethermint/x/evm/types" + + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" @@ -17,12 +21,14 @@ import ( // PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. type PublicEthAPI struct { cliCtx context.CLIContext + key emintcrypto.PrivKeySecp256k1 } // NewPublicEthAPI creates an instance of the public ETH Web3 API. -func NewPublicEthAPI(cliCtx context.CLIContext) *PublicEthAPI { +func NewPublicEthAPI(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) *PublicEthAPI { return &PublicEthAPI{ cliCtx: cliCtx, + key: key, } } @@ -176,8 +182,19 @@ func (e *PublicEthAPI) GetCode(address common.Address, blockNumber rpc.BlockNumb } // Sign signs the provided data using the private key of address via Geth's signature standard. -func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) hexutil.Bytes { - return nil +func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + // TODO: Change this functionality to find an unlocked account by address + if e.key == nil || !bytes.Equal(e.key.PubKey().Address().Bytes(), address.Bytes()) { + return nil, keystore.ErrLocked + } + + // Sign the requested hash with the wallet + signature, err := e.key.Sign(data) + if err == nil { + signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + } + + return signature, err } // SendTransaction sends an Ethereum transaction. diff --git a/rpc/personal_api.go b/rpc/personal_api.go new file mode 100644 index 00000000..1aecd9b8 --- /dev/null +++ b/rpc/personal_api.go @@ -0,0 +1,34 @@ +package rpc + +import ( + "context" + + sdkcontext "github.com/cosmos/cosmos-sdk/client/context" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PersonalEthAPI struct { + cliCtx sdkcontext.CLIContext +} + +// NewPersonalEthAPI creates an instance of the public ETH Web3 API. +func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext) *PersonalEthAPI { + return &PersonalEthAPI{ + cliCtx: cliCtx, + } +} + +// Sign calculates an Ethereum ECDSA signature for: +// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message)) +// +// Note, the produced signature conforms to the secp256k1 curve R, S and V values, +// where the V value will be 27 or 28 for legacy reasons. +// +// The key used to calculate the signature is decrypted with the given password. +// +// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign +func (e *PersonalEthAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) { + return nil, nil +}