rpc: personal API (#21)

* rpc: personal API
* rpc: fix personal_newAccount
This commit is contained in:
Federico Kunze
2021-05-12 12:50:26 -04:00
committed by GitHub
parent 65453e4aa0
commit a952bc36d1
6 changed files with 230 additions and 302 deletions
+6
View File
@@ -51,5 +51,11 @@ func GetRPCAPIs(clientCtx client.Context, tmWSClient *rpcclient.WSClient) []rpc.
Service: NewPublicNetAPI(clientCtx),
Public: true,
},
{
Namespace: PersonalNamespace,
Version: apiVersion,
Service: NewPersonalAPI(ethAPI),
Public: true,
},
}
}
+31 -5
View File
@@ -9,19 +9,20 @@ import (
"strings"
"sync"
"github.com/cosmos/ethermint/ethereum/rpc/types"
"github.com/gogo/protobuf/jsonpb"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/pkg/errors"
"github.com/spf13/viper"
log "github.com/xlab/suplog"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/tmhash"
"github.com/ethereum/go-ethereum/accounts/keystore"
@@ -30,6 +31,8 @@ import (
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/cosmos/ethermint/crypto/hd"
"github.com/cosmos/ethermint/ethereum/rpc/types"
rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
@@ -39,11 +42,11 @@ import (
type PublicEthAPI struct {
ctx context.Context
clientCtx client.Context
queryClient *types.QueryClient
queryClient *rpctypes.QueryClient
chainIDEpoch *big.Int
logger log.Logger
backend Backend
nonceLock *types.AddrLocker
nonceLock *rpctypes.AddrLocker
keyringLock sync.Mutex
}
@@ -58,6 +61,24 @@ func NewPublicEthAPI(
panic(err)
}
algos, _ := clientCtx.Keyring.SupportedAlgorithms()
if !algos.Contains(hd.EthSecp256k1) {
kr, err := keyring.New(
sdk.KeyringServiceName(),
viper.GetString(flags.FlagKeyringBackend),
clientCtx.KeyringDir,
clientCtx.Input,
hd.EthSecp256k1Option(),
)
if err != nil {
panic(err)
}
clientCtx = clientCtx.WithKeyring(kr)
}
api := &PublicEthAPI{
ctx: context.Background(),
clientCtx: clientCtx,
@@ -71,6 +92,11 @@ func NewPublicEthAPI(
return api
}
// ClientCtx returns client context
func (e *PublicEthAPI) ClientCtx() client.Context {
return e.clientCtx
}
// ProtocolVersion returns the supported Ethereum protocol version.
func (e *PublicEthAPI) ProtocolVersion() hexutil.Uint {
e.logger.Debugln("eth_protocolVersion")
+193
View File
@@ -0,0 +1,193 @@
package rpc
import (
"context"
"fmt"
"os"
"time"
"github.com/cosmos/ethermint/crypto/hd"
ethermint "github.com/cosmos/ethermint/types"
log "github.com/xlab/suplog"
sdkcrypto "github.com/cosmos/cosmos-sdk/crypto"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
rpctypes "github.com/cosmos/ethermint/ethereum/rpc/types"
)
// PrivateAccountAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec.
type PrivateAccountAPI struct {
ethAPI *PublicEthAPI
logger log.Logger
}
// NewPersonalAPI creates an instance of the public Personal Eth API.
func NewPersonalAPI(ethAPI *PublicEthAPI) *PrivateAccountAPI {
return &PrivateAccountAPI{
ethAPI: ethAPI,
logger: log.WithField("module", "personal"),
}
}
// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory.
// The name of the key will have the format "personal_<length-keys>", where <length-keys> is the total number of
// keys stored on the keyring.
//
// NOTE: The key will be both armored and encrypted using the same passphrase.
func (api *PrivateAccountAPI) ImportRawKey(privkey, password string) (common.Address, error) {
api.logger.Debug("personal_importRawKey")
priv, err := crypto.HexToECDSA(privkey)
if err != nil {
return common.Address{}, err
}
privKey := &ethsecp256k1.PrivKey{Key: crypto.FromECDSA(priv)}
addr := sdk.AccAddress(privKey.PubKey().Address().Bytes())
ethereumAddr := common.BytesToAddress(addr)
// return if the key has already been imported
if _, err := api.ethAPI.ClientCtx().Keyring.KeyByAddress(addr); err == nil {
return ethereumAddr, nil
}
// ignore error as we only care about the length of the list
list, _ := api.ethAPI.ClientCtx().Keyring.List()
privKeyName := fmt.Sprintf("personal_%d", len(list))
armor := sdkcrypto.EncryptArmorPrivKey(privKey, password, ethsecp256k1.KeyType)
if err := api.ethAPI.ClientCtx().Keyring.ImportPrivKey(privKeyName, armor, password); err != nil {
return common.Address{}, err
}
api.logger.Info("key successfully imported", "name", privKeyName, "address", ethereumAddr.String())
return ethereumAddr, nil
}
// ListAccounts will return a list of addresses for accounts this node manages.
func (api *PrivateAccountAPI) ListAccounts() ([]common.Address, error) {
api.logger.Debug("personal_listAccounts")
addrs := []common.Address{}
list, err := api.ethAPI.ClientCtx().Keyring.List()
if err != nil {
return nil, err
}
for _, info := range list {
addrs = append(addrs, common.BytesToAddress(info.GetPubKey().Address()))
}
return addrs, nil
}
// LockAccount will lock the account associated with the given address when it's unlocked.
// It removes the key corresponding to the given address from the API's local keys.
func (api *PrivateAccountAPI) LockAccount(address common.Address) bool {
api.logger.Debugln("personal_lockAccount", "address", address.String())
api.logger.Info("personal_lockAccount not supported")
return false
}
// NewAccount will create a new account and returns the address for the new account.
func (api *PrivateAccountAPI) NewAccount(password string) (common.Address, error) {
api.logger.Debug("personal_newAccount")
name := "key_" + time.Now().UTC().Format(time.RFC3339)
// create the mnemonic and save the account
info, _, err := api.ethAPI.ClientCtx().Keyring.NewMnemonic(name, keyring.English, ethermint.BIP44HDPath, hd.EthSecp256k1)
if err != nil {
return common.Address{}, err
}
addr := common.BytesToAddress(info.GetPubKey().Address().Bytes())
api.logger.Infoln("Your new key was generated", "address", addr.String())
api.logger.Infoln("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermint/"+name)
api.logger.Infoln("Please remember your password!")
return addr, nil
}
// UnlockAccount will unlock the account associated with the given address with
// the given password for duration seconds. If duration is nil it will use a
// default of 300 seconds. It returns an indication if the account was unlocked.
// It exports the private key corresponding to the given address from the keyring and stores it in the API's local keys.
func (api *PrivateAccountAPI) UnlockAccount(_ context.Context, addr common.Address, password string, _ *uint64) (bool, error) { // nolint: interfacer
api.logger.Debugln("personal_unlockAccount", "address", addr.String())
api.logger.Info("personal_unlockAccount not supported")
return false, fmt.Errorf("not supported")
}
// SendTransaction will create a transaction from the given arguments and
// tries to sign it with the key associated with args.To. If the given password isn't
// able to decrypt the key it fails.
func (api *PrivateAccountAPI) SendTransaction(_ context.Context, args rpctypes.SendTxArgs, pwrd string) (common.Hash, error) {
return api.ethAPI.SendTransaction(args)
}
// Sign calculates an Ethereum ECDSA signature for:
// keccak256("\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 (api *PrivateAccountAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, pwrd string) (hexutil.Bytes, error) {
api.logger.Debug("personal_sign", "data", data, "address", addr.String())
cosmosAddr := sdk.AccAddress(addr.Bytes())
sig, _, err := api.ethAPI.ClientCtx().Keyring.SignByAddress(cosmosAddr, accounts.TextHash(data))
if err != nil {
api.logger.WithError(err).Debugln("failed to sign with key", "data", data, "address", addr.String())
return nil, err
}
sig[crypto.RecoveryIDOffset] += 27 // transform V from 0/1 to 27/28
return sig, nil
}
// EcRecover returns the address for the account that was used to create the signature.
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers
// the address of:
// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
// addr = ecrecover(hash, signature)
//
// Note, the signature must conform to the secp256k1 curve R, S and V values, where
// the V value must be 27 or 28 for legacy reasons.
//
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove
func (api *PrivateAccountAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) {
api.logger.Debug("personal_ecRecover", "data", data, "sig", sig)
if len(sig) != crypto.SignatureLength {
return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength)
}
if sig[crypto.RecoveryIDOffset] != 27 && sig[crypto.RecoveryIDOffset] != 28 {
return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
}
sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
pubkey, err := crypto.SigToPub(accounts.TextHash(data), sig)
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(*pubkey), nil
}
-12
View File
@@ -1,7 +1,6 @@
package types
import (
"bytes"
"context"
"encoding/hex"
"fmt"
@@ -17,7 +16,6 @@ import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
"github.com/cosmos/ethermint/crypto/ethsecp256k1"
ethermint "github.com/cosmos/ethermint/types"
evmtypes "github.com/cosmos/ethermint/x/evm/types"
@@ -198,16 +196,6 @@ func FormatBlock(
}
}
// GetKeyByAddress returns the private key matching the given address. If not found it returns false.
func GetKeyByAddress(keys []ethsecp256k1.PrivKey, address common.Address) (key *ethsecp256k1.PrivKey, exist bool) {
for _, key := range keys {
if bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) {
return &key, true
}
}
return nil, false
}
// BuildEthereumTx builds and signs a Cosmos transaction from a MsgEthereumTx and returns the tx
func BuildEthereumTx(clientCtx client.Context, msg *evmtypes.MsgEthereumTx, accNumber, seq uint64, privKey cryptotypes.PrivKey) ([]byte, error) {
// TODO: user defined evm coin