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 9 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()))
}
28 changes: 28 additions & 0 deletions crypto/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,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 @@ -426,6 +429,31 @@ 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 errors.New(fmt.Sprintf("rename failed: %s already exists in the keyring", newName))
technicallyty marked this conversation as resolved.
Show resolved Hide resolved
}

passPhrase := "temp"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this should be a private const

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup that makes much more sense - moved it to the other private const vars

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

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

Choose a reason for hiding this comment

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

Should this be called after ImportPrivKey? What happens if ImportPrivKey fails? The account is deleted and no new account is created.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so import will actually fail if you delete last, cause import doesn't allow same pubkeys. some solutions i can think of:

  1. dump the key information in a recovery file

  2. update the ImportPrivKey method to allow keys with same pubkeys, but different keyring names

thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the name is the primary index, I think it's OK to go with (2).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

alright, changed the write behavior to allow duplicate keys, as long as the names are different.


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

return nil
}

func (ks keystore) Delete(uid string) error {
info, err := ks.Key(uid)
if err != nil {
Expand Down
53 changes: 53 additions & 0 deletions crypto/keyring/keyring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,59 @@ 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.

kb, err := New("keybasename", "test", t.TempDir(), nil)
require.NoError(t, err)

oldKeyUID, newKeyUID := "oldName", "newName"

// create key with "oldName"
_, _, err = kb.NewMnemonic(oldKeyUID, English, sdk.FullFundraiserPath, DefaultBIP39Passphrase, hd.Secp256k1)
require.NoError(t, err)

oldKey, err := kb.Key(oldKeyUID)
require.NoError(t, err)
require.Equal(t, oldKeyUID, oldKey.GetName())

// basic rename
err = kb.Rename(oldKeyUID, newKeyUID)
require.NoError(t, err)

// "newName" now has "oldName"'s info
newKey, err := kb.Key(newKeyUID)

// check that keyinfo is the same, except for their names
requireEqualRenamedKey(t, newKey, oldKey)

// oldName should be deleted
oldKey, err = kb.Key(oldKeyUID)
require.Error(t, err)

// can't rename from a non-existent key 'foo'
err = kb.Rename("foo", "bar")
require.Error(t, err)

// create new key
_, _, err = kb.NewMnemonic("tester", English, sdk.FullFundraiserPath, DefaultBIP39Passphrase, hd.Secp256k1)
require.NoError(t, err)

// renaming a key to an existing name should fail
err = kb.Rename("tester", newKeyUID)
require.Error(t, err)

// rename to itself should fail
err = kb.Rename(newKeyUID, newKeyUID)
require.Error(t, err)
}

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 Down