Skip to content
This repository was archived by the owner on Nov 30, 2021. It is now read-only.

Commit 811ca7f

Browse files
authored
rpc: implement personal_importRawKey (#552)
1 parent 592eca9 commit 811ca7f

File tree

6 files changed

+112
-89
lines changed

6 files changed

+112
-89
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
3737

3838
## Unreleased
3939

40+
### Features
41+
42+
* (rpc) [\#552](https://github.com/ChainSafe/ethermint/pull/552) Implement Eth Personal namespace `personal_importRawKey`.
43+
4044
### Bug fixes
4145

4246
* (keys) [\#554](https://github.com/ChainSafe/ethermint/pull/554) Fix private key derivation.

crypto/algorithm.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import (
1818
)
1919

2020
const (
21+
// EthSecp256k1Type string constant for the EthSecp256k1 algorithm
22+
EthSecp256k1Type = "eth_secp256k1"
2123
// EthSecp256k1 defines the ECDSA secp256k1 used on Ethereum
22-
EthSecp256k1 = keys.SigningAlgo("eth_secp256k1")
24+
EthSecp256k1 = keys.SigningAlgo(EthSecp256k1Type)
2325
)
2426

2527
// SupportedAlgorithms defines the list of signing algorithms used on Ethermint:

rpc/apis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func GetRPCAPIs(cliCtx context.CLIContext, keys []emintcrypto.PrivKeySecp256k1)
4040
{
4141
Namespace: PersonalNamespace,
4242
Version: apiVersion,
43-
Service: NewPersonalEthAPI(cliCtx, ethAPI, nonceLock, keys),
43+
Service: NewPersonalEthAPI(ethAPI),
4444
Public: false,
4545
},
4646
{

rpc/eth_api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type PublicEthAPI struct {
4646
chainIDEpoch *big.Int
4747
logger log.Logger
4848
backend Backend
49-
keys []crypto.PrivKeySecp256k1
49+
keys []crypto.PrivKeySecp256k1 // unlocked keys
5050
nonceLock *AddrLocker
5151
keybaseLock sync.Mutex
5252
}

rpc/personal_api.go

Lines changed: 82 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,34 @@ import (
55
"context"
66
"fmt"
77
"os"
8-
"sync"
98
"time"
109

11-
sdkcontext "github.com/cosmos/cosmos-sdk/client/context"
10+
"github.com/spf13/viper"
11+
1212
"github.com/cosmos/cosmos-sdk/client/flags"
1313
"github.com/cosmos/cosmos-sdk/crypto/keys"
14+
"github.com/cosmos/cosmos-sdk/crypto/keys/mintkey"
1415
sdk "github.com/cosmos/cosmos-sdk/types"
15-
emintcrypto "github.com/cosmos/ethermint/crypto"
16-
params "github.com/cosmos/ethermint/rpc/args"
17-
"github.com/spf13/viper"
18-
"github.com/tendermint/tendermint/libs/log"
1916

2017
"github.com/ethereum/go-ethereum/accounts"
2118
"github.com/ethereum/go-ethereum/common"
2219
"github.com/ethereum/go-ethereum/common/hexutil"
2320
"github.com/ethereum/go-ethereum/crypto"
21+
22+
emintcrypto "github.com/cosmos/ethermint/crypto"
23+
params "github.com/cosmos/ethermint/rpc/args"
2424
)
2525

26-
// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
26+
// PersonalEthAPI is the personal_ prefixed set of APIs in the Web3 JSON-RPC spec.
2727
type PersonalEthAPI struct {
28-
logger log.Logger
29-
cliCtx sdkcontext.CLIContext
30-
ethAPI *PublicEthAPI
31-
nonceLock *AddrLocker
32-
keys []emintcrypto.PrivKeySecp256k1
33-
keyInfos []keys.Info
34-
keybaseLock sync.Mutex
28+
ethAPI *PublicEthAPI
29+
keyInfos []keys.Info // all keys, both locked and unlocked. unlocked keys are stored in ethAPI.keys
3530
}
3631

37-
// NewPersonalEthAPI creates an instance of the public ETH Web3 API.
38-
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext, ethAPI *PublicEthAPI, nonceLock *AddrLocker, keys []emintcrypto.PrivKeySecp256k1) *PersonalEthAPI {
32+
// NewPersonalEthAPI creates an instance of the public Personal Eth API.
33+
func NewPersonalEthAPI(ethAPI *PublicEthAPI) *PersonalEthAPI {
3934
api := &PersonalEthAPI{
40-
logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "json-rpc"),
41-
cliCtx: cliCtx,
42-
ethAPI: ethAPI,
43-
nonceLock: nonceLock,
44-
keys: keys,
35+
ethAPI: ethAPI,
4536
}
4637

4738
infos, err := api.getKeybaseInfo()
@@ -54,43 +45,68 @@ func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext, ethAPI *PublicEthAPI, nonce
5445
}
5546

5647
func (e *PersonalEthAPI) getKeybaseInfo() ([]keys.Info, error) {
57-
e.keybaseLock.Lock()
58-
defer e.keybaseLock.Unlock()
48+
e.ethAPI.keybaseLock.Lock()
49+
defer e.ethAPI.keybaseLock.Unlock()
5950

60-
if e.cliCtx.Keybase == nil {
51+
if e.ethAPI.cliCtx.Keybase == nil {
6152
keybase, err := keys.NewKeyring(
6253
sdk.KeyringServiceName(),
6354
viper.GetString(flags.FlagKeyringBackend),
6455
viper.GetString(flags.FlagHome),
65-
e.cliCtx.Input,
56+
e.ethAPI.cliCtx.Input,
6657
emintcrypto.EthSecp256k1Options()...,
6758
)
6859
if err != nil {
6960
return nil, err
7061
}
7162

72-
e.cliCtx.Keybase = keybase
63+
e.ethAPI.cliCtx.Keybase = keybase
7364
}
7465

75-
return e.cliCtx.Keybase.List()
66+
return e.ethAPI.cliCtx.Keybase.List()
7667
}
7768

78-
// ImportRawKey stores the given hex encoded ECDSA key into the key directory,
79-
// encrypting it with the passphrase.
80-
// Currently, this is not implemented since the feature is not supported by the keys.
69+
// ImportRawKey armors and encrypts a given raw hex encoded ECDSA key and stores it into the key directory.
70+
// The name of the key will have the format "personal_<length-keys>", where <length-keys> is the total number of
71+
// keys stored on the keyring.
72+
// NOTE: The key will be both armored and encrypted using the same passphrase.
8173
func (e *PersonalEthAPI) ImportRawKey(privkey, password string) (common.Address, error) {
82-
e.logger.Debug("personal_importRawKey", "error", "not implemented")
83-
_, err := crypto.HexToECDSA(privkey)
74+
e.ethAPI.logger.Debug("personal_importRawKey")
75+
priv, err := crypto.HexToECDSA(privkey)
8476
if err != nil {
8577
return common.Address{}, err
8678
}
8779

88-
return common.Address{}, nil
80+
privKey := emintcrypto.PrivKeySecp256k1(crypto.FromECDSA(priv))
81+
82+
armor := mintkey.EncryptArmorPrivKey(privKey, password, emintcrypto.EthSecp256k1Type)
83+
84+
// ignore error as we only care about the length of the list
85+
list, _ := e.ethAPI.cliCtx.Keybase.List()
86+
privKeyName := fmt.Sprintf("personal_%d", len(list))
87+
88+
if err := e.ethAPI.cliCtx.Keybase.ImportPrivKey(privKeyName, armor, password); err != nil {
89+
return common.Address{}, err
90+
}
91+
92+
addr := common.BytesToAddress(privKey.PubKey().Address().Bytes())
93+
94+
info, err := e.ethAPI.cliCtx.Keybase.Get(privKeyName)
95+
if err != nil {
96+
return common.Address{}, err
97+
}
98+
99+
// append key and info to be able to lock and list the account
100+
//e.ethAPI.keys = append(e.ethAPI.keys, privKey)
101+
e.keyInfos = append(e.keyInfos, info)
102+
e.ethAPI.logger.Info("key successfully imported", "name", privKeyName, "address", addr.String())
103+
104+
return addr, nil
89105
}
90106

91107
// ListAccounts will return a list of addresses for accounts this node manages.
92108
func (e *PersonalEthAPI) ListAccounts() ([]common.Address, error) {
93-
e.logger.Debug("personal_listAccounts")
109+
e.ethAPI.logger.Debug("personal_listAccounts")
94110
addrs := []common.Address{}
95111
for _, info := range e.keyInfos {
96112
addressBytes := info.GetPubKey().Address().Bytes()
@@ -103,18 +119,7 @@ func (e *PersonalEthAPI) ListAccounts() ([]common.Address, error) {
103119
// LockAccount will lock the account associated with the given address when it's unlocked.
104120
// It removes the key corresponding to the given address from the API's local keys.
105121
func (e *PersonalEthAPI) LockAccount(address common.Address) bool {
106-
e.logger.Debug("personal_lockAccount", "address", address)
107-
for i, key := range e.keys {
108-
if !bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) {
109-
continue
110-
}
111-
112-
tmp := make([]emintcrypto.PrivKeySecp256k1, len(e.keys)-1)
113-
copy(tmp[:i], e.keys[:i])
114-
copy(tmp[i:], e.keys[i+1:])
115-
e.keys = tmp
116-
return true
117-
}
122+
e.ethAPI.logger.Debug("personal_lockAccount", "address", address.String())
118123

119124
for i, key := range e.ethAPI.keys {
120125
if !bytes.Equal(key.PubKey().Address().Bytes(), address.Bytes()) {
@@ -125,6 +130,8 @@ func (e *PersonalEthAPI) LockAccount(address common.Address) bool {
125130
copy(tmp[:i], e.ethAPI.keys[:i])
126131
copy(tmp[i:], e.ethAPI.keys[i+1:])
127132
e.ethAPI.keys = tmp
133+
134+
e.ethAPI.logger.Debug("account unlocked", "address", address.String())
128135
return true
129136
}
130137

@@ -133,62 +140,55 @@ func (e *PersonalEthAPI) LockAccount(address common.Address) bool {
133140

134141
// NewAccount will create a new account and returns the address for the new account.
135142
func (e *PersonalEthAPI) NewAccount(password string) (common.Address, error) {
136-
e.logger.Debug("personal_newAccount")
143+
e.ethAPI.logger.Debug("personal_newAccount")
137144
_, err := e.getKeybaseInfo()
138145
if err != nil {
139146
return common.Address{}, err
140147
}
141148

142149
name := "key_" + time.Now().UTC().Format(time.RFC3339)
143-
info, _, err := e.cliCtx.Keybase.CreateMnemonic(name, keys.English, password, emintcrypto.EthSecp256k1)
150+
info, _, err := e.ethAPI.cliCtx.Keybase.CreateMnemonic(name, keys.English, password, emintcrypto.EthSecp256k1)
144151
if err != nil {
145152
return common.Address{}, err
146153
}
147154

148155
e.keyInfos = append(e.keyInfos, info)
149156

150-
// update ethAPI
151-
privKey, err := e.cliCtx.Keybase.ExportPrivateKeyObject(name, password)
152-
if err != nil {
153-
return common.Address{}, err
154-
}
155-
156-
emintKey, ok := privKey.(emintcrypto.PrivKeySecp256k1)
157-
if !ok {
158-
return common.Address{}, fmt.Errorf("invalid private key type: %T", privKey)
159-
}
160-
e.ethAPI.keys = append(e.ethAPI.keys, emintKey)
161-
e.logger.Debug("personal_newAccount", "address", fmt.Sprintf("0x%x", emintKey.PubKey().Address().Bytes()))
162-
163157
addr := common.BytesToAddress(info.GetPubKey().Address().Bytes())
164-
e.logger.Info("Your new key was generated", "address", addr)
165-
e.logger.Info("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermintcli/"+name)
166-
e.logger.Info("Please remember your password!")
158+
e.ethAPI.logger.Info("Your new key was generated", "address", addr.String())
159+
e.ethAPI.logger.Info("Please backup your key file!", "path", os.Getenv("HOME")+"/.ethermintcli/"+name)
160+
e.ethAPI.logger.Info("Please remember your password!")
167161
return addr, nil
168162
}
169163

170164
// UnlockAccount will unlock the account associated with the given address with
171165
// the given password for duration seconds. If duration is nil it will use a
172166
// default of 300 seconds. It returns an indication if the account was unlocked.
173167
// It exports the private key corresponding to the given address from the keyring and stores it in the API's local keys.
174-
func (e *PersonalEthAPI) UnlockAccount(ctx context.Context, addr common.Address, password string, _ *uint64) (bool, error) {
175-
e.logger.Debug("personal_unlockAccount", "address", addr)
168+
func (e *PersonalEthAPI) UnlockAccount(_ context.Context, addr common.Address, password string, _ *uint64) (bool, error) { // nolint: interfacer
169+
e.ethAPI.logger.Debug("personal_unlockAccount", "address", addr.String())
176170
// TODO: use duration
177171

178-
name := ""
172+
var keyInfo keys.Info
173+
179174
for _, info := range e.keyInfos {
180175
addressBytes := info.GetPubKey().Address().Bytes()
181176
if bytes.Equal(addressBytes, addr[:]) {
182-
name = info.GetName()
177+
keyInfo = info
178+
break
183179
}
184180
}
185181

186-
if name == "" {
187-
return false, fmt.Errorf("cannot find key with given address")
182+
if keyInfo == nil {
183+
return false, fmt.Errorf("cannot find key with given address %s", addr.String())
188184
}
189185

190-
// TODO: this only works on local keys
191-
privKey, err := e.cliCtx.Keybase.ExportPrivateKeyObject(name, password)
186+
// exporting private key only works on local keys
187+
if keyInfo.GetType() != keys.TypeLocal {
188+
return false, fmt.Errorf("key type must be %s, got %s", keys.TypeLedger.String(), keyInfo.GetType().String())
189+
}
190+
191+
privKey, err := e.ethAPI.cliCtx.Keybase.ExportPrivateKeyObject(keyInfo.GetName(), password)
192192
if err != nil {
193193
return false, err
194194
}
@@ -198,17 +198,15 @@ func (e *PersonalEthAPI) UnlockAccount(ctx context.Context, addr common.Address,
198198
return false, fmt.Errorf("invalid private key type: %T", privKey)
199199
}
200200

201-
e.keys = append(e.keys, emintKey)
202201
e.ethAPI.keys = append(e.ethAPI.keys, emintKey)
203-
e.logger.Debug("personal_unlockAccount", "address", fmt.Sprintf("0x%x", emintKey.PubKey().Address().Bytes()))
204-
202+
e.ethAPI.logger.Debug("account unlocked", "address", addr.String())
205203
return true, nil
206204
}
207205

208206
// SendTransaction will create a transaction from the given arguments and
209207
// tries to sign it with the key associated with args.To. If the given password isn't
210208
// able to decrypt the key it fails.
211-
func (e *PersonalEthAPI) SendTransaction(ctx context.Context, args params.SendTxArgs, passwd string) (common.Hash, error) {
209+
func (e *PersonalEthAPI) SendTransaction(_ context.Context, args params.SendTxArgs, _ string) (common.Hash, error) {
212210
return e.ethAPI.SendTransaction(args)
213211
}
214212

@@ -221,12 +219,12 @@ func (e *PersonalEthAPI) SendTransaction(ctx context.Context, args params.SendTx
221219
// The key used to calculate the signature is decrypted with the given password.
222220
//
223221
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
224-
func (e *PersonalEthAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) {
225-
e.logger.Debug("personal_sign", "data", data, "address", addr)
222+
func (e *PersonalEthAPI) Sign(_ context.Context, data hexutil.Bytes, addr common.Address, _ string) (hexutil.Bytes, error) {
223+
e.ethAPI.logger.Debug("personal_sign", "data", data, "address", addr.String())
226224

227-
key, ok := checkKeyInKeyring(e.keys, addr)
225+
key, ok := checkKeyInKeyring(e.ethAPI.keys, addr)
228226
if !ok {
229-
return nil, fmt.Errorf("cannot find key with given address")
227+
return nil, fmt.Errorf("cannot find key with address %s", addr.String())
230228
}
231229

232230
sig, err := crypto.Sign(accounts.TextHash(data), key.ToECDSA())
@@ -248,8 +246,8 @@ func (e *PersonalEthAPI) Sign(ctx context.Context, data hexutil.Bytes, addr comm
248246
// the V value must be 27 or 28 for legacy reasons.
249247
//
250248
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecove
251-
func (e *PersonalEthAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
252-
e.logger.Debug("personal_ecRecover", "data", data, "sig", sig)
249+
func (e *PersonalEthAPI) EcRecover(_ context.Context, data, sig hexutil.Bytes) (common.Address, error) {
250+
e.ethAPI.logger.Debug("personal_ecRecover", "data", data, "sig", sig)
253251

254252
if len(sig) != crypto.SignatureLength {
255253
return common.Address{}, fmt.Errorf("signature must be %d bytes long", crypto.SignatureLength)
@@ -259,9 +257,9 @@ func (e *PersonalEthAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes)
259257
}
260258
sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
261259

262-
rpk, err := crypto.SigToPub(accounts.TextHash(data), sig)
260+
pubkey, err := crypto.SigToPub(accounts.TextHash(data), sig)
263261
if err != nil {
264262
return common.Address{}, err
265263
}
266-
return crypto.PubkeyToAddress(*rpk), nil
264+
return crypto.PubkeyToAddress(*pubkey), nil
267265
}

tests/personal_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/ethereum/go-ethereum/common"
88
"github.com/ethereum/go-ethereum/common/hexutil"
9+
ethcrypto "github.com/ethereum/go-ethereum/crypto"
910

1011
"github.com/stretchr/testify/require"
1112
)
@@ -42,6 +43,24 @@ func TestPersonal_Sign(t *testing.T) {
4243
// TODO: check that signature is same as with geth, requires importing a key
4344
}
4445

46+
func TestPersonal_ImportRawKey(t *testing.T) {
47+
privkey, err := ethcrypto.GenerateKey()
48+
require.NoError(t, err)
49+
50+
// parse priv key to hex
51+
hexPriv := common.Bytes2Hex(ethcrypto.FromECDSA(privkey))
52+
rpcRes := call(t, "personal_importRawKey", []string{hexPriv, "password"})
53+
54+
var res hexutil.Bytes
55+
err = json.Unmarshal(rpcRes.Result, &res)
56+
require.NoError(t, err)
57+
58+
addr := ethcrypto.PubkeyToAddress(privkey.PublicKey)
59+
resAddr := common.BytesToAddress(res)
60+
61+
require.Equal(t, addr.String(), resAddr.String())
62+
}
63+
4564
func TestPersonal_EcRecover(t *testing.T) {
4665
data := hexutil.Bytes{0x88}
4766
rpcRes := call(t, "personal_sign", []interface{}{data, hexutil.Bytes(from), ""})
@@ -67,7 +86,7 @@ func TestPersonal_UnlockAccount(t *testing.T) {
6786

6887
// try to sign, should be locked
6988
_, err = callWithError("personal_sign", []interface{}{hexutil.Bytes{0x88}, addr, ""})
70-
require.NotNil(t, err)
89+
require.Error(t, err)
7190

7291
rpcRes = call(t, "personal_unlockAccount", []interface{}{addr, ""})
7392
var unlocked bool
@@ -104,5 +123,5 @@ func TestPersonal_LockAccount(t *testing.T) {
104123

105124
// try to sign, should be locked
106125
_, err = callWithError("personal_sign", []interface{}{hexutil.Bytes{0x88}, addr, ""})
107-
require.NotNil(t, err)
126+
require.Error(t, err)
108127
}

0 commit comments

Comments
 (0)