Skip to content

Commit

Permalink
Recover private validator key (#8099)
Browse files Browse the repository at this point in the history
* 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 <quadrispro@ubuntu.com>

Co-authored-by: Alessio Treglia <alessio@tendermint.com>
Co-authored-by: Alessio Treglia <quadrispro@ubuntu.com>
  • Loading branch information
3 people committed Dec 8, 2020
1 parent 0cc39cc commit e843878
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 2 deletions.
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/
* 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.
Expand Down
24 changes: 23 additions & 1 deletion x/genutil/client/cli/init.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions x/genutil/client/cli/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
23 changes: 22 additions & 1 deletion x/genutil/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
45 changes: 45 additions & 0 deletions x/genutil/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
})
}
}

0 comments on commit e843878

Please sign in to comment.