Skip to content

Commit e843878

Browse files
RiccardoMAlessio Tregliaalessio
authored
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 <[email protected]> Co-authored-by: Alessio Treglia <[email protected]> Co-authored-by: Alessio Treglia <[email protected]>
1 parent 0cc39cc commit e843878

File tree

5 files changed

+123
-2
lines changed

5 files changed

+123
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
4949
* Updated gRPC dependency to v1.33.2
5050
* Updated iavl dependency to v0.15-rc2
5151
* (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.
52+
* (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
5253

5354
### State Machine Breaking Changes
5455
* (x/upgrade) [\#7979](https://github.com/cosmos/cosmos-sdk/pull/7979) keeper pubkey storage serialization migration from bech32 to protobuf.

x/genutil/client/cli/init.go

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cli
22

33
import (
4+
"bufio"
45
"encoding/json"
56
"fmt"
67
"os"
78
"path/filepath"
89

10+
"github.com/cosmos/go-bip39"
911
"github.com/pkg/errors"
1012
"github.com/spf13/cobra"
1113
cfg "github.com/tendermint/tendermint/config"
@@ -16,6 +18,7 @@ import (
1618

1719
"github.com/cosmos/cosmos-sdk/client"
1820
"github.com/cosmos/cosmos-sdk/client/flags"
21+
"github.com/cosmos/cosmos-sdk/client/input"
1922
"github.com/cosmos/cosmos-sdk/server"
2023
sdk "github.com/cosmos/cosmos-sdk/types"
2124
"github.com/cosmos/cosmos-sdk/types/module"
@@ -25,6 +28,9 @@ import (
2528
const (
2629
// FlagOverwrite defines a flag to overwrite an existing genesis JSON file.
2730
FlagOverwrite = "overwrite"
31+
32+
// FlagSeed defines a flag to initialize the private validator key from a specific seed.
33+
FlagRecover = "recover"
2834
)
2935

3036
type printInfo struct {
@@ -78,7 +84,22 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command {
7884
chainID = fmt.Sprintf("test-chain-%v", tmrand.Str(6))
7985
}
8086

81-
nodeID, _, err := genutil.InitializeNodeValidatorFiles(config)
87+
// Get bip39 mnemonic
88+
var mnemonic string
89+
recover, _ := cmd.Flags().GetBool(FlagRecover)
90+
if recover {
91+
inBuf := bufio.NewReader(cmd.InOrStdin())
92+
mnemonic, err := input.GetString("Enter your bip39 mnemonic", inBuf)
93+
if err != nil {
94+
return err
95+
}
96+
97+
if !bip39.IsMnemonicValid(mnemonic) {
98+
return errors.New("invalid mnemonic")
99+
}
100+
}
101+
102+
nodeID, _, err := genutil.InitializeNodeValidatorFilesFromMnemonic(config, mnemonic)
82103
if err != nil {
83104
return err
84105
}
@@ -124,6 +145,7 @@ func InitCmd(mbm module.BasicManager, defaultNodeHome string) *cobra.Command {
124145

125146
cmd.Flags().String(cli.HomeFlag, defaultNodeHome, "node's home directory")
126147
cmd.Flags().BoolP(FlagOverwrite, "o", false, "overwrite the genesis.json file")
148+
cmd.Flags().Bool(FlagRecover, false, "provide seed phrase to recover existing key instead of creating")
127149
cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created")
128150

129151
return cmd

x/genutil/client/cli/init_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
2222
"github.com/cosmos/cosmos-sdk/server"
2323
"github.com/cosmos/cosmos-sdk/server/mock"
24+
"github.com/cosmos/cosmos-sdk/testutil"
2425
sdk "github.com/cosmos/cosmos-sdk/types"
2526
"github.com/cosmos/cosmos-sdk/types/module"
2627
"github.com/cosmos/cosmos-sdk/x/genutil"
@@ -86,6 +87,37 @@ func TestInitCmd(t *testing.T) {
8687

8788
}
8889

90+
func TestInitRecover(t *testing.T) {
91+
home := t.TempDir()
92+
logger := log.NewNopLogger()
93+
cfg, err := genutiltest.CreateDefaultTendermintConfig(home)
94+
require.NoError(t, err)
95+
96+
serverCtx := server.NewContext(viper.New(), cfg, logger)
97+
interfaceRegistry := types.NewInterfaceRegistry()
98+
marshaler := codec.NewProtoCodec(interfaceRegistry)
99+
clientCtx := client.Context{}.
100+
WithJSONMarshaler(marshaler).
101+
WithLegacyAmino(makeCodec()).
102+
WithHomeDir(home)
103+
104+
ctx := context.Background()
105+
ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx)
106+
ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx)
107+
108+
cmd := genutilcli.InitCmd(testMbm, home)
109+
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)
110+
111+
cmd.SetArgs([]string{
112+
"appnode-test",
113+
fmt.Sprintf("--%s=true", genutilcli.FlagRecover),
114+
})
115+
116+
// use valid mnemonic and complete recovery key generation successfully
117+
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")
118+
require.NoError(t, cmd.ExecuteContext(ctx))
119+
}
120+
89121
func TestEmptyState(t *testing.T) {
90122
home := t.TempDir()
91123
logger := log.NewNopLogger()

x/genutil/utils.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package genutil
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"path/filepath"
67
"time"
78

9+
"github.com/cosmos/go-bip39"
810
cfg "github.com/tendermint/tendermint/config"
11+
tmed25519 "github.com/tendermint/tendermint/crypto/ed25519"
912
tmos "github.com/tendermint/tendermint/libs/os"
1013
"github.com/tendermint/tendermint/p2p"
1114
"github.com/tendermint/tendermint/privval"
@@ -48,6 +51,16 @@ func ExportGenesisFileWithTime(
4851

4952
// InitializeNodeValidatorFiles creates private validator and p2p configuration files.
5053
func InitializeNodeValidatorFiles(config *cfg.Config) (nodeID string, valPubKey cryptotypes.PubKey, err error) {
54+
return InitializeNodeValidatorFilesFromMnemonic(config, "")
55+
}
56+
57+
// InitializeNodeValidatorFiles creates private validator and p2p configuration files using the given mnemonic.
58+
// If no valid mnemonic is given, a random one will be used instead.
59+
func InitializeNodeValidatorFilesFromMnemonic(config *cfg.Config, mnemonic string) (nodeID string, valPubKey cryptotypes.PubKey, err error) {
60+
if len(mnemonic) > 0 && !bip39.IsMnemonicValid(mnemonic) {
61+
return "", nil, fmt.Errorf("invalid mnemonic")
62+
}
63+
5164
nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile())
5265
if err != nil {
5366
return "", nil, err
@@ -65,7 +78,15 @@ func InitializeNodeValidatorFiles(config *cfg.Config) (nodeID string, valPubKey
6578
return "", nil, err
6679
}
6780

68-
tmValPubKey, err := privval.LoadOrGenFilePV(pvKeyFile, pvStateFile).GetPubKey()
81+
var filePV *privval.FilePV
82+
if len(mnemonic) == 0 {
83+
filePV = privval.LoadOrGenFilePV(pvKeyFile, pvStateFile)
84+
} else {
85+
privKey := tmed25519.GenPrivKeyFromSecret([]byte(mnemonic))
86+
filePV = privval.NewFilePV(privKey, pvKeyFile, pvStateFile)
87+
}
88+
89+
tmValPubKey, err := filePV.GetPubKey()
6990
if err != nil {
7091
return "", nil, err
7192
}

x/genutil/utils_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package genutil
22

33
import (
44
"encoding/json"
5+
"os"
56
"path/filepath"
67
"testing"
78
"time"
89

910
"github.com/stretchr/testify/require"
11+
"github.com/tendermint/tendermint/config"
1012
)
1113

1214
func TestExportGenesisFileWithTime(t *testing.T) {
@@ -16,3 +18,46 @@ func TestExportGenesisFileWithTime(t *testing.T) {
1618

1719
require.NoError(t, ExportGenesisFileWithTime(fname, "test", nil, json.RawMessage(`{"account_owner": "Bob"}`), time.Now()))
1820
}
21+
22+
func TestInitializeNodeValidatorFilesFromMnemonic(t *testing.T) {
23+
t.Parallel()
24+
25+
cfg := config.TestConfig()
26+
cfg.RootDir = t.TempDir()
27+
require.NoError(t, os.MkdirAll(filepath.Join(cfg.RootDir, "config"), 0755))
28+
29+
tests := []struct {
30+
name string
31+
mnemonic string
32+
expError bool
33+
}{
34+
{
35+
name: "invalid mnemonic returns error",
36+
mnemonic: "side video kiss hotel essence",
37+
expError: true,
38+
},
39+
{
40+
name: "empty mnemonic does not return error",
41+
mnemonic: "",
42+
expError: false,
43+
},
44+
{
45+
name: "valid mnemonic does not return error",
46+
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",
47+
expError: false,
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
tt := tt
53+
t.Run(tt.name, func(t *testing.T) {
54+
_, _, err := InitializeNodeValidatorFilesFromMnemonic(cfg, tt.mnemonic)
55+
56+
if tt.expError {
57+
require.Error(t, err)
58+
} else {
59+
require.NoError(t, err)
60+
}
61+
})
62+
}
63+
}

0 commit comments

Comments
 (0)