From e8438789200696e7e9226e53c5b5d63b7740b6f1 Mon Sep 17 00:00:00 2001 From: Riccardo Montagnin Date: Tue, 8 Dec 2020 18:22:43 +0100 Subject: [PATCH] Recover private validator key (#8099) * Recover private validator key Added the ability to recover the private validator key from a given mnemonic when initializing the node * Added CHANGELOG entry * Reverted dependencies * Added tests * Fixed mnemonic checking as suggested and added tests * Run make format and fixed go.sum checksum * Run make format * fix imports * fix TestInitializeNodeValidatorFilesFromMnemonic * Update CHANGELOG.md Co-authored-by: Alessio Treglia Co-authored-by: Alessio Treglia Co-authored-by: Alessio Treglia --- CHANGELOG.md | 1 + x/genutil/client/cli/init.go | 24 ++++++++++++++++- x/genutil/client/cli/init_test.go | 32 ++++++++++++++++++++++ x/genutil/utils.go | 23 +++++++++++++++- x/genutil/utils_test.go | 45 +++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99d256e7b061..15b01df779a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Updated gRPC dependency to v1.33.2 * Updated iavl dependency to v0.15-rc2 * (version) [\#7848](https://github.com/cosmos/cosmos-sdk/pull/7848) [\#7941](https://github.com/cosmos/cosmos-sdk/pull/7941) `version --long` output now shows the list of build dependencies and replaced build dependencies. +* (x/genutil) [\#8099](https://github.com/cosmos/cosmos-sdk/pull/8099) `init` now supports a `--recover` flag to recover the private validator key from a given mnemonic ### State Machine Breaking Changes * (x/upgrade) [\#7979](https://github.com/cosmos/cosmos-sdk/pull/7979) keeper pubkey storage serialization migration from bech32 to protobuf. diff --git a/x/genutil/client/cli/init.go b/x/genutil/client/cli/init.go index 60eb18deed29..074bb7da4e74 100644 --- a/x/genutil/client/cli/init.go +++ b/x/genutil/client/cli/init.go @@ -1,11 +1,13 @@ package cli import ( + "bufio" "encoding/json" "fmt" "os" "path/filepath" + "github.com/cosmos/go-bip39" "github.com/pkg/errors" "github.com/spf13/cobra" cfg "github.com/tendermint/tendermint/config" @@ -16,6 +18,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/input" "github.com/cosmos/cosmos-sdk/server" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" @@ -25,6 +28,9 @@ import ( const ( // FlagOverwrite defines a flag to overwrite an existing genesis JSON file. FlagOverwrite = "overwrite" + + // FlagSeed defines a flag to initialize the private validator key from a specific seed. + FlagRecover = "recover" ) type printInfo struct { @@ -78,7 +84,22 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command { chainID = fmt.Sprintf("test-chain-%v", tmrand.Str(6)) } - nodeID, _, err := genutil.InitializeNodeValidatorFiles(config) + // Get bip39 mnemonic + var mnemonic string + recover, _ := cmd.Flags().GetBool(FlagRecover) + if recover { + inBuf := bufio.NewReader(cmd.InOrStdin()) + mnemonic, err := input.GetString("Enter your bip39 mnemonic", inBuf) + if err != nil { + return err + } + + if !bip39.IsMnemonicValid(mnemonic) { + return errors.New("invalid mnemonic") + } + } + + nodeID, _, err := genutil.InitializeNodeValidatorFilesFromMnemonic(config, mnemonic) if err != nil { return err } @@ -124,6 +145,7 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command { cmd.Flags().String(cli.HomeFlag, defaultNodeHome, "node's home directory") cmd.Flags().BoolP(FlagOverwrite, "o", false, "overwrite the genesis.json file") + cmd.Flags().Bool(FlagRecover, false, "provide seed phrase to recover existing key instead of creating") cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") return cmd diff --git a/x/genutil/client/cli/init_test.go b/x/genutil/client/cli/init_test.go index 213a20e0028c..80820052dfa1 100644 --- a/x/genutil/client/cli/init_test.go +++ b/x/genutil/client/cli/init_test.go @@ -21,6 +21,7 @@ import ( cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/server/mock" + "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/genutil" @@ -86,6 +87,37 @@ func TestInitCmd(t *testing.T) { } +func TestInitRecover(t *testing.T) { + home := t.TempDir() + logger := log.NewNopLogger() + cfg, err := genutiltest.CreateDefaultTendermintConfig(home) + require.NoError(t, err) + + serverCtx := server.NewContext(viper.New(), cfg, logger) + interfaceRegistry := types.NewInterfaceRegistry() + marshaler := codec.NewProtoCodec(interfaceRegistry) + clientCtx := client.Context{}. + WithJSONMarshaler(marshaler). + WithLegacyAmino(makeCodec()). + WithHomeDir(home) + + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx) + + cmd := genutilcli.InitCmd(testMbm, home) + mockIn := testutil.ApplyMockIODiscardOutErr(cmd) + + cmd.SetArgs([]string{ + "appnode-test", + fmt.Sprintf("--%s=true", genutilcli.FlagRecover), + }) + + // use valid mnemonic and complete recovery key generation successfully + mockIn.Reset("decide praise business actor peasant farm drastic weather extend front hurt later song give verb rhythm worry fun pond reform school tumble august one\n") + require.NoError(t, cmd.ExecuteContext(ctx)) +} + func TestEmptyState(t *testing.T) { home := t.TempDir() logger := log.NewNopLogger() diff --git a/x/genutil/utils.go b/x/genutil/utils.go index 14ac11f533ac..4c46bdb6f121 100644 --- a/x/genutil/utils.go +++ b/x/genutil/utils.go @@ -2,10 +2,13 @@ package genutil import ( "encoding/json" + "fmt" "path/filepath" "time" + "github.com/cosmos/go-bip39" cfg "github.com/tendermint/tendermint/config" + tmed25519 "github.com/tendermint/tendermint/crypto/ed25519" tmos "github.com/tendermint/tendermint/libs/os" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/privval" @@ -48,6 +51,16 @@ func ExportGenesisFileWithTime( // InitializeNodeValidatorFiles creates private validator and p2p configuration files. func InitializeNodeValidatorFiles(config *cfg.Config) (nodeID string, valPubKey cryptotypes.PubKey, err error) { + return InitializeNodeValidatorFilesFromMnemonic(config, "") +} + +// InitializeNodeValidatorFiles creates private validator and p2p configuration files using the given mnemonic. +// If no valid mnemonic is given, a random one will be used instead. +func InitializeNodeValidatorFilesFromMnemonic(config *cfg.Config, mnemonic string) (nodeID string, valPubKey cryptotypes.PubKey, err error) { + if len(mnemonic) > 0 && !bip39.IsMnemonicValid(mnemonic) { + return "", nil, fmt.Errorf("invalid mnemonic") + } + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) if err != nil { return "", nil, err @@ -65,7 +78,15 @@ func InitializeNodeValidatorFiles(config *cfg.Config) (nodeID string, valPubKey return "", nil, err } - tmValPubKey, err := privval.LoadOrGenFilePV(pvKeyFile, pvStateFile).GetPubKey() + var filePV *privval.FilePV + if len(mnemonic) == 0 { + filePV = privval.LoadOrGenFilePV(pvKeyFile, pvStateFile) + } else { + privKey := tmed25519.GenPrivKeyFromSecret([]byte(mnemonic)) + filePV = privval.NewFilePV(privKey, pvKeyFile, pvStateFile) + } + + tmValPubKey, err := filePV.GetPubKey() if err != nil { return "", nil, err } diff --git a/x/genutil/utils_test.go b/x/genutil/utils_test.go index d1a80d58c1c0..a845cc68cd96 100644 --- a/x/genutil/utils_test.go +++ b/x/genutil/utils_test.go @@ -2,11 +2,13 @@ package genutil import ( "encoding/json" + "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/config" ) func TestExportGenesisFileWithTime(t *testing.T) { @@ -16,3 +18,46 @@ func TestExportGenesisFileWithTime(t *testing.T) { require.NoError(t, ExportGenesisFileWithTime(fname, "test", nil, json.RawMessage(`{"account_owner": "Bob"}`), time.Now())) } + +func TestInitializeNodeValidatorFilesFromMnemonic(t *testing.T) { + t.Parallel() + + cfg := config.TestConfig() + cfg.RootDir = t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(cfg.RootDir, "config"), 0755)) + + tests := []struct { + name string + mnemonic string + expError bool + }{ + { + name: "invalid mnemonic returns error", + mnemonic: "side video kiss hotel essence", + expError: true, + }, + { + name: "empty mnemonic does not return error", + mnemonic: "", + expError: false, + }, + { + name: "valid mnemonic does not return error", + mnemonic: "side video kiss hotel essence door angle student degree during vague adjust submit trick globe muscle frozen vacuum artwork million shield bind useful wave", + expError: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + _, _, err := InitializeNodeValidatorFilesFromMnemonic(cfg, tt.mnemonic) + + if tt.expError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +}