diff --git a/gno.land/cmd/gnokey/README.md b/gno.land/cmd/gnokey/README.md index fa196ed7e51..243ba1b0fe9 100644 --- a/gno.land/cmd/gnokey/README.md +++ b/gno.land/cmd/gnokey/README.md @@ -8,6 +8,16 @@ $> cd ./gno $> make install_gnokey +## Validator keys + +Use an existing gnokey key as a validator key by exporting it in the `priv_validator_key.json` format expected by gnoland or a remote signer: + +```bash +gnokey export -key mykey -validator -output-path ./priv_validator_key.json +``` + +If no `-output-path` is provided, the key is written to `priv_validator_key.json` in the current directory. + Also, see the [quickstart guide](../../../docs/users/interact-with-gnokey.md). ## Manual Entropy Generation diff --git a/tm2/pkg/bft/privval/signer/local/key.go b/tm2/pkg/bft/privval/signer/local/key.go index feb6aebe80d..7006e589ed2 100644 --- a/tm2/pkg/bft/privval/signer/local/key.go +++ b/tm2/pkg/bft/privval/signer/local/key.go @@ -110,6 +110,22 @@ func GeneratePersistedFileKey(filePath string) (*FileKey, error) { // Generate a new random FileKey. fk := GenerateFileKey() + return PersistFileKey(filePath, fk.PrivKey) +} + +// PersistFileKey persists the given private key to disk using the FileKey format. +// The provided private key is used to derive the public key and address. +func PersistFileKey(filePath string, privKey crypto.PrivKey) (*FileKey, error) { + if privKey == nil { + return nil, errInvalidPrivateKey + } + + fk := &FileKey{ + PrivKey: privKey, + PubKey: privKey.PubKey(), + Address: privKey.PubKey().Address(), + } + // Persist the FileKey to disk. if err := fk.save(filePath); err != nil { return nil, err diff --git a/tm2/pkg/bft/privval/signer/local/key_test.go b/tm2/pkg/bft/privval/signer/local/key_test.go index 8e8dbfa186b..72435d3cd05 100644 --- a/tm2/pkg/bft/privval/signer/local/key_test.go +++ b/tm2/pkg/bft/privval/signer/local/key_test.go @@ -7,6 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -86,6 +87,33 @@ func TestSave(t *testing.T) { }) } +func TestPersistFileKey(t *testing.T) { + t.Parallel() + + t.Run("nil private key", func(t *testing.T) { + t.Parallel() + + fk, err := PersistFileKey("", nil) + require.Nil(t, fk) + assert.ErrorIs(t, err, errInvalidPrivateKey) + }) + + t.Run("persist provided key", func(t *testing.T) { + t.Parallel() + + priv := ed25519.GenPrivKey() + filePath := path.Join(t.TempDir(), "validator_key.json") + + fk, err := PersistFileKey(filePath, priv) + require.NoError(t, err) + require.NotNil(t, fk) + + loaded, err := LoadFileKey(filePath) + require.NoError(t, err) + assert.Equal(t, fk, loaded) + }) +} + func TestLoadFileKey(t *testing.T) { t.Parallel() diff --git a/tm2/pkg/crypto/keys/client/export.go b/tm2/pkg/crypto/keys/client/export.go index cad0c046f0f..ef0c7c1a7f4 100644 --- a/tm2/pkg/crypto/keys/client/export.go +++ b/tm2/pkg/crypto/keys/client/export.go @@ -7,16 +7,22 @@ import ( "fmt" "os" + "github.com/gnolang/gno/tm2/pkg/bft/privval/signer/local" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/armor" ) +const ( + defaultValidatorKeyFileName = "priv_validator_key.json" +) + type ExportCfg struct { RootCfg *BaseCfg NameOrBech32 string OutputPath string + AsValidator bool } func NewExportCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { @@ -28,7 +34,7 @@ func NewExportCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { commands.Metadata{ Name: "export", ShortUsage: "export [flags]", - ShortHelp: "exports private key armor", + ShortHelp: "exports a private key as encrypted armor or a validator key file", }, cfg, func(_ context.Context, args []string) error { @@ -49,7 +55,14 @@ func (c *ExportCfg) RegisterFlags(fs *flag.FlagSet) { &c.OutputPath, "output-path", "", - "the desired output path for the armor file", + "the desired output path for the armor file or validator key", + ) + + fs.BoolVar( + &c.AsValidator, + "validator", + false, + "export the key as a validator private key file (priv_validator_key.json)", ) } @@ -89,6 +102,24 @@ func execExport(cfg *ExportCfg, io commands.IO) error { return fmt.Errorf("unable to export private key, %w", err) } + // If exporting as a validator key, persist it in the priv_validator_key.json format. + if cfg.AsValidator { + outputPath := cfg.OutputPath + if outputPath == "" { + outputPath = defaultValidatorKeyFileName + } + + fk, err := local.PersistFileKey(outputPath, privateKey) + if err != nil { + return fmt.Errorf("unable to write validator key, %w", err) + } + + io.Printfln("Validator private key saved at %s", outputPath) + io.Printfln("Validator address: %s", fk.Address) + + return nil + } + // Get the armor encrypt password pw, err := promptPassphrase(io, cfg.RootCfg.InsecurePasswordStdin) if err != nil { diff --git a/tm2/pkg/crypto/keys/client/export_test.go b/tm2/pkg/crypto/keys/client/export_test.go index e7c7f3ef42d..45fa6e3915d 100644 --- a/tm2/pkg/crypto/keys/client/export_test.go +++ b/tm2/pkg/crypto/keys/client/export_test.go @@ -4,13 +4,16 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "testing" + "github.com/gnolang/gno/tm2/pkg/bft/privval/signer/local" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // newTestKeybase generates a new test key-base @@ -71,7 +74,8 @@ type testCmdKeyOptsBase struct { type testExportKeyOpts struct { testCmdKeyOptsBase - outputPath string + outputPath string + asValidator bool } // exportKey runs the private key export command @@ -89,6 +93,7 @@ func exportKey( }, NameOrBech32: exportOpts.keyName, OutputPath: exportOpts.outputPath, + AsValidator: exportOpts.asValidator, } cmdIO := commands.NewTestIO() @@ -198,6 +203,45 @@ func TestExport_ExportKey(t *testing.T) { } } +func TestExport_AsValidatorKey(t *testing.T) { + t.Parallel() + + const ( + keyName = "validator-key" + password = "password" + ) + + // Generate a temporary key-base directory + kb, kbHome := newTestKeybase(t) + + // Add an initial key to the key base + info, err := addRandomKeyToKeybase(kb, keyName, password) + require.NoError(t, err) + + outputPath := filepath.Join(t.TempDir(), "priv_validator_key.json") + + assert.NoError( + t, + exportKey( + testExportKeyOpts{ + testCmdKeyOptsBase: testCmdKeyOptsBase{ + kbHome: kbHome, + keyName: info.GetName(), + }, + outputPath: outputPath, + asValidator: true, + }, + strings.NewReader(fmt.Sprintf("%s\n", password)), + ), + ) + + fileKey, err := local.LoadFileKey(outputPath) + require.NoError(t, err) + + assert.Equal(t, info.GetPubKey(), fileKey.PubKey) + assert.Equal(t, info.GetAddress(), fileKey.Address) +} + func TestExport_ExportKeyWithEmptyName(t *testing.T) { t.Parallel()