Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: key rename cli command #9601

Merged
merged 29 commits into from
Jul 19, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
238dbe2
added key renaming
technicallyty Jun 28, 2021
46762a6
add cli cmds
technicallyty Jun 28, 2021
950752a
changelog
technicallyty Jun 28, 2021
4368e14
gofmt
technicallyty Jun 28, 2021
8283b67
move changelog
technicallyty Jun 28, 2021
9acfb00
fix root_test
technicallyty Jun 29, 2021
16b6632
Apply suggestions from code review
technicallyty Jul 5, 2021
ec01a52
Merge branch 'master' into ty/9407-rename_keys_cli
technicallyty Jul 5, 2021
26ec3a5
add equality function
technicallyty Jul 5, 2021
9a2ba38
Update crypto/keyring/keyring.go
technicallyty Jul 6, 2021
2c7395b
move passphrase to const defs
technicallyty Jul 6, 2021
588c082
refactor test
technicallyty Jul 6, 2021
ed51363
allow duplicate keys as long as names are different
technicallyty Jul 6, 2021
8d464aa
Merge branch 'master' into ty/9407-rename_keys_cli
technicallyty Jul 6, 2021
fc19867
lint
technicallyty Jul 6, 2021
c6aed98
Update CHANGELOG.md
technicallyty Jul 8, 2021
3c1e9b9
Update crypto/keyring/keyring.go
technicallyty Jul 8, 2021
4044320
Merge branch 'master' into ty/9407-rename_keys_cli
technicallyty Jul 9, 2021
e4d2a3f
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 10, 2021
2d563b2
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 12, 2021
1a9347c
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 12, 2021
102169b
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 12, 2021
e3cca96
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 12, 2021
3865a9e
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 13, 2021
aae59f9
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 13, 2021
e6f8508
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 13, 2021
e5e35f6
Merge branch 'master' into ty/9407-rename_keys_cli
mergify[bot] Jul 17, 2021
4edd229
Merge branch 'master' into ty/9407-rename_keys_cli
technicallyty Jul 18, 2021
c21bd5f
Merge branch 'master' into ty/9407-rename_keys_cli
amaury1093 Jul 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [\#9427](https://github.com/cosmos/cosmos-sdk/pull/9427) Move simapp `FundAccount` and `FundModuleAccount` to `x/bank/testutil`
* (client/tx) [\#9421](https://github.com/cosmos/cosmos-sdk/pull/9421/) `BuildUnsignedTx`, `BuildSimTx`, `PrintUnsignedStdTx` functions are moved to
the Tx Factory as methods.
* (client/keys) [\#9407](https://github.com/cosmos/cosmos-sdk/pull/9601) Added a new CLI command and Keyring interface function, `./simd keys rename [old_name] [new_name]` to rename a key in the keyring.
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
* (x/slashing) [\#9458](https://github.com/cosmos/cosmos-sdk/pull/9458) Coins burned from slashing is now returned from Slash function and included in Slash event.
* [\#9246](https://github.com/cosmos/cosmos-sdk/pull/9246) The `New` method for the network package now returns an error.
* (codec) [\#9521](https://github.com/cosmos/cosmos-sdk/pull/9521) Removed deprecated `clientCtx.JSONCodec` from `client.Context`.
Expand Down
67 changes: 67 additions & 0 deletions client/keys/rename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package keys

import (
"bufio"
"fmt"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/input"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/spf13/cobra"
)

// RenameKeyCommand renames a key from the key store.
func RenameKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "rename <old_name> <new_name>",
Short: "Rename an existing key",
Long: `Rename a key from the Keybase backend.

Note that renaming offline or ledger keys will rename
only the public key references stored locally, i.e.
private keys stored in a ledger device cannot be renamed with the CLI.
`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
buf := bufio.NewReader(cmd.InOrStdin())
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

oldName, newName := args[0], args[1]

info, err := clientCtx.Keyring.Key(oldName)
if err != nil {
return err
}

// confirm rename, unless -y is passed
if skip, _ := cmd.Flags().GetBool(flagYes); !skip {
prompt := fmt.Sprintf("Key reference will be renamed from %s to %s. Continue?", args[0], args[1])
if yes, err := input.GetConfirmation(prompt, buf, cmd.ErrOrStderr()); err != nil {
return err
} else if !yes {
return nil
}
}

if err := clientCtx.Keyring.Rename(oldName, newName); err != nil {
return err
}

if info.GetType() == keyring.TypeLedger || info.GetType() == keyring.TypeOffline {
cmd.PrintErrln("Public key reference renamed")
return nil
}

cmd.PrintErrln(fmt.Sprintf("Key was successfully renamed from %s to %s", oldName, newName))

return nil
},
}

cmd.Flags().BoolP(flagYes, "y", false, "Skip confirmation prompt when renaming offline or ledger key references")

return cmd
}
98 changes: 98 additions & 0 deletions client/keys/rename_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package keys

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func Test_runRenameCmd(t *testing.T) {
// temp keybase
kbHome := t.TempDir()
cmd := RenameKeyCommand()
cmd.Flags().AddFlagSet(Commands(kbHome).PersistentFlags())
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)

yesF, _ := cmd.Flags().GetBool(flagYes)
require.False(t, yesF)

fakeKeyName1 := "runRenameCmd_Key1"
fakeKeyName2 := "runRenameCmd_Key2"

path := sdk.GetConfig().GetFullBIP44Path()

kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
require.NoError(t, err)

// put fakeKeyName1 in keyring
_, err = kb.NewAccount(fakeKeyName1, testutil.TestMnemonic, "", path, hd.Secp256k1)
require.NoError(t, err)

clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)

ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)

// rename a key 'blah' which doesnt exist
cmd.SetArgs([]string{"blah", "blaah", fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome)})
err = cmd.ExecuteContext(ctx)
require.Error(t, err)
require.EqualError(t, err, "blah.info: key not found")

// User confirmation missing
cmd.SetArgs([]string{
fakeKeyName1,
"nokey",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})
err = cmd.Execute()
require.Error(t, err)
require.Equal(t, "EOF", err.Error())

oldKey, err := kb.Key(fakeKeyName1)
require.NoError(t, err)

// add a confirmation
cmd.SetArgs([]string{
fakeKeyName1,
fakeKeyName2,
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=true", flagYes),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})
require.NoError(t, cmd.Execute())

// key1 is gone
_, err = kb.Key(fakeKeyName1)
require.Error(t, err)

// key2 exists now
renamedKey, err := kb.Key(fakeKeyName2)
require.NoError(t, err)

require.Equal(t, oldKey.GetPubKey(), renamedKey.GetPubKey())
require.Equal(t, oldKey.GetType(), renamedKey.GetType())
require.Equal(t, oldKey.GetAddress(), renamedKey.GetAddress())
require.Equal(t, oldKey.GetAlgo(), renamedKey.GetAlgo())

// try to rename key1 but it doesnt exist anymore so error
cmd.SetArgs([]string{
fakeKeyName1,
fakeKeyName2,
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=true", flagYes),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})
require.Error(t, cmd.Execute())
}
1 change: 1 addition & 0 deletions client/keys/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The pass backend requires GnuPG: https://gnupg.org/
ListKeysCmd(),
ShowKeysCmd(),
DeleteKeyCommand(),
RenameKeyCommand(),
ParseKeyStringCommand(),
MigrateCommand(),
)
Expand Down
2 changes: 1 addition & 1 deletion client/keys/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ func TestCommands(t *testing.T) {
assert.NotNil(t, rootCommands)

// Commands are registered
assert.Equal(t, 9, len(rootCommands.Commands()))
assert.Equal(t, 10, len(rootCommands.Commands()))
}
50 changes: 44 additions & 6 deletions crypto/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (
keyringFileDirName = "keyring-file"
keyringTestDirName = "keyring-test"
passKeyringPrefix = "keyring-%s"

// temporary pass phrase for exporting a key during a key rename
passPhrase = "temp"
)

var (
Expand All @@ -64,6 +67,9 @@ type Keyring interface {
Delete(uid string) error
DeleteByAddress(address sdk.Address) error

// Rename an existing key from the Keyring
Rename(from string, to string) error

// NewMnemonic generates a new mnemonic, derives a hierarchical deterministic key from it, and
// persists the key to storage. Returns the generated mnemonic and the key Info.
// It returns an error if it fails to generate a key for the given algo type, or if
Expand Down Expand Up @@ -288,8 +294,10 @@ func (ks keystore) ExportPrivKeyArmorByAddress(address sdk.Address, encryptPassp
}

func (ks keystore) ImportPrivKey(uid, armor, passphrase string) error {
if _, err := ks.Key(uid); err == nil {
return fmt.Errorf("cannot overwrite key: %s", uid)
if k, err := ks.Key(uid); err == nil {
if uid == k.GetName() {
return fmt.Errorf("cannot overwrite key: %s", uid)
}
}

privKey, algo, err := crypto.UnarmorDecryptPrivKey(armor, passphrase)
Expand Down Expand Up @@ -426,6 +434,30 @@ func (ks keystore) DeleteByAddress(address sdk.Address) error {
return nil
}

func (ks keystore) Rename(oldName, newName string) error {
_, err := ks.Key(newName)
if err == nil {
return fmt.Errorf("rename failed: %s already exists in the keyring", newName)
}

armor, err := ks.ExportPrivKeyArmor(oldName, passPhrase)
if err != nil {
return err
}

err = ks.ImportPrivKey(newName, armor, passPhrase)
if err != nil {
return err
}

err = ks.Delete(oldName)
if err != nil {
return err
}

return nil
}

func (ks keystore) Delete(uid string) error {
info, err := ks.Key(uid)
if err != nil {
Expand Down Expand Up @@ -785,14 +817,20 @@ func (ks keystore) writeInfo(info Info) error {
// existsInDb returns true if key is in DB. Error is returned only when we have error
// different thant ErrKeyNotFound
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
func (ks keystore) existsInDb(info Info) (bool, error) {
if _, err := ks.db.Get(addrHexKeyAsString(info.GetAddress())); err == nil {
return true, nil // address lookup succeeds - info exists
if item, err := ks.db.Get(addrHexKeyAsString(info.GetAddress())); err == nil {
if item.Key == info.GetName() {
return true, nil // address lookup succeeds - info exists
}
return false, nil
} else if err != keyring.ErrKeyNotFound {
return false, err // received unexpected error - returns error
}

if _, err := ks.db.Get(infoKey(info.GetName())); err == nil {
return true, nil // uid lookup succeeds - info exists
if item, err := ks.db.Get(infoKey(info.GetName())); err == nil {
if item.Key == info.GetName() {
return true, nil // uid lookup succeeds - info exists
}
return false, nil
} else if err != keyring.ErrKeyNotFound {
return false, err // received unexpected error - returns
}
Expand Down
85 changes: 85 additions & 0 deletions crypto/keyring/keyring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,72 @@ func TestBackendConfigConstructors(t *testing.T) {
require.Equal(t, "keyring-test", backend.PassPrefix)
}

func TestRenameKey(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each test, should we check more than just an error? Should we also check whether each key does or does not exist within the keyring after renaming?

Copy link
Contributor Author

@technicallyty technicallyty Jul 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also check whether each key does or does not exist within the keyring after renaming?

this is already checked on L1175 and L1182-L1183

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within this test, rename is called three times at the end without checking the specific error thrown or what keys exist after each failed attempt. I was thinking the extra checks would be added to the failed attempts. At the least, I think we should check to make sure we are receiving the expected error and not just any error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored the tests. should be a bit more concise. let me know if its missing anything please!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I like the direction with adding test cases. I'm not sure if it's necessary given that the only parameter for each test case is the run function and there is no overlap with the key names you are using so creating a new keyring for each test case might not be necessary but I also think it's ok to leave it as is.

testCases := []struct {
name string
run func(Keyring)
}{
{
name: "rename a key",
run: func(kr Keyring) {
oldKeyUID, newKeyUID := "old", "new"
oldKeyInfo := newKeyInfo(t, kr, oldKeyUID)
err := kr.Rename(oldKeyUID, newKeyUID) // rename from "old" to "new"
require.NoError(t, err)
newInfo, err := kr.Key(newKeyUID) // new key should be in keyring
require.NoError(t, err)
requireEqualRenamedKey(t, newInfo, oldKeyInfo) // oldinfo and newinfo should be the same
oldKeyInfo, err = kr.Key(oldKeyUID) // old key should be gone from keyring
require.Error(t, err)
},
},
{
name: "cant rename a key that doesnt exist",
run: func(kr Keyring) {
err := kr.Rename("bogus", "bogus2")
require.Error(t, err)
},
},
{
name: "cant rename a key to an already existing key name",
run: func(kr Keyring) {
key1, key2 := "existingKey", "existingKey2" // create 2 keys
newKeyInfo(t, kr, key1)
newKeyInfo(t, kr, key2)
err := kr.Rename(key2, key1)
require.Equal(t, fmt.Errorf("rename failed: %s already exists in the keyring", key1), err)
assertKeysExist(t, kr, key1, key2) // keys should still exist after failed rename
},
},
{
name: "cant rename key to itself",
run: func(kr Keyring) {
keyName := "keyName"
newKeyInfo(t, kr, keyName)
err := kr.Rename(keyName, keyName)
require.Equal(t, fmt.Errorf("rename failed: %s already exists in the keyring", keyName), err)
assertKeysExist(t, kr, keyName)
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
},
},
}

for _, tc := range testCases {
tc := tc
kr := newKeyring(t, "testKeyring")
t.Run(tc.name, func(t *testing.T) {
tc.run(kr)
})
}
}

func requireEqualRenamedKey(t *testing.T, newKey, oldKey Info) {
require.NotEqual(t, newKey.GetName(), oldKey.GetName())
require.Equal(t, newKey.GetAddress(), oldKey.GetAddress())
require.Equal(t, newKey.GetPubKey(), oldKey.GetPubKey())
require.Equal(t, newKey.GetAlgo(), oldKey.GetAlgo())
require.Equal(t, newKey.GetType(), oldKey.GetType())
}

func requireEqualInfo(t *testing.T, key Info, mnemonic Info) {
require.Equal(t, key.GetName(), mnemonic.GetName())
require.Equal(t, key.GetAddress(), mnemonic.GetAddress())
Expand All @@ -1161,4 +1227,23 @@ func requireEqualInfo(t *testing.T, key Info, mnemonic Info) {
require.Equal(t, key.GetType(), mnemonic.GetType())
}

func newKeyring(t *testing.T, name string) Keyring {
kr, err := New(name, "test", t.TempDir(), nil)
require.NoError(t, err)
return kr
}
technicallyty marked this conversation as resolved.
Show resolved Hide resolved

func newKeyInfo(t *testing.T, kr Keyring, name string) Info {
keyInfo, _, err := kr.NewMnemonic(name, English, sdk.FullFundraiserPath, DefaultBIP39Passphrase, hd.Secp256k1)
require.NoError(t, err)
return keyInfo
}

func assertKeysExist(t *testing.T, kr Keyring, names ...string) {
for _, n := range names {
_, err := kr.Key(n)
require.NoError(t, err)
}
}

func accAddr(info Info) sdk.AccAddress { return info.GetAddress() }