From 00a36d80f36c41d5725f50742f45c683b124cdd8 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 00:17:52 +0100 Subject: [PATCH 01/18] Add secrets init all --- gno.land/Makefile | 6 +- gno.land/cmd/secrets/common.go | 84 ++++++++++++++++++++++++++++ gno.land/cmd/secrets/init.go | 26 +++++++++ gno.land/cmd/secrets/init_all.go | 90 ++++++++++++++++++++++++++++++ gno.land/cmd/secrets/main.go | 94 ++++++++++++++++++++++++++++++++ tm2/pkg/bft/privval/file.go | 10 ++-- 6 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 gno.land/cmd/secrets/common.go create mode 100644 gno.land/cmd/secrets/init.go create mode 100644 gno.land/cmd/secrets/init_all.go create mode 100644 gno.land/cmd/secrets/main.go diff --git a/gno.land/Makefile b/gno.land/Makefile index 8138dfcfe3e..50e21e7050b 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -17,7 +17,7 @@ start.gnoland:; go run ./cmd/gnoland start start.gnoweb:; go run ./cmd/gnoweb .PHONY: build -build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync build.genesis +build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync build.genesis build.secrets build.gnoland:; go build -o build/gnoland ./cmd/gnoland build.gnoweb:; go build -o build/gnoweb ./cmd/gnoweb @@ -25,12 +25,13 @@ build.gnofaucet:; go build -o build/gnofaucet ./cmd/gnofaucet build.gnokey:; go build -o build/gnokey ./cmd/gnokey build.gnotxsync:; go build -o build/gnotxsync ./cmd/gnotxsync build.genesis:; go build -o build/genesis ./cmd/genesis +build.secrets:; go build -o build/secrets ./cmd/secrets run.gnoland:; go run ./cmd/gnoland start run.gnoweb:; go run ./cmd/gnoweb .PHONY: install -install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync install.genesis +install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync install.genesis install.secrets install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb @@ -38,6 +39,7 @@ install.gnofaucet:; go install ./cmd/gnofaucet install.gnokey:; go install ./cmd/gnokey install.gnotxsync:; go install ./cmd/gnotxsync install.genesis:; go install ./cmd/genesis +install.secrets:; go install ./cmd/secrets .PHONY: fclean fclean: clean diff --git a/gno.land/cmd/secrets/common.go b/gno.land/cmd/secrets/common.go new file mode 100644 index 00000000000..f0054e6ba6d --- /dev/null +++ b/gno.land/cmd/secrets/common.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/p2p" +) + +// generateValidatorPrivateKey generates the validator's private key +func generateValidatorPrivateKey() privval.FilePVKey { + privKey := ed25519.GenPrivKey() + + return privval.FilePVKey{ + Address: privKey.PubKey().Address(), + PubKey: privKey.PubKey(), + PrivKey: privKey, + } +} + +// saveValidatorPrivateKey saves the validator's private key to the given path +func saveValidatorPrivateKey(key privval.FilePVKey, path string) error { + // Get Amino JSON + marshalledKey, err := amino.MarshalJSONIndent(key, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal validator private key into JSON, %w", err) + } + + // Save the key to disk + if err := os.WriteFile(path, marshalledKey, 0o644); err != nil { + return fmt.Errorf("unable to save validator private key, %w", err) + } + + return nil +} + +// generateLastSignValidatorState generates the empty last sign state +func generateLastSignValidatorState() privval.FilePVLastSignState { + return privval.FilePVLastSignState{} // Empty last sign state +} + +// saveLastSignValidatorState saves the last sign validator state to the given path +func saveLastSignValidatorState(state privval.FilePVLastSignState, path string) error { + // Get Amino JSON + marshalledState, err := amino.MarshalJSONIndent(state, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal last validator sign state into JSON, %w", err) + } + + // Save the sign state to disk + if err := os.WriteFile(path, marshalledState, 0o644); err != nil { + return fmt.Errorf("unable to save last validator sign state, %w", err) + } + + return nil +} + +// generateNodeKey generates the p2p node key +func generateNodeKey() *p2p.NodeKey { + privKey := ed25519.GenPrivKey() + + return &p2p.NodeKey{ + PrivKey: privKey, + } +} + +// saveNodeKey saves the node key to the given path +func saveNodeKey(key *p2p.NodeKey, path string) error { + // Get Amino JSON + marshalledKey, err := amino.MarshalJSON(key) + if err != nil { + return fmt.Errorf("unable to marshal node key into JSON, %w", err) + } + + // Save the sign state to disk + if err := os.WriteFile(path, marshalledKey, 0o644); err != nil { + return fmt.Errorf("unable to save node key, %w", err) + } + + return nil +} diff --git a/gno.land/cmd/secrets/init.go b/gno.land/cmd/secrets/init.go new file mode 100644 index 00000000000..2ffd97304d9 --- /dev/null +++ b/gno.land/cmd/secrets/init.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newInitCmd creates the new secrets init command +func newInitCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "init", + ShortUsage: "init [subcommand] [flags]", + ShortHelp: "Initializes the Gno node secrets", + LongHelp: "Initializes the Gno node secrets locally, including the validator key, validator state and node key", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newInitAllCmd(io), + // newInitSingleCmd(io), + ) + + return cmd +} diff --git a/gno.land/cmd/secrets/init_all.go b/gno.land/cmd/secrets/init_all.go new file mode 100644 index 00000000000..bac95ddd602 --- /dev/null +++ b/gno.land/cmd/secrets/init_all.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type initAllCfg struct { + commonAllCfg +} + +// newInitAllCmd creates the secrets init all command +func newInitAllCmd(io commands.IO) *commands.Command { + cfg := &initAllCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "all", + ShortUsage: "init all [flags]", + ShortHelp: "Initializes required Gno secrets", + LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state", + }, + cfg, + func(_ context.Context, _ []string) error { + return execInitAll(cfg, io) + }, + ) + + return cmd +} + +func (c *initAllCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonAllCfg.RegisterFlags(fs) +} + +func execInitAll(cfg *initAllCfg, io commands.IO) error { + // Check the data output directory path + if cfg.dataDir == "" { + return errInvalidDataDir + } + + // Make sure the directory is there + if err := os.MkdirAll(cfg.dataDir, 0o755); err != nil { + return fmt.Errorf("unable to create secrets dir, %w", err) + } + + // Construct the paths + var ( + validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) + validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) + nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) + ) + + // Initialize the validator's private key + privateKey := generateValidatorPrivateKey() + + // Save the key + if err := saveValidatorPrivateKey(privateKey, validatorKeyPath); err != nil { + return err + } + + io.Printfln("Validator private key saved at %s", validatorKeyPath) + + // Initialize the validator's last sign state + validatorState := generateLastSignValidatorState() + + // Save the last sign state + if err := saveLastSignValidatorState(validatorState, validatorStatePath); err != nil { + return err + } + + io.Printfln("Validator last sign state saved at %s", validatorStatePath) + + // Initialize the node's p2p key + nodeKey := generateNodeKey() + + // Save the node key + if err := saveNodeKey(nodeKey, nodeKeyPath); err != nil { + return err + } + + io.Printfln("Node key saved at %s", validatorStatePath) + + return nil +} diff --git a/gno.land/cmd/secrets/main.go b/gno.land/cmd/secrets/main.go new file mode 100644 index 00000000000..a00af6339f7 --- /dev/null +++ b/gno.land/cmd/secrets/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "errors" + "flag" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errInvalidDataDir = errors.New("invalid data directory provided") + +const ( + defaultSecretsDir = "./secrets" + defaultValidatorKeyName = "priv_validator_key.json" + defaultNodeKeyName = "node_key.json" + defaultValidatorStateName = "priv_validator_state.json" +) + +func main() { + io := commands.NewDefaultIO() + cmd := newRootCmd(io) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +// newRootCmd creates the new secrets root command +func newRootCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + LongHelp: "Gno secrets manipulation suite", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newInitCmd(io), + // newVerifyCmd(io), + // newShowCmd(io), + ) + + return cmd +} + +// commonAllCfg is the common +// configuration for secrets commands +// that require a bundled secrets dir +type commonAllCfg struct { + dataDir string +} + +func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.dataDir, + "data-dir", + defaultSecretsDir, + "the secrets output directory", + ) +} + +// commonIndividualCfg is the common +// configuration for secrets commands +// that require individual secret path management +type commonIndividualCfg struct { + validatorKeyPath string + validatorStatePath string + nodeKeyPath string +} + +func (c *commonIndividualCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.validatorKeyPath, + "validator-key-path", + "", + "the path to the validator private key", + ) + + fs.StringVar( + &c.validatorStatePath, + "validator-state-path", + "", + "the path to the last validator state", + ) + + fs.StringVar( + &c.nodeKeyPath, + "node-key-path", + "", + "the path to the node p2p key", + ) +} diff --git a/tm2/pkg/bft/privval/file.go b/tm2/pkg/bft/privval/file.go index 4af1e9e24a6..b1bac8416f7 100644 --- a/tm2/pkg/bft/privval/file.go +++ b/tm2/pkg/bft/privval/file.go @@ -35,7 +35,7 @@ func voteToStep(vote *types.Vote) int8 { } } -//------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------- // FilePVKey stores the immutable part of PrivValidator. type FilePVKey struct { @@ -63,7 +63,7 @@ func (pvKey FilePVKey) Save() { } } -//------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------- // FilePVLastSignState stores the mutable part of PrivValidator. type FilePVLastSignState struct { @@ -126,7 +126,7 @@ func (lss *FilePVLastSignState) Save() { } } -//------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------- // FilePV implements PrivValidator using data persisted to disk // to prevent double signing. @@ -273,7 +273,7 @@ func (pv *FilePV) String() string { return fmt.Sprintf("PrivValidator{%v LH:%v, LR:%v, LS:%v}", pv.GetAddress(), pv.LastSignState.Height, pv.LastSignState.Round, pv.LastSignState.Step) } -//------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------ // signVote checks if the vote is good to sign and sets the vote signature. // It may need to set the timestamp as well if the vote is otherwise the same as @@ -371,7 +371,7 @@ func (pv *FilePV) saveSigned(height int64, round int, step int8, pv.LastSignState.Save() } -//----------------------------------------------------------------------------------------- +// ----------------------------------------------------------------------------------------- // returns the timestamp from the lastSignBytes. // returns true if the only difference in the votes is their timestamp. From a248ff5535cfd468952f3c37ccec251f7cc66b65 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 00:39:27 +0100 Subject: [PATCH 02/18] Add secrets init single --- gno.land/cmd/secrets/common.go | 46 +++--------------- gno.land/cmd/secrets/init.go | 49 ++++++++++++++++++- gno.land/cmd/secrets/init_all.go | 33 +++---------- gno.land/cmd/secrets/init_single.go | 75 +++++++++++++++++++++++++++++ gno.land/cmd/secrets/main.go | 6 +-- 5 files changed, 140 insertions(+), 69 deletions(-) create mode 100644 gno.land/cmd/secrets/init_single.go diff --git a/gno.land/cmd/secrets/common.go b/gno.land/cmd/secrets/common.go index f0054e6ba6d..d0b488a3f5b 100644 --- a/gno.land/cmd/secrets/common.go +++ b/gno.land/cmd/secrets/common.go @@ -21,43 +21,11 @@ func generateValidatorPrivateKey() privval.FilePVKey { } } -// saveValidatorPrivateKey saves the validator's private key to the given path -func saveValidatorPrivateKey(key privval.FilePVKey, path string) error { - // Get Amino JSON - marshalledKey, err := amino.MarshalJSONIndent(key, "", " ") - if err != nil { - return fmt.Errorf("unable to marshal validator private key into JSON, %w", err) - } - - // Save the key to disk - if err := os.WriteFile(path, marshalledKey, 0o644); err != nil { - return fmt.Errorf("unable to save validator private key, %w", err) - } - - return nil -} - // generateLastSignValidatorState generates the empty last sign state func generateLastSignValidatorState() privval.FilePVLastSignState { return privval.FilePVLastSignState{} // Empty last sign state } -// saveLastSignValidatorState saves the last sign validator state to the given path -func saveLastSignValidatorState(state privval.FilePVLastSignState, path string) error { - // Get Amino JSON - marshalledState, err := amino.MarshalJSONIndent(state, "", " ") - if err != nil { - return fmt.Errorf("unable to marshal last validator sign state into JSON, %w", err) - } - - // Save the sign state to disk - if err := os.WriteFile(path, marshalledState, 0o644); err != nil { - return fmt.Errorf("unable to save last validator sign state, %w", err) - } - - return nil -} - // generateNodeKey generates the p2p node key func generateNodeKey() *p2p.NodeKey { privKey := ed25519.GenPrivKey() @@ -67,17 +35,17 @@ func generateNodeKey() *p2p.NodeKey { } } -// saveNodeKey saves the node key to the given path -func saveNodeKey(key *p2p.NodeKey, path string) error { +// saveDataToPath saves the given data as Amino JSON to the path +func saveDataToPath(data any, path string) error { // Get Amino JSON - marshalledKey, err := amino.MarshalJSON(key) + marshalledState, err := amino.MarshalJSONIndent(data, "", " ") if err != nil { - return fmt.Errorf("unable to marshal node key into JSON, %w", err) + return fmt.Errorf("unable to marshal data into JSON, %w", err) } - // Save the sign state to disk - if err := os.WriteFile(path, marshalledKey, 0o644); err != nil { - return fmt.Errorf("unable to save node key, %w", err) + // Save the data to disk + if err := os.WriteFile(path, marshalledState, 0o644); err != nil { + return fmt.Errorf("unable to save data to disk, %w", err) } return nil diff --git a/gno.land/cmd/secrets/init.go b/gno.land/cmd/secrets/init.go index 2ffd97304d9..3462f305fcc 100644 --- a/gno.land/cmd/secrets/init.go +++ b/gno.land/cmd/secrets/init.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -19,8 +21,53 @@ func newInitCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newInitAllCmd(io), - // newInitSingleCmd(io), + newInitSingleCmd(io), ) return cmd } + +// initAndSaveValidatorKey generates a validator private key and saves it to the given path +func initAndSaveValidatorKey(path string, io commands.IO) error { + // Initialize the validator's private key + privateKey := generateValidatorPrivateKey() + + // Save the key + if err := saveDataToPath(privateKey, path); err != nil { + return fmt.Errorf("unable to save validator key, %w", err) + } + + io.Printfln("Validator private key saved at %s", path) + + return nil +} + +// initAndSaveValidatorState generates an empty last validator sign state and saves it to the given path +func initAndSaveValidatorState(path string, io commands.IO) error { + // Initialize the validator's last sign state + validatorState := generateLastSignValidatorState() + + // Save the last sign state + if err := saveDataToPath(validatorState, path); err != nil { + return fmt.Errorf("unable to save last validator sign state, %w", err) + } + + io.Printfln("Validator last sign state saved at %s", path) + + return nil +} + +// initAndSaveNodeKey generates a node p2p key and saves it to the given path +func initAndSaveNodeKey(path string, io commands.IO) error { + // Initialize the node's p2p key + nodeKey := generateNodeKey() + + // Save the node key + if err := saveDataToPath(nodeKey, path); err != nil { + return fmt.Errorf("unable to save node p2p key, %w", err) + } + + io.Printfln("Node key saved at %s", path) + + return nil +} diff --git a/gno.land/cmd/secrets/init_all.go b/gno.land/cmd/secrets/init_all.go index bac95ddd602..e81d61b9e63 100644 --- a/gno.land/cmd/secrets/init_all.go +++ b/gno.land/cmd/secrets/init_all.go @@ -22,7 +22,7 @@ func newInitAllCmd(io commands.IO) *commands.Command { commands.Metadata{ Name: "all", ShortUsage: "init all [flags]", - ShortHelp: "Initializes required Gno secrets", + ShortHelp: "Initializes required Gno secrets in a common directory", LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state", }, cfg, @@ -56,35 +56,16 @@ func execInitAll(cfg *initAllCfg, io commands.IO) error { nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) ) - // Initialize the validator's private key - privateKey := generateValidatorPrivateKey() - - // Save the key - if err := saveValidatorPrivateKey(privateKey, validatorKeyPath); err != nil { + // Initialize and save the validator's private key + if err := initAndSaveValidatorKey(validatorKeyPath, io); err != nil { return err } - io.Printfln("Validator private key saved at %s", validatorKeyPath) - - // Initialize the validator's last sign state - validatorState := generateLastSignValidatorState() - - // Save the last sign state - if err := saveLastSignValidatorState(validatorState, validatorStatePath); err != nil { + // Initialize and save the validator's last sign state + if err := initAndSaveValidatorState(validatorStatePath, io); err != nil { return err } - io.Printfln("Validator last sign state saved at %s", validatorStatePath) - - // Initialize the node's p2p key - nodeKey := generateNodeKey() - - // Save the node key - if err := saveNodeKey(nodeKey, nodeKeyPath); err != nil { - return err - } - - io.Printfln("Node key saved at %s", validatorStatePath) - - return nil + // Initialize and save the node's p2p key + return initAndSaveNodeKey(nodeKeyPath, io) } diff --git a/gno.land/cmd/secrets/init_single.go b/gno.land/cmd/secrets/init_single.go new file mode 100644 index 00000000000..091af064d72 --- /dev/null +++ b/gno.land/cmd/secrets/init_single.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "errors" + "flag" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errNoOutputSet = errors.New("no individual output path set") + +type initSingleCfg struct { + commonSingleCfg +} + +// newInitSingleCmd creates the secrets init single command +func newInitSingleCmd(io commands.IO) *commands.Command { + cfg := &initSingleCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "single", + ShortUsage: "init single [flags]", + ShortHelp: "Initializes required Gno secrets individually", + LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state" + + " at custom paths", + }, + cfg, + func(_ context.Context, _ []string) error { + return execInitSingle(cfg, io) + }, + ) + + return cmd +} + +func (c *initSingleCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonSingleCfg.RegisterFlags(fs) +} + +func execInitSingle(cfg *initSingleCfg, io commands.IO) error { + var ( + validatorKeyPathSet = cfg.validatorKeyPath != "" + validatorStatePathSet = cfg.validatorStatePath != "" + nodeKeyPathSet = cfg.nodeKeyPath != "" + ) + + if !validatorKeyPathSet && !validatorStatePathSet && !nodeKeyPathSet { + return errNoOutputSet + } + + // Save the validator private key, if any + if validatorKeyPathSet { + if err := initAndSaveValidatorKey(cfg.validatorKeyPath, io); err != nil { + return err + } + } + + // Save the last validator sign state, if any + if validatorStatePathSet { + if err := initAndSaveValidatorState(cfg.validatorStatePath, io); err != nil { + return err + } + } + + // Save the node key, if any + if nodeKeyPathSet { + if err := initAndSaveNodeKey(cfg.nodeKeyPath, io); err != nil { + return err + } + } + + return nil +} diff --git a/gno.land/cmd/secrets/main.go b/gno.land/cmd/secrets/main.go index a00af6339f7..44b181eff1f 100644 --- a/gno.land/cmd/secrets/main.go +++ b/gno.land/cmd/secrets/main.go @@ -61,16 +61,16 @@ func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) { ) } -// commonIndividualCfg is the common +// commonSingleCfg is the common // configuration for secrets commands // that require individual secret path management -type commonIndividualCfg struct { +type commonSingleCfg struct { validatorKeyPath string validatorStatePath string nodeKeyPath string } -func (c *commonIndividualCfg) RegisterFlags(fs *flag.FlagSet) { +func (c *commonSingleCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.validatorKeyPath, "validator-key-path", From d21b713d05c31ffb7237d748a0bd1b07d73fb985 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 10:46:10 +0100 Subject: [PATCH 03/18] Add secrets init unit tests --- gno.land/cmd/secrets/common.go | 4 +- gno.land/cmd/secrets/init_all_test.go | 109 +++++++++++++++++++++++ gno.land/cmd/secrets/init_single_test.go | 87 ++++++++++++++++++ 3 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 gno.land/cmd/secrets/init_all_test.go create mode 100644 gno.land/cmd/secrets/init_single_test.go diff --git a/gno.land/cmd/secrets/common.go b/gno.land/cmd/secrets/common.go index d0b488a3f5b..34fb157f644 100644 --- a/gno.land/cmd/secrets/common.go +++ b/gno.land/cmd/secrets/common.go @@ -38,13 +38,13 @@ func generateNodeKey() *p2p.NodeKey { // saveDataToPath saves the given data as Amino JSON to the path func saveDataToPath(data any, path string) error { // Get Amino JSON - marshalledState, err := amino.MarshalJSONIndent(data, "", " ") + marshalledData, err := amino.MarshalJSONIndent(data, "", " ") if err != nil { return fmt.Errorf("unable to marshal data into JSON, %w", err) } // Save the data to disk - if err := os.WriteFile(path, marshalledState, 0o644); err != nil { + if err := os.WriteFile(path, marshalledData, 0o644); err != nil { return fmt.Errorf("unable to save data to disk, %w", err) } diff --git a/gno.land/cmd/secrets/init_all_test.go b/gno.land/cmd/secrets/init_all_test.go new file mode 100644 index 00000000000..0c97af6a879 --- /dev/null +++ b/gno.land/cmd/secrets/init_all_test.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSecrets_Init_All(t *testing.T) { + t.Parallel() + + t.Run("invalid data directory", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "init", + "all", + "--data-dir", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidDataDir.Error()) + }) + + t.Run("all secrets initialized", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "init", + "all", + "--data-dir", + tempDir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the validator key is saved + validateValidatorKey(t, filepath.Join(tempDir, defaultValidatorKeyName)) + + // Verify the last sign validator state is saved + validateValidatorState(t, filepath.Join(tempDir, defaultValidatorStateName)) + + // Verify the node p2p key is saved + validateNodeKey(t, filepath.Join(tempDir, defaultNodeKeyName)) + }) +} + +func validateValidatorKey(t *testing.T, path string) { + t.Helper() + + validatorKeyRaw, err := os.ReadFile(path) + require.NoError(t, err) + + var validatorKey privval.FilePVKey + require.NoError(t, amino.UnmarshalJSON(validatorKeyRaw, &validatorKey)) + + assert.NotNil(t, validatorKey.Address) + assert.NotEqual(t, types.Address{}, validatorKey.Address) + assert.NotNil(t, validatorKey.PrivKey) + assert.NotNil(t, validatorKey.PubKey) +} + +func validateValidatorState(t *testing.T, path string) { + t.Helper() + + validatorStateRaw, err := os.ReadFile(path) + require.NoError(t, err) + + var validatorState privval.FilePVLastSignState + require.NoError(t, amino.UnmarshalJSON(validatorStateRaw, &validatorState)) + + assert.Zero(t, validatorState.Height) + assert.Zero(t, validatorState.Round) + assert.Zero(t, validatorState.Step) + assert.Nil(t, validatorState.Signature) + assert.Nil(t, validatorState.SignBytes) +} + +func validateNodeKey(t *testing.T, path string) { + t.Helper() + + nodeKeyRaw, err := os.ReadFile(path) + require.NoError(t, err) + + var nodeKey p2p.NodeKey + require.NoError(t, amino.UnmarshalJSON(nodeKeyRaw, &nodeKey)) + + assert.NotNil(t, nodeKey.PrivKey) +} diff --git a/gno.land/cmd/secrets/init_single_test.go b/gno.land/cmd/secrets/init_single_test.go new file mode 100644 index 00000000000..e5a748a52c1 --- /dev/null +++ b/gno.land/cmd/secrets/init_single_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSecrets_Init_Single(t *testing.T) { + t.Parallel() + + t.Run("no individual path set", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "init", + "single", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoOutputSet.Error()) + }) + + t.Run("individual secrets initialized", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + flagValue string + validateFn func(*testing.T, string) + }{ + { + "validator key initialized", + "--validator-key-path", + validateValidatorKey, + }, + { + "validator state initialized", + "--validator-state-path", + validateValidatorState, + }, + { + "node p2p initialized", + "--node-key-path", + validateNodeKey, + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + tempDir = t.TempDir() + dataName = "data.json" + + expectedPath = filepath.Join(tempDir, dataName) + ) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "init", + "single", + testCase.flagValue, + expectedPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the validator key is saved + testCase.validateFn(t, expectedPath) + }) + } + }) +} From dc2ccf910923308f498026a53bac2f007a1fb878 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 15:25:26 +0100 Subject: [PATCH 04/18] Add verify subcommands and tests --- gno.land/cmd/secrets/common.go | 142 ++++++++++- gno.land/cmd/secrets/common_test.go | 259 +++++++++++++++++++++ gno.land/cmd/secrets/init.go | 6 +- gno.land/cmd/secrets/init_all_test.go | 37 +-- gno.land/cmd/secrets/init_single_test.go | 14 +- gno.land/cmd/secrets/main.go | 2 +- gno.land/cmd/secrets/verify.go | 76 ++++++ gno.land/cmd/secrets/verify_all.go | 71 ++++++ gno.land/cmd/secrets/verify_all_test.go | 253 ++++++++++++++++++++ gno.land/cmd/secrets/verify_single.go | 90 +++++++ gno.land/cmd/secrets/verify_single_test.go | 192 +++++++++++++++ 11 files changed, 1098 insertions(+), 44 deletions(-) create mode 100644 gno.land/cmd/secrets/common_test.go create mode 100644 gno.land/cmd/secrets/verify.go create mode 100644 gno.land/cmd/secrets/verify_all.go create mode 100644 gno.land/cmd/secrets/verify_all_test.go create mode 100644 gno.land/cmd/secrets/verify_single.go create mode 100644 gno.land/cmd/secrets/verify_single_test.go diff --git a/gno.land/cmd/secrets/common.go b/gno.land/cmd/secrets/common.go index 34fb157f644..8c50fbe5950 100644 --- a/gno.land/cmd/secrets/common.go +++ b/gno.land/cmd/secrets/common.go @@ -1,20 +1,37 @@ package main import ( + "errors" "fmt" "os" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" "github.com/gnolang/gno/tm2/pkg/p2p" ) +var ( + errInvalidPrivateKey = errors.New("invalid validator private key") + errPublicKeyMismatch = errors.New("public key does not match private key derivation") + errAddressMismatch = errors.New("address does not match public key") + + errInvalidSignStateStep = errors.New("invalid sign state step value") + errInvalidSignStateHeight = errors.New("invalid sign state height value") + errInvalidSignStateRound = errors.New("invalid sign state round value") + + errSignatureMismatch = errors.New("signature does not match signature bytes") + errSignatureValuesMissing = errors.New("missing signature value") + + errInvalidNodeKey = errors.New("invalid node p2p key") +) + // generateValidatorPrivateKey generates the validator's private key -func generateValidatorPrivateKey() privval.FilePVKey { +func generateValidatorPrivateKey() *privval.FilePVKey { privKey := ed25519.GenPrivKey() - return privval.FilePVKey{ + return &privval.FilePVKey{ Address: privKey.PubKey().Address(), PubKey: privKey.PubKey(), PrivKey: privKey, @@ -22,8 +39,8 @@ func generateValidatorPrivateKey() privval.FilePVKey { } // generateLastSignValidatorState generates the empty last sign state -func generateLastSignValidatorState() privval.FilePVLastSignState { - return privval.FilePVLastSignState{} // Empty last sign state +func generateLastSignValidatorState() *privval.FilePVLastSignState { + return &privval.FilePVLastSignState{} // Empty last sign state } // generateNodeKey generates the p2p node key @@ -35,8 +52,8 @@ func generateNodeKey() *p2p.NodeKey { } } -// saveDataToPath saves the given data as Amino JSON to the path -func saveDataToPath(data any, path string) error { +// saveSecretData saves the given data as Amino JSON to the path +func saveSecretData(data any, path string) error { // Get Amino JSON marshalledData, err := amino.MarshalJSONIndent(data, "", " ") if err != nil { @@ -45,7 +62,118 @@ func saveDataToPath(data any, path string) error { // Save the data to disk if err := os.WriteFile(path, marshalledData, 0o644); err != nil { - return fmt.Errorf("unable to save data to disk, %w", err) + return fmt.Errorf("unable to save data to path, %w", err) + } + + return nil +} + +// isValidDirectory verifies the directory at the given path exists +func isValidDirectory(dirPath string) bool { + fileInfo, err := os.Stat(dirPath) + if err != nil { + return false + } + + // Check if the path is indeed a directory + return fileInfo.IsDir() +} + +type secretData interface { + privval.FilePVKey | privval.FilePVLastSignState | p2p.NodeKey +} + +// readSecretData reads the secret data from the given path +func readSecretData[T secretData]( + path string, +) (*T, error) { + dataRaw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("unable to read data, %w", err) + } + + var data T + if err := amino.UnmarshalJSON(dataRaw, &data); err != nil { + return nil, fmt.Errorf("unable to unmarshal data, %w", err) + } + + return &data, nil +} + +// validateValidatorKey validates the validator's private key +func validateValidatorKey(key *privval.FilePVKey) error { + // Make sure the private key is set + if key.PrivKey == nil { + return errInvalidPrivateKey + } + + // Make sure the public key is derived + // from the private one + if !key.PrivKey.PubKey().Equals(key.PubKey) { + return errPublicKeyMismatch + } + + // Make sure the address is derived + // from the public key + if key.PubKey.Address().Compare(key.Address) != 0 { + return errAddressMismatch + } + + return nil +} + +// validateValidatorState validates the validator's last sign state +func validateValidatorState(state *privval.FilePVLastSignState) error { + // Make sure the sign step is valid + if state.Step < 0 { + return errInvalidSignStateStep + } + + // Make sure the height is valid + if state.Height < 0 { + return errInvalidSignStateHeight + } + + // Make sure the round is valid + if state.Round < 0 { + return errInvalidSignStateRound + } + + return nil +} + +// validateValidatorStateSignature validates the signature section +// of the last sign validator state +func validateValidatorStateSignature( + state *privval.FilePVLastSignState, + key crypto.PubKey, +) error { + // Make sure the signature and signature bytes are valid + signBytesPresent := state.SignBytes != nil + signaturePresent := state.Signature != nil + + if signBytesPresent && !signaturePresent || + !signBytesPresent && signaturePresent { + return errSignatureValuesMissing + } + + if !signaturePresent { + // No need to verify further + return nil + } + + // Make sure the signature bytes match the signature + if !key.VerifyBytes(state.SignBytes, state.Signature) { + return errSignatureMismatch + } + + return nil +} + +// validateNodeKey validates the node's p2p key +func validateNodeKey(key *p2p.NodeKey) error { + if key.PrivKey == nil { + return errInvalidNodeKey } return nil diff --git a/gno.land/cmd/secrets/common_test.go b/gno.land/cmd/secrets/common_test.go new file mode 100644 index 00000000000..34592c3bd8f --- /dev/null +++ b/gno.land/cmd/secrets/common_test.go @@ -0,0 +1,259 @@ +package main + +import ( + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCommon_SaveReadData(t *testing.T) { + t.Parallel() + + t.Run("invalid data save path", func(t *testing.T) { + t.Parallel() + + assert.ErrorContains( + t, + saveSecretData(nil, ""), + "unable to save data to path", + ) + }) + + t.Run("invalid data read path", func(t *testing.T) { + t.Parallel() + + readData, err := readSecretData[p2p.NodeKey]("") + assert.Nil(t, readData) + + assert.ErrorContains( + t, + err, + "unable to read data", + ) + }) + + t.Run("invalid data read", func(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + path := filepath.Join(dir, "key.json") + + require.NoError(t, saveSecretData("totally valid key", path)) + + readData, err := readSecretData[p2p.NodeKey](path) + require.Nil(t, readData) + + assert.ErrorContains(t, err, "unable to unmarshal data") + }) + + t.Run("valid data save and read", func(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + path := filepath.Join(dir, "key.json") + key := generateNodeKey() + + require.NoError(t, saveSecretData(key, path)) + + readKey, err := readSecretData[p2p.NodeKey](path) + require.NoError(t, err) + + assert.Equal(t, key, readKey) + }) +} + +func TestCommon_ValidateValidatorKey(t *testing.T) { + t.Parallel() + + t.Run("valid validator key", func(t *testing.T) { + t.Parallel() + + key := generateValidatorPrivateKey() + + assert.NoError(t, validateValidatorKey(key)) + }) + + t.Run("invalid private key", func(t *testing.T) { + t.Parallel() + + key := generateValidatorPrivateKey() + key.PrivKey = nil + + assert.ErrorIs(t, validateValidatorKey(key), errInvalidPrivateKey) + }) + + t.Run("public key mismatch", func(t *testing.T) { + t.Parallel() + + key := generateValidatorPrivateKey() + key.PubKey = nil + + assert.ErrorIs(t, validateValidatorKey(key), errPublicKeyMismatch) + }) + + t.Run("address mismatch", func(t *testing.T) { + t.Parallel() + + key := generateValidatorPrivateKey() + key.Address = crypto.Address{} // zero address + + assert.ErrorIs(t, validateValidatorKey(key), errAddressMismatch) + }) +} + +func TestCommon_ValidateValidatorState(t *testing.T) { + t.Parallel() + + t.Run("valid validator state", func(t *testing.T) { + t.Parallel() + + state := generateLastSignValidatorState() + + assert.NoError(t, validateValidatorState(state)) + }) + + t.Run("invalid step", func(t *testing.T) { + t.Parallel() + + state := generateLastSignValidatorState() + state.Step = -1 + + assert.ErrorIs(t, validateValidatorState(state), errInvalidSignStateStep) + }) + + t.Run("invalid height", func(t *testing.T) { + t.Parallel() + + state := generateLastSignValidatorState() + state.Height = -1 + + assert.ErrorIs(t, validateValidatorState(state), errInvalidSignStateHeight) + }) + + t.Run("invalid round", func(t *testing.T) { + t.Parallel() + + state := generateLastSignValidatorState() + state.Round = -1 + + assert.ErrorIs(t, validateValidatorState(state), errInvalidSignStateRound) + }) +} + +func TestCommon_ValidateStateSignature(t *testing.T) { + t.Parallel() + + t.Run("valid state signature", func(t *testing.T) { + t.Parallel() + + var ( + key = generateValidatorPrivateKey() + state = generateLastSignValidatorState() + + signData = []byte("random data") + ) + + // Prepare the signature + signature, err := key.PrivKey.Sign(signData) + require.NoError(t, err) + + state.Signature = signature + state.SignBytes = signData + + assert.NoError(t, validateValidatorStateSignature(state, key.PubKey)) + }) + + t.Run("no state signature", func(t *testing.T) { + t.Parallel() + + var ( + key = generateValidatorPrivateKey() + state = generateLastSignValidatorState() + ) + + assert.NoError(t, validateValidatorStateSignature(state, key.PubKey)) + }) + + t.Run("signature values missing, sign bytes", func(t *testing.T) { + t.Parallel() + + var ( + key = generateValidatorPrivateKey() + state = generateLastSignValidatorState() + ) + + state.Signature = []byte("signature") + + assert.ErrorIs( + t, + validateValidatorStateSignature(state, key.PubKey), + errSignatureValuesMissing, + ) + }) + + t.Run("signature values missing, signature", func(t *testing.T) { + t.Parallel() + + var ( + key = generateValidatorPrivateKey() + state = generateLastSignValidatorState() + ) + + state.SignBytes = []byte("signature") + + assert.ErrorIs( + t, + validateValidatorStateSignature(state, key.PubKey), + errSignatureValuesMissing, + ) + }) + + t.Run("signature mismatch", func(t *testing.T) { + t.Parallel() + + var ( + key = generateValidatorPrivateKey() + state = generateLastSignValidatorState() + + signData = []byte("random data") + ) + + // Prepare the signature + signature, err := key.PrivKey.Sign(signData) + require.NoError(t, err) + + state.Signature = signature + state.SignBytes = []byte("something different") + + assert.ErrorIs( + t, + validateValidatorStateSignature(state, key.PubKey), + errSignatureMismatch, + ) + }) +} + +func TestCommon_ValidateNodeKey(t *testing.T) { + t.Parallel() + + t.Run("valid node key", func(t *testing.T) { + t.Parallel() + + key := generateNodeKey() + + assert.NoError(t, validateNodeKey(key)) + }) + + t.Run("invalid node key", func(t *testing.T) { + t.Parallel() + + key := generateNodeKey() + key.PrivKey = nil + + assert.ErrorIs(t, validateNodeKey(key), errInvalidNodeKey) + }) +} diff --git a/gno.land/cmd/secrets/init.go b/gno.land/cmd/secrets/init.go index 3462f305fcc..8249f7e654d 100644 --- a/gno.land/cmd/secrets/init.go +++ b/gno.land/cmd/secrets/init.go @@ -33,7 +33,7 @@ func initAndSaveValidatorKey(path string, io commands.IO) error { privateKey := generateValidatorPrivateKey() // Save the key - if err := saveDataToPath(privateKey, path); err != nil { + if err := saveSecretData(privateKey, path); err != nil { return fmt.Errorf("unable to save validator key, %w", err) } @@ -48,7 +48,7 @@ func initAndSaveValidatorState(path string, io commands.IO) error { validatorState := generateLastSignValidatorState() // Save the last sign state - if err := saveDataToPath(validatorState, path); err != nil { + if err := saveSecretData(validatorState, path); err != nil { return fmt.Errorf("unable to save last validator sign state, %w", err) } @@ -63,7 +63,7 @@ func initAndSaveNodeKey(path string, io commands.IO) error { nodeKey := generateNodeKey() // Save the node key - if err := saveDataToPath(nodeKey, path); err != nil { + if err := saveSecretData(nodeKey, path); err != nil { return fmt.Errorf("unable to save node p2p key, %w", err) } diff --git a/gno.land/cmd/secrets/init_all_test.go b/gno.land/cmd/secrets/init_all_test.go index 0c97af6a879..288ab85cf74 100644 --- a/gno.land/cmd/secrets/init_all_test.go +++ b/gno.land/cmd/secrets/init_all_test.go @@ -2,13 +2,10 @@ package main import ( "context" - "os" "path/filepath" "testing" - "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/privval" - "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/stretchr/testify/assert" @@ -55,40 +52,31 @@ func TestSecrets_Init_All(t *testing.T) { require.NoError(t, cmdErr) // Verify the validator key is saved - validateValidatorKey(t, filepath.Join(tempDir, defaultValidatorKeyName)) + verifyValidatorKey(t, filepath.Join(tempDir, defaultValidatorKeyName)) // Verify the last sign validator state is saved - validateValidatorState(t, filepath.Join(tempDir, defaultValidatorStateName)) + verifyValidatorState(t, filepath.Join(tempDir, defaultValidatorStateName)) // Verify the node p2p key is saved - validateNodeKey(t, filepath.Join(tempDir, defaultNodeKeyName)) + verifyNodeKey(t, filepath.Join(tempDir, defaultNodeKeyName)) }) } -func validateValidatorKey(t *testing.T, path string) { +func verifyValidatorKey(t *testing.T, path string) { t.Helper() - validatorKeyRaw, err := os.ReadFile(path) + validatorKey, err := readSecretData[privval.FilePVKey](path) require.NoError(t, err) - var validatorKey privval.FilePVKey - require.NoError(t, amino.UnmarshalJSON(validatorKeyRaw, &validatorKey)) - - assert.NotNil(t, validatorKey.Address) - assert.NotEqual(t, types.Address{}, validatorKey.Address) - assert.NotNil(t, validatorKey.PrivKey) - assert.NotNil(t, validatorKey.PubKey) + assert.NoError(t, validateValidatorKey(validatorKey)) } -func validateValidatorState(t *testing.T, path string) { +func verifyValidatorState(t *testing.T, path string) { t.Helper() - validatorStateRaw, err := os.ReadFile(path) + validatorState, err := readSecretData[privval.FilePVLastSignState](path) require.NoError(t, err) - var validatorState privval.FilePVLastSignState - require.NoError(t, amino.UnmarshalJSON(validatorStateRaw, &validatorState)) - assert.Zero(t, validatorState.Height) assert.Zero(t, validatorState.Round) assert.Zero(t, validatorState.Step) @@ -96,14 +84,11 @@ func validateValidatorState(t *testing.T, path string) { assert.Nil(t, validatorState.SignBytes) } -func validateNodeKey(t *testing.T, path string) { +func verifyNodeKey(t *testing.T, path string) { t.Helper() - nodeKeyRaw, err := os.ReadFile(path) + nodeKey, err := readSecretData[p2p.NodeKey](path) require.NoError(t, err) - var nodeKey p2p.NodeKey - require.NoError(t, amino.UnmarshalJSON(nodeKeyRaw, &nodeKey)) - - assert.NotNil(t, nodeKey.PrivKey) + assert.NoError(t, validateNodeKey(nodeKey)) } diff --git a/gno.land/cmd/secrets/init_single_test.go b/gno.land/cmd/secrets/init_single_test.go index e5a748a52c1..8ea48ee935a 100644 --- a/gno.land/cmd/secrets/init_single_test.go +++ b/gno.land/cmd/secrets/init_single_test.go @@ -32,24 +32,24 @@ func TestSecrets_Init_Single(t *testing.T) { t.Parallel() testTable := []struct { - name string - flagValue string - validateFn func(*testing.T, string) + name string + flagValue string + verifyFn func(*testing.T, string) }{ { "validator key initialized", "--validator-key-path", - validateValidatorKey, + verifyValidatorKey, }, { "validator state initialized", "--validator-state-path", - validateValidatorState, + verifyValidatorState, }, { "node p2p initialized", "--node-key-path", - validateNodeKey, + verifyNodeKey, }, } @@ -80,7 +80,7 @@ func TestSecrets_Init_Single(t *testing.T) { require.NoError(t, cmdErr) // Verify the validator key is saved - testCase.validateFn(t, expectedPath) + testCase.verifyFn(t, expectedPath) }) } }) diff --git a/gno.land/cmd/secrets/main.go b/gno.land/cmd/secrets/main.go index 44b181eff1f..21b5697f4e6 100644 --- a/gno.land/cmd/secrets/main.go +++ b/gno.land/cmd/secrets/main.go @@ -38,7 +38,7 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newInitCmd(io), - // newVerifyCmd(io), + newVerifyCmd(io), // newShowCmd(io), ) diff --git a/gno.land/cmd/secrets/verify.go b/gno.land/cmd/secrets/verify.go new file mode 100644 index 00000000000..d8499d2d1c2 --- /dev/null +++ b/gno.land/cmd/secrets/verify.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" +) + +// newVerifyCmd creates the new secrets verify command +func newVerifyCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "verify", + ShortUsage: "verify [subcommand] [flags]", + ShortHelp: "Verifies the Gno node secrets", + LongHelp: "Verifies the Gno node secrets locally, including the validator key, validator state and node key", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newVerifyAllCmd(io), + newVerifySingleCmd(io), + ) + + return cmd +} + +// readAndVerifyValidatorKey reads the validator key from the given path and verifies it +func readAndVerifyValidatorKey(path string, io commands.IO) (*privval.FilePVKey, error) { + validatorKey, err := readSecretData[privval.FilePVKey](path) + if err != nil { + return nil, fmt.Errorf("unable to read validator key, %w", err) + } + + if err := validateValidatorKey(validatorKey); err != nil { + return nil, err + } + + io.Printfln("Validator Private Key at %s is valid", path) + + return validatorKey, nil +} + +func readAndVerifyValidatorState(path string, io commands.IO) (*privval.FilePVLastSignState, error) { + validatorState, err := readSecretData[privval.FilePVLastSignState](path) + if err != nil { + return nil, fmt.Errorf("unable to read last validator sign state, %w", err) + } + + if err := validateValidatorState(validatorState); err != nil { + return nil, err + } + + io.Printfln("Last Validator Sign state at %s is valid", path) + + return validatorState, nil +} + +func readAndVerifyNodeKey(path string, io commands.IO) error { + nodeKey, err := readSecretData[p2p.NodeKey](path) + if err != nil { + return fmt.Errorf("unable to read node p2p key, %w", err) + } + + if err := validateNodeKey(nodeKey); err != nil { + return err + } + + io.Printfln("Node P2P key at %s is valid", path) + + return nil +} diff --git a/gno.land/cmd/secrets/verify_all.go b/gno.land/cmd/secrets/verify_all.go new file mode 100644 index 00000000000..1d3f9623c36 --- /dev/null +++ b/gno.land/cmd/secrets/verify_all.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "flag" + "path/filepath" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type verifyAllCfg struct { + commonAllCfg +} + +// newVerifyAllCmd creates the secrets verify all command +func newVerifyAllCmd(io commands.IO) *commands.Command { + cfg := &verifyAllCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "all", + ShortUsage: "verify all [flags]", + ShortHelp: "Verifies all Gno secrets in a common directory", + LongHelp: "Verifies the validator private key, the node p2p key and the validator's last sign state", + }, + cfg, + func(_ context.Context, _ []string) error { + return execVerifyAll(cfg, io) + }, + ) + + return cmd +} + +func (c *verifyAllCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonAllCfg.RegisterFlags(fs) +} + +func execVerifyAll(cfg *verifyAllCfg, io commands.IO) error { + // Make sure the directory is there + if cfg.dataDir == "" || !isValidDirectory(cfg.dataDir) { + return errInvalidDataDir + } + + // Construct the paths + var ( + validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) + validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) + nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) + ) + + // Validate the validator's private key + validatorKey, err := readAndVerifyValidatorKey(validatorKeyPath, io) + if err != nil { + return err + } + + // Validate the validator's last sign state + validatorState, err := readAndVerifyValidatorState(validatorStatePath, io) + if err != nil { + return err + } + + // Validate the signature bytes + if err = validateValidatorStateSignature(validatorState, validatorKey.PubKey); err != nil { + return err + } + + // Validate the node's p2p key + return readAndVerifyNodeKey(nodeKeyPath, io) +} diff --git a/gno.land/cmd/secrets/verify_all_test.go b/gno.land/cmd/secrets/verify_all_test.go new file mode 100644 index 00000000000..e42640d1521 --- /dev/null +++ b/gno.land/cmd/secrets/verify_all_test.go @@ -0,0 +1,253 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSecrets_Verify_All(t *testing.T) { + t.Parallel() + + t.Run("invalid data directory", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "all", + "--data-dir", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidDataDir.Error()) + }) + + t.Run("invalid data directory", func(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + path := filepath.Join(dir, "example.json") + + require.NoError( + t, + os.WriteFile( + path, + []byte("hello"), + 0o644, + ), + ) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "all", + "--data-dir", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidDataDir.Error()) + }) + + t.Run("signature mismatch", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + + // Run the init command + initArgs := []string{ + "init", + "all", + "--data-dir", + tempDir, + } + + // Run the init command + require.NoError(t, cmd.ParseAndRun(context.Background(), initArgs)) + + // Modify the signature + statePath := filepath.Join(tempDir, defaultValidatorStateName) + state, err := readSecretData[privval.FilePVLastSignState](statePath) + require.NoError(t, err) + + state.SignBytes = []byte("something totally random") + state.Signature = []byte("signature") + + require.NoError(t, saveSecretData(state, statePath)) + + cmd = newRootCmd(commands.NewTestIO()) + + // Run the verify command + verifyArgs := []string{ + "verify", + "all", + "--data-dir", + tempDir, + } + + assert.ErrorContains( + t, + cmd.ParseAndRun(context.Background(), verifyArgs), + errSignatureMismatch.Error(), + ) + }) + + t.Run("all secrets valid", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + + // Run the init command + initArgs := []string{ + "init", + "all", + "--data-dir", + tempDir, + } + + // Run the init command + require.NoError(t, cmd.ParseAndRun(context.Background(), initArgs)) + + cmd = newRootCmd(commands.NewTestIO()) + + // Run the verify command + verifyArgs := []string{ + "verify", + "all", + "--data-dir", + tempDir, + } + + assert.NoError(t, cmd.ParseAndRun(context.Background(), verifyArgs)) + }) +} + +func TestSecrets_Verify_All_Missing(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + fileName string + expectedErrorMessage string + }{ + { + "invalid validator key path", + defaultValidatorKeyName, + "unable to read validator key", + }, + { + "invalid validator state path", + defaultValidatorStateName, + "unable to read last validator sign state", + }, + { + "invalid node p2p key path", + defaultNodeKeyName, + "unable to read node p2p key", + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + + // Run the init command + initArgs := []string{ + "init", + "all", + "--data-dir", + tempDir, + } + + // Run the init command + require.NoError(t, cmd.ParseAndRun(context.Background(), initArgs)) + + // Delete the validator key + require.NoError(t, os.Remove(filepath.Join(tempDir, testCase.fileName))) + + cmd = newRootCmd(commands.NewTestIO()) + + // Run the verify command + verifyArgs := []string{ + "verify", + "all", + "--data-dir", + tempDir, + } + + assert.ErrorContains( + t, + cmd.ParseAndRun(context.Background(), verifyArgs), + testCase.expectedErrorMessage, + ) + }) + } + + t.Run("invalid validator key path", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + + // Run the init command + initArgs := []string{ + "init", + "all", + "--data-dir", + tempDir, + } + + // Run the init command + require.NoError(t, cmd.ParseAndRun(context.Background(), initArgs)) + + // Delete the validator key + require.NoError(t, os.Remove(filepath.Join(tempDir, defaultValidatorKeyName))) + + cmd = newRootCmd(commands.NewTestIO()) + + // Run the verify command + verifyArgs := []string{ + "verify", + "all", + "--data-dir", + tempDir, + } + + assert.ErrorContains( + t, + cmd.ParseAndRun(context.Background(), verifyArgs), + "unable to read validator key", + ) + }) +} diff --git a/gno.land/cmd/secrets/verify_single.go b/gno.land/cmd/secrets/verify_single.go new file mode 100644 index 00000000000..c2bbc62ff8f --- /dev/null +++ b/gno.land/cmd/secrets/verify_single.go @@ -0,0 +1,90 @@ +package main + +import ( + "context" + "flag" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type verifySingleCfg struct { + commonSingleCfg +} + +// newVerifySingleCmd creates the secrets verify single command +func newVerifySingleCmd(io commands.IO) *commands.Command { + cfg := &verifySingleCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "single", + ShortUsage: "verify single [flags]", + ShortHelp: "Verifies required Gno secrets individually", + LongHelp: "Verifies the validator private key, the node p2p key and the validator's last sign state" + + " at custom paths", + }, + cfg, + func(_ context.Context, _ []string) error { + return execVerifySingle(cfg, io) + }, + ) + + return cmd +} + +func (c *verifySingleCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonSingleCfg.RegisterFlags(fs) +} + +func execVerifySingle(cfg *verifySingleCfg, io commands.IO) error { + var ( + validatorKeyPathSet = cfg.validatorKeyPath != "" + validatorStatePathSet = cfg.validatorStatePath != "" + nodeKeyPathSet = cfg.nodeKeyPath != "" + ) + + var ( + validatorKey *privval.FilePVKey + validatorState *privval.FilePVLastSignState + + err error + ) + + if !validatorKeyPathSet && !validatorStatePathSet && !nodeKeyPathSet { + return errNoOutputSet + } + + // Verify the validator private key, if any + if validatorKeyPathSet { + validatorKey, err = readAndVerifyValidatorKey(cfg.validatorKeyPath, io) + if err != nil { + return err + } + } + + // Verify the last validator sign state, if any + if validatorStatePathSet { + validatorState, err = readAndVerifyValidatorState(cfg.validatorStatePath, io) + if err != nil { + return err + } + } + + // Verify the signature bytes if the key and validator state + // is provided + if validatorKey != nil && validatorState != nil { + if err = validateValidatorStateSignature(validatorState, validatorKey.PubKey); err != nil { + return err + } + } + + // Verify the node key, if any + if nodeKeyPathSet { + if err = readAndVerifyNodeKey(cfg.nodeKeyPath, io); err != nil { + return err + } + } + + return nil +} diff --git a/gno.land/cmd/secrets/verify_single_test.go b/gno.land/cmd/secrets/verify_single_test.go new file mode 100644 index 00000000000..8b057e60cd2 --- /dev/null +++ b/gno.land/cmd/secrets/verify_single_test.go @@ -0,0 +1,192 @@ +package main + +import ( + "context" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSecrets_Verify_Single(t *testing.T) { + t.Parallel() + + t.Run("no individual path set", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoOutputSet.Error()) + }) + + t.Run("invalid validator key path", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + "--validator-key-path", + "random path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to read validator key") + }) + + t.Run("invalid validator key", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + path := filepath.Join(dirPath, "data.json") + + invalidKey := &privval.FilePVKey{ + PrivKey: nil, // invalid + } + + require.NoError(t, saveSecretData(invalidKey, path)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + "--validator-key-path", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidPrivateKey) + }) + + t.Run("invalid validator state", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + path := filepath.Join(dirPath, "data.json") + + invalidState := &privval.FilePVLastSignState{ + Height: -1, // invalid + } + + require.NoError(t, saveSecretData(invalidState, path)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + "--validator-state-path", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidSignStateHeight) + }) + + t.Run("invalid validator state signature", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + keyPath := filepath.Join(dirPath, "key.json") + statePath := filepath.Join(dirPath, "state.json") + + validKey := generateValidatorPrivateKey() + validState := generateLastSignValidatorState() + + // Save an invalid signature + validState.Signature = []byte("totally valid signature") + + require.NoError(t, saveSecretData(validKey, keyPath)) + require.NoError(t, saveSecretData(validState, statePath)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + "--validator-key-path", + keyPath, + "--validator-state-path", + statePath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errSignatureValuesMissing) + }) + + t.Run("invalid node key", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + path := filepath.Join(dirPath, "data.json") + + invalidNodeKey := &p2p.NodeKey{ + PrivKey: nil, // invalid + } + + require.NoError(t, saveSecretData(invalidNodeKey, path)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + "--node-key-path", + path, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidNodeKey) + }) + + t.Run("all secrets set and valid", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + keyPath := filepath.Join(dirPath, "key.json") + statePath := filepath.Join(dirPath, "state.json") + nodeKeyPath := filepath.Join(dirPath, "p2p.json") + + validKey := generateValidatorPrivateKey() + validState := generateLastSignValidatorState() + validNodeKey := generateNodeKey() + + require.NoError(t, saveSecretData(validKey, keyPath)) + require.NoError(t, saveSecretData(validState, statePath)) + require.NoError(t, saveSecretData(validNodeKey, nodeKeyPath)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "single", + "--validator-key-path", + keyPath, + "--validator-state-path", + statePath, + "--node-key-path", + nodeKeyPath, + } + + // Run the command + assert.NoError(t, cmd.ParseAndRun(context.Background(), args)) + }) +} From d81e84bc4302d64083bc803b08ca602605f68ca8 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 16:53:24 +0100 Subject: [PATCH 05/18] Add show subcommands --- gno.land/cmd/secrets/main.go | 2 +- gno.land/cmd/secrets/show.go | 139 ++++++++++++++++++++++++++++ gno.land/cmd/secrets/show_all.go | 64 +++++++++++++ gno.land/cmd/secrets/show_single.go | 72 ++++++++++++++ gno.land/cmd/secrets/verify.go | 2 + 5 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 gno.land/cmd/secrets/show.go create mode 100644 gno.land/cmd/secrets/show_all.go create mode 100644 gno.land/cmd/secrets/show_single.go diff --git a/gno.land/cmd/secrets/main.go b/gno.land/cmd/secrets/main.go index 21b5697f4e6..48be808fc79 100644 --- a/gno.land/cmd/secrets/main.go +++ b/gno.land/cmd/secrets/main.go @@ -39,7 +39,7 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newInitCmd(io), newVerifyCmd(io), - // newShowCmd(io), + newShowCmd(io), ) return cmd diff --git a/gno.land/cmd/secrets/show.go b/gno.land/cmd/secrets/show.go new file mode 100644 index 00000000000..72d2df1c038 --- /dev/null +++ b/gno.land/cmd/secrets/show.go @@ -0,0 +1,139 @@ +package main + +import ( + "fmt" + "text/tabwriter" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" +) + +// newShowCmd creates the new secrets show command +func newShowCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "show", + ShortUsage: "show [subcommand] [flags]", + ShortHelp: "Shows the Gno node secrets", + LongHelp: "Shows the Gno node secrets locally, including the validator key, validator state and node key", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newShowAllCmd(io), + newShowSingleCmd(io), + ) + + return cmd +} + +// readAndShowValidatorKey reads and shows the validator key from the given path +func readAndShowValidatorKey(path string, io commands.IO) error { + validatorKey, err := readSecretData[privval.FilePVKey](path) + if err != nil { + return fmt.Errorf("unable to read validator key, %w", err) + } + + w := tabwriter.NewWriter(io.Out(), 0, 0, 2, ' ', 0) + + if _, err := fmt.Fprintln(w, "[Validator Key Info]\n"); err != nil { + return err + } + + if _, err := fmt.Fprintf(w, "Address:\t%s\n", validatorKey.Address.String()); err != nil { + return err + } + + if _, err := fmt.Fprintf(w, "Public Key:\t%s\n", validatorKey.PubKey.String()); err != nil { + return err + } + + return w.Flush() +} + +// readAndShowValidatorState reads and shows the validator state from the given path +func readAndShowValidatorState(path string, io commands.IO) error { + validatorState, err := readSecretData[privval.FilePVLastSignState](path) + if err != nil { + return fmt.Errorf("unable to read validator state, %w", err) + } + + w := tabwriter.NewWriter(io.Out(), 0, 0, 2, ' ', 0) + + if _, err := fmt.Fprintln(w, "[Last Validator Sign State Info]\n"); err != nil { + return err + } + + if _, err := fmt.Fprintf( + w, + "Height:\t%d\n", + validatorState.Height, + ); err != nil { + return err + } + + if _, err := fmt.Fprintf( + w, + "Round:\t%d\n", + validatorState.Round, + ); err != nil { + return err + } + + if _, err := fmt.Fprintf( + w, + "Step:\t%d\n", + validatorState.Step, + ); err != nil { + return err + } + + if validatorState.Signature != nil { + if _, err := fmt.Fprintf( + w, + "Signature:\t%X\n", + validatorState.Signature, + ); err != nil { + return err + } + } + + if validatorState.SignBytes != nil { + if _, err := fmt.Fprintf( + w, + "Sign Bytes:\t%X\n", + validatorState.SignBytes, + ); err != nil { + return err + } + } + + return w.Flush() +} + +// readAndShowNodeKey reads and shows the node p2p key from the given path +func readAndShowNodeKey(path string, io commands.IO) error { + nodeKey, err := readSecretData[p2p.NodeKey](path) + if err != nil { + return fmt.Errorf("unable to read node key, %w", err) + } + + w := tabwriter.NewWriter(io.Out(), 0, 0, 2, ' ', 0) + + if _, err := fmt.Fprintln(w, "[Node P2P Info]\n"); err != nil { + return err + } + + if _, err := fmt.Fprintf( + w, + "Node ID:\t%s\n", + nodeKey.ID(), + ); err != nil { + return err + } + + return w.Flush() +} diff --git a/gno.land/cmd/secrets/show_all.go b/gno.land/cmd/secrets/show_all.go new file mode 100644 index 00000000000..812f385c9a3 --- /dev/null +++ b/gno.land/cmd/secrets/show_all.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "flag" + "path/filepath" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type showAllCfg struct { + commonAllCfg +} + +// newShowAllCmd creates the secrets show all command +func newShowAllCmd(io commands.IO) *commands.Command { + cfg := &showAllCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "all", + ShortUsage: "show all [flags]", + ShortHelp: "Shows all Gno secrets in a common directory", + LongHelp: "Shows the validator private key, the node p2p key and the validator's last sign state", + }, + cfg, + func(_ context.Context, _ []string) error { + return execShowAll(cfg, io) + }, + ) + + return cmd +} + +func (c *showAllCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonAllCfg.RegisterFlags(fs) +} + +func execShowAll(cfg *showAllCfg, io commands.IO) error { + // Make sure the directory is there + if cfg.dataDir == "" || !isValidDirectory(cfg.dataDir) { + return errInvalidDataDir + } + + // Construct the paths + var ( + validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) + validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) + nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) + ) + + // Show the node's p2p info + if err := readAndShowNodeKey(nodeKeyPath, io); err != nil { + return err + } + + // Show the validator's key info + if err := readAndShowValidatorKey(validatorKeyPath, io); err != nil { + return err + } + + // Show the validator's last sign state + return readAndShowValidatorState(validatorStatePath, io) +} diff --git a/gno.land/cmd/secrets/show_single.go b/gno.land/cmd/secrets/show_single.go new file mode 100644 index 00000000000..d70d5f3393e --- /dev/null +++ b/gno.land/cmd/secrets/show_single.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "flag" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type ShowSingleCfg struct { + commonSingleCfg +} + +// newShowSingleCmd creates the secrets show single command +func newShowSingleCmd(io commands.IO) *commands.Command { + cfg := &ShowSingleCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "single", + ShortUsage: "show single [flags]", + ShortHelp: "Shows required Gno secrets individually", + LongHelp: "Shows the validator private key, the node p2p key and the validator's last sign state" + + " at custom paths", + }, + cfg, + func(_ context.Context, _ []string) error { + return execShowSingle(cfg, io) + }, + ) + + return cmd +} + +func (c *ShowSingleCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonSingleCfg.RegisterFlags(fs) +} + +func execShowSingle(cfg *ShowSingleCfg, io commands.IO) error { + var ( + validatorKeyPathSet = cfg.validatorKeyPath != "" + validatorStatePathSet = cfg.validatorStatePath != "" + nodeKeyPathSet = cfg.nodeKeyPath != "" + ) + + if !validatorKeyPathSet && !validatorStatePathSet && !nodeKeyPathSet { + return errNoOutputSet + } + + // Show the validator private key info, if any + if validatorKeyPathSet { + if err := readAndShowValidatorKey(cfg.validatorKeyPath, io); err != nil { + return err + } + } + + // Show the last validator sign state, if any + if validatorStatePathSet { + if err := readAndShowValidatorState(cfg.validatorStatePath, io); err != nil { + return err + } + } + + // Show the node key, if any + if nodeKeyPathSet { + if err := readAndShowNodeKey(cfg.nodeKeyPath, io); err != nil { + return err + } + } + + return nil +} diff --git a/gno.land/cmd/secrets/verify.go b/gno.land/cmd/secrets/verify.go index d8499d2d1c2..63caab00e17 100644 --- a/gno.land/cmd/secrets/verify.go +++ b/gno.land/cmd/secrets/verify.go @@ -45,6 +45,7 @@ func readAndVerifyValidatorKey(path string, io commands.IO) (*privval.FilePVKey, return validatorKey, nil } +// readAndVerifyValidatorState reads the validator state from the given path and verifies it func readAndVerifyValidatorState(path string, io commands.IO) (*privval.FilePVLastSignState, error) { validatorState, err := readSecretData[privval.FilePVLastSignState](path) if err != nil { @@ -60,6 +61,7 @@ func readAndVerifyValidatorState(path string, io commands.IO) (*privval.FilePVLa return validatorState, nil } +// readAndVerifyNodeKey reads the node p2p key from the given path and verifies it func readAndVerifyNodeKey(path string, io commands.IO) error { nodeKey, err := readSecretData[p2p.NodeKey](path) if err != nil { From 267a4f04ff9b4eafe5aaf27b424c8d0932fdc81b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 17:19:38 +0100 Subject: [PATCH 06/18] Add show all unit tests --- gno.land/cmd/secrets/show.go | 6 +- gno.land/cmd/secrets/show_all_test.go | 143 ++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 gno.land/cmd/secrets/show_all_test.go diff --git a/gno.land/cmd/secrets/show.go b/gno.land/cmd/secrets/show.go index 72d2df1c038..db455b6b760 100644 --- a/gno.land/cmd/secrets/show.go +++ b/gno.land/cmd/secrets/show.go @@ -39,7 +39,7 @@ func readAndShowValidatorKey(path string, io commands.IO) error { w := tabwriter.NewWriter(io.Out(), 0, 0, 2, ' ', 0) - if _, err := fmt.Fprintln(w, "[Validator Key Info]\n"); err != nil { + if _, err := fmt.Fprintf(w, "[Validator Key Info]\n\n"); err != nil { return err } @@ -63,7 +63,7 @@ func readAndShowValidatorState(path string, io commands.IO) error { w := tabwriter.NewWriter(io.Out(), 0, 0, 2, ' ', 0) - if _, err := fmt.Fprintln(w, "[Last Validator Sign State Info]\n"); err != nil { + if _, err := fmt.Fprintf(w, "[Last Validator Sign State Info]\n\n"); err != nil { return err } @@ -123,7 +123,7 @@ func readAndShowNodeKey(path string, io commands.IO) error { w := tabwriter.NewWriter(io.Out(), 0, 0, 2, ' ', 0) - if _, err := fmt.Fprintln(w, "[Node P2P Info]\n"); err != nil { + if _, err := fmt.Fprintf(w, "[Node P2P Info]\n\n"); err != nil { return err } diff --git a/gno.land/cmd/secrets/show_all_test.go b/gno.land/cmd/secrets/show_all_test.go new file mode 100644 index 00000000000..9fbf33c89cc --- /dev/null +++ b/gno.land/cmd/secrets/show_all_test.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "path/filepath" + "strconv" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSecrets_Show_All(t *testing.T) { + t.Parallel() + + t.Run("invalid data directory", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "show", + "all", + "--data-dir", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidDataDir.Error()) + }) + + t.Run("all secrets shown", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + + // Run the init command + initArgs := []string{ + "init", + "all", + "--data-dir", + tempDir, + } + + // Run the init command + require.NoError(t, cmd.ParseAndRun(context.Background(), initArgs)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + cmd = newRootCmd(io) + + // Get the node key + nodeKeyPath := filepath.Join(tempDir, defaultNodeKeyName) + nodeKey, err := readSecretData[p2p.NodeKey](nodeKeyPath) + require.NoError(t, err) + + // Get the validator private key + validatorKeyPath := filepath.Join(tempDir, defaultValidatorKeyName) + validatorKey, err := readSecretData[privval.FilePVKey](validatorKeyPath) + require.NoError(t, err) + + // Get the validator state + validatorStatePath := filepath.Join(tempDir, defaultValidatorStateName) + state, err := readSecretData[privval.FilePVLastSignState](validatorStatePath) + require.NoError(t, err) + + // Run the show command + showArgs := []string{ + "show", + "all", + "--data-dir", + tempDir, + } + + require.NoError(t, cmd.ParseAndRun(context.Background(), showArgs)) + + output := mockOutput.String() + + // Make sure the node p2p key is displayed + assert.Contains( + t, + output, + nodeKey.ID().String(), + ) + + // Make sure the private key info is displayed + assert.Contains( + t, + output, + validatorKey.Address.String(), + ) + + assert.Contains( + t, + output, + validatorKey.PubKey.String(), + ) + + // Make sure the private key info is displayed + assert.Contains( + t, + output, + validatorKey.Address.String(), + ) + + assert.Contains( + t, + output, + validatorKey.PubKey.String(), + ) + + // Make sure the state info is displayed + assert.Contains( + t, + output, + fmt.Sprintf("%d", state.Step), + ) + + assert.Contains( + t, + output, + fmt.Sprintf("%d", state.Height), + ) + + assert.Contains( + t, + output, + strconv.Itoa(state.Round), + ) + }) +} From 862e79636a6ca928c4f1db9c40e6f6768b204ad2 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Jan 2024 17:28:35 +0100 Subject: [PATCH 07/18] Add show subcommand unit tests --- gno.land/cmd/secrets/show_single_test.go | 159 +++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 gno.land/cmd/secrets/show_single_test.go diff --git a/gno.land/cmd/secrets/show_single_test.go b/gno.land/cmd/secrets/show_single_test.go new file mode 100644 index 00000000000..f585923c308 --- /dev/null +++ b/gno.land/cmd/secrets/show_single_test.go @@ -0,0 +1,159 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "path/filepath" + "strconv" + "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSecrets_Show_Single(t *testing.T) { + t.Parallel() + + t.Run("no individual path set", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "show", + "single", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoOutputSet.Error()) + }) + + t.Run("validator key shown", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + keyPath := filepath.Join(dirPath, "key.json") + + validKey := generateValidatorPrivateKey() + + require.NoError(t, saveSecretData(validKey, keyPath)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + // Create the command + cmd := newRootCmd(io) + args := []string{ + "show", + "single", + "--validator-key-path", + keyPath, + } + + // Run the command + require.NoError(t, cmd.ParseAndRun(context.Background(), args)) + + output := mockOutput.String() + + // Make sure the private key info is displayed + assert.Contains( + t, + output, + validKey.Address.String(), + ) + + assert.Contains( + t, + output, + validKey.PubKey.String(), + ) + }) + + t.Run("validator state shown", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + statePath := filepath.Join(dirPath, "state.json") + + validState := generateLastSignValidatorState() + + require.NoError(t, saveSecretData(validState, statePath)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + // Create the command + cmd := newRootCmd(io) + args := []string{ + "show", + "single", + "--validator-state-path", + statePath, + } + + // Run the command + require.NoError(t, cmd.ParseAndRun(context.Background(), args)) + + output := mockOutput.String() + + // Make sure the state info is displayed + assert.Contains( + t, + output, + fmt.Sprintf("%d", validState.Step), + ) + + assert.Contains( + t, + output, + fmt.Sprintf("%d", validState.Height), + ) + + assert.Contains( + t, + output, + strconv.Itoa(validState.Round), + ) + }) + + t.Run("node key shown", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + nodeKeyPath := filepath.Join(dirPath, "p2p.json") + + validNodeKey := generateNodeKey() + + require.NoError(t, saveSecretData(validNodeKey, nodeKeyPath)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + // Create the command + cmd := newRootCmd(io) + args := []string{ + "show", + "single", + "--node-key-path", + nodeKeyPath, + } + + // Run the command + require.NoError(t, cmd.ParseAndRun(context.Background(), args)) + + output := mockOutput.String() + + // Make sure the node p2p key is displayed + assert.Contains( + t, + output, + validNodeKey.ID().String(), + ) + }) +} From 7d24dd45d40857747ba9c86144111061ebb4c27a Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 29 Jan 2024 20:50:42 +0100 Subject: [PATCH 08/18] Make 'secrets' a subcommand of 'gnoland' --- gno.land/Makefile | 6 ++---- gno.land/cmd/gnoland/root.go | 1 + .../{secrets/main.go => gnoland/secrets.go} | 19 ++++++------------- .../common.go => gnoland/secrets_common.go} | 0 .../secrets_common_test.go} | 0 .../init.go => gnoland/secrets_init.go} | 2 +- .../secrets_init_all.go} | 2 +- .../secrets_init_all_test.go} | 2 ++ .../secrets_init_single.go} | 2 +- .../secrets_init_single_test.go} | 2 ++ .../show.go => gnoland/secrets_show.go} | 2 +- .../secrets_show_all.go} | 2 +- .../secrets_show_all_test.go} | 3 +++ .../secrets_show_single.go} | 2 +- .../secrets_show_single_test.go} | 4 ++++ .../verify.go => gnoland/secrets_verify.go} | 2 +- .../secrets_verify_all.go} | 2 +- .../secrets_verify_all_test.go} | 10 ++++++++++ .../secrets_verify_single.go} | 2 +- .../secrets_verify_single_test.go} | 7 +++++++ 20 files changed, 46 insertions(+), 26 deletions(-) rename gno.land/cmd/{secrets/main.go => gnoland/secrets.go} (81%) rename gno.land/cmd/{secrets/common.go => gnoland/secrets_common.go} (100%) rename gno.land/cmd/{secrets/common_test.go => gnoland/secrets_common_test.go} (100%) rename gno.land/cmd/{secrets/init.go => gnoland/secrets_init.go} (97%) rename gno.land/cmd/{secrets/init_all.go => gnoland/secrets_init_all.go} (97%) rename gno.land/cmd/{secrets/init_all_test.go => gnoland/secrets_init_all_test.go} (98%) rename gno.land/cmd/{secrets/init_single.go => gnoland/secrets_init_single.go} (97%) rename gno.land/cmd/{secrets/init_single_test.go => gnoland/secrets_init_single_test.go} (98%) rename gno.land/cmd/{secrets/show.go => gnoland/secrets_show.go} (98%) rename gno.land/cmd/{secrets/show_all.go => gnoland/secrets_show_all.go} (97%) rename gno.land/cmd/{secrets/show_all_test.go => gnoland/secrets_show_all_test.go} (98%) rename gno.land/cmd/{secrets/show_single.go => gnoland/secrets_show_single.go} (97%) rename gno.land/cmd/{secrets/show_single_test.go => gnoland/secrets_show_single_test.go} (98%) rename gno.land/cmd/{secrets/verify.go => gnoland/secrets_verify.go} (97%) rename gno.land/cmd/{secrets/verify_all.go => gnoland/secrets_verify_all.go} (97%) rename gno.land/cmd/{secrets/verify_all_test.go => gnoland/secrets_verify_all_test.go} (97%) rename gno.land/cmd/{secrets/verify_single.go => gnoland/secrets_verify_single.go} (97%) rename gno.land/cmd/{secrets/verify_single_test.go => gnoland/secrets_verify_single_test.go} (97%) diff --git a/gno.land/Makefile b/gno.land/Makefile index 50e21e7050b..8138dfcfe3e 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -17,7 +17,7 @@ start.gnoland:; go run ./cmd/gnoland start start.gnoweb:; go run ./cmd/gnoweb .PHONY: build -build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync build.genesis build.secrets +build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync build.genesis build.gnoland:; go build -o build/gnoland ./cmd/gnoland build.gnoweb:; go build -o build/gnoweb ./cmd/gnoweb @@ -25,13 +25,12 @@ build.gnofaucet:; go build -o build/gnofaucet ./cmd/gnofaucet build.gnokey:; go build -o build/gnokey ./cmd/gnokey build.gnotxsync:; go build -o build/gnotxsync ./cmd/gnotxsync build.genesis:; go build -o build/genesis ./cmd/genesis -build.secrets:; go build -o build/secrets ./cmd/secrets run.gnoland:; go run ./cmd/gnoland start run.gnoweb:; go run ./cmd/gnoweb .PHONY: install -install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync install.genesis install.secrets +install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync install.genesis install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb @@ -39,7 +38,6 @@ install.gnofaucet:; go install ./cmd/gnofaucet install.gnokey:; go install ./cmd/gnokey install.gnotxsync:; go install ./cmd/gnotxsync install.genesis:; go install ./cmd/genesis -install.secrets:; go install ./cmd/secrets .PHONY: fclean fclean: clean diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index 5b87b9452c7..9919d712112 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -31,6 +31,7 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newStartCmd(io), + newSecretsCmd(io), ) return cmd diff --git a/gno.land/cmd/secrets/main.go b/gno.land/cmd/gnoland/secrets.go similarity index 81% rename from gno.land/cmd/secrets/main.go rename to gno.land/cmd/gnoland/secrets.go index 48be808fc79..f1192327669 100644 --- a/gno.land/cmd/secrets/main.go +++ b/gno.land/cmd/gnoland/secrets.go @@ -1,10 +1,8 @@ package main import ( - "context" "errors" "flag" - "os" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -18,19 +16,14 @@ const ( defaultValidatorStateName = "priv_validator_state.json" ) -func main() { - io := commands.NewDefaultIO() - cmd := newRootCmd(io) - - cmd.Execute(context.Background(), os.Args[1:]) -} - -// newRootCmd creates the new secrets root command -func newRootCmd(io commands.IO) *commands.Command { +// newSecretsCmd creates the new secrets root command +func newSecretsCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ - ShortUsage: " [flags] [...]", - LongHelp: "Gno secrets manipulation suite", + Name: "secrets", + ShortUsage: "secrets [flags] [...]", + ShortHelp: "Gno secrets manipulation suite", + LongHelp: "Gno secrets manipulation suite, for managing the validator key, p2p key and validator state", }, commands.NewEmptyConfig(), commands.HelpExec, diff --git a/gno.land/cmd/secrets/common.go b/gno.land/cmd/gnoland/secrets_common.go similarity index 100% rename from gno.land/cmd/secrets/common.go rename to gno.land/cmd/gnoland/secrets_common.go diff --git a/gno.land/cmd/secrets/common_test.go b/gno.land/cmd/gnoland/secrets_common_test.go similarity index 100% rename from gno.land/cmd/secrets/common_test.go rename to gno.land/cmd/gnoland/secrets_common_test.go diff --git a/gno.land/cmd/secrets/init.go b/gno.land/cmd/gnoland/secrets_init.go similarity index 97% rename from gno.land/cmd/secrets/init.go rename to gno.land/cmd/gnoland/secrets_init.go index 8249f7e654d..3220027e820 100644 --- a/gno.land/cmd/secrets/init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -11,7 +11,7 @@ func newInitCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "init", - ShortUsage: "init [subcommand] [flags]", + ShortUsage: "secrets init [flags]", ShortHelp: "Initializes the Gno node secrets", LongHelp: "Initializes the Gno node secrets locally, including the validator key, validator state and node key", }, diff --git a/gno.land/cmd/secrets/init_all.go b/gno.land/cmd/gnoland/secrets_init_all.go similarity index 97% rename from gno.land/cmd/secrets/init_all.go rename to gno.land/cmd/gnoland/secrets_init_all.go index e81d61b9e63..3da02b1e1b0 100644 --- a/gno.land/cmd/secrets/init_all.go +++ b/gno.land/cmd/gnoland/secrets_init_all.go @@ -21,7 +21,7 @@ func newInitAllCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "all", - ShortUsage: "init all [flags]", + ShortUsage: "secrets init all [flags]", ShortHelp: "Initializes required Gno secrets in a common directory", LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state", }, diff --git a/gno.land/cmd/secrets/init_all_test.go b/gno.land/cmd/gnoland/secrets_init_all_test.go similarity index 98% rename from gno.land/cmd/secrets/init_all_test.go rename to gno.land/cmd/gnoland/secrets_init_all_test.go index 288ab85cf74..83ee67ad19a 100644 --- a/gno.land/cmd/secrets/init_all_test.go +++ b/gno.land/cmd/gnoland/secrets_init_all_test.go @@ -21,6 +21,7 @@ func TestSecrets_Init_All(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "init", "all", "--data-dir", @@ -41,6 +42,7 @@ func TestSecrets_Init_All(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "init", "all", "--data-dir", diff --git a/gno.land/cmd/secrets/init_single.go b/gno.land/cmd/gnoland/secrets_init_single.go similarity index 97% rename from gno.land/cmd/secrets/init_single.go rename to gno.land/cmd/gnoland/secrets_init_single.go index 091af064d72..0a7e889db5c 100644 --- a/gno.land/cmd/secrets/init_single.go +++ b/gno.land/cmd/gnoland/secrets_init_single.go @@ -21,7 +21,7 @@ func newInitSingleCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "single", - ShortUsage: "init single [flags]", + ShortUsage: "secrets init single [flags]", ShortHelp: "Initializes required Gno secrets individually", LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state" + " at custom paths", diff --git a/gno.land/cmd/secrets/init_single_test.go b/gno.land/cmd/gnoland/secrets_init_single_test.go similarity index 98% rename from gno.land/cmd/secrets/init_single_test.go rename to gno.land/cmd/gnoland/secrets_init_single_test.go index 8ea48ee935a..09af07857b6 100644 --- a/gno.land/cmd/secrets/init_single_test.go +++ b/gno.land/cmd/gnoland/secrets_init_single_test.go @@ -19,6 +19,7 @@ func TestSecrets_Init_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "init", "single", } @@ -69,6 +70,7 @@ func TestSecrets_Init_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "init", "single", testCase.flagValue, diff --git a/gno.land/cmd/secrets/show.go b/gno.land/cmd/gnoland/secrets_show.go similarity index 98% rename from gno.land/cmd/secrets/show.go rename to gno.land/cmd/gnoland/secrets_show.go index db455b6b760..defc0ad7406 100644 --- a/gno.land/cmd/secrets/show.go +++ b/gno.land/cmd/gnoland/secrets_show.go @@ -14,7 +14,7 @@ func newShowCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "show", - ShortUsage: "show [subcommand] [flags]", + ShortUsage: "secrets show [flags]", ShortHelp: "Shows the Gno node secrets", LongHelp: "Shows the Gno node secrets locally, including the validator key, validator state and node key", }, diff --git a/gno.land/cmd/secrets/show_all.go b/gno.land/cmd/gnoland/secrets_show_all.go similarity index 97% rename from gno.land/cmd/secrets/show_all.go rename to gno.land/cmd/gnoland/secrets_show_all.go index 812f385c9a3..15941a51f32 100644 --- a/gno.land/cmd/secrets/show_all.go +++ b/gno.land/cmd/gnoland/secrets_show_all.go @@ -19,7 +19,7 @@ func newShowAllCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "all", - ShortUsage: "show all [flags]", + ShortUsage: "secrets show all [flags]", ShortHelp: "Shows all Gno secrets in a common directory", LongHelp: "Shows the validator private key, the node p2p key and the validator's last sign state", }, diff --git a/gno.land/cmd/secrets/show_all_test.go b/gno.land/cmd/gnoland/secrets_show_all_test.go similarity index 98% rename from gno.land/cmd/secrets/show_all_test.go rename to gno.land/cmd/gnoland/secrets_show_all_test.go index 9fbf33c89cc..ca70626fcb4 100644 --- a/gno.land/cmd/secrets/show_all_test.go +++ b/gno.land/cmd/gnoland/secrets_show_all_test.go @@ -24,6 +24,7 @@ func TestSecrets_Show_All(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "show", "all", "--data-dir", @@ -46,6 +47,7 @@ func TestSecrets_Show_All(t *testing.T) { // Run the init command initArgs := []string{ + "secrets", "init", "all", "--data-dir", @@ -78,6 +80,7 @@ func TestSecrets_Show_All(t *testing.T) { // Run the show command showArgs := []string{ + "secrets", "show", "all", "--data-dir", diff --git a/gno.land/cmd/secrets/show_single.go b/gno.land/cmd/gnoland/secrets_show_single.go similarity index 97% rename from gno.land/cmd/secrets/show_single.go rename to gno.land/cmd/gnoland/secrets_show_single.go index d70d5f3393e..1b4c68102e8 100644 --- a/gno.land/cmd/secrets/show_single.go +++ b/gno.land/cmd/gnoland/secrets_show_single.go @@ -18,7 +18,7 @@ func newShowSingleCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "single", - ShortUsage: "show single [flags]", + ShortUsage: "secrets show single [flags]", ShortHelp: "Shows required Gno secrets individually", LongHelp: "Shows the validator private key, the node p2p key and the validator's last sign state" + " at custom paths", diff --git a/gno.land/cmd/secrets/show_single_test.go b/gno.land/cmd/gnoland/secrets_show_single_test.go similarity index 98% rename from gno.land/cmd/secrets/show_single_test.go rename to gno.land/cmd/gnoland/secrets_show_single_test.go index f585923c308..0db7c87f70a 100644 --- a/gno.land/cmd/secrets/show_single_test.go +++ b/gno.land/cmd/gnoland/secrets_show_single_test.go @@ -22,6 +22,7 @@ func TestSecrets_Show_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "show", "single", } @@ -48,6 +49,7 @@ func TestSecrets_Show_Single(t *testing.T) { // Create the command cmd := newRootCmd(io) args := []string{ + "secrets", "show", "single", "--validator-key-path", @@ -90,6 +92,7 @@ func TestSecrets_Show_Single(t *testing.T) { // Create the command cmd := newRootCmd(io) args := []string{ + "secrets", "show", "single", "--validator-state-path", @@ -138,6 +141,7 @@ func TestSecrets_Show_Single(t *testing.T) { // Create the command cmd := newRootCmd(io) args := []string{ + "secrets", "show", "single", "--node-key-path", diff --git a/gno.land/cmd/secrets/verify.go b/gno.land/cmd/gnoland/secrets_verify.go similarity index 97% rename from gno.land/cmd/secrets/verify.go rename to gno.land/cmd/gnoland/secrets_verify.go index 63caab00e17..7b0e6eefe27 100644 --- a/gno.land/cmd/secrets/verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -13,7 +13,7 @@ func newVerifyCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "verify", - ShortUsage: "verify [subcommand] [flags]", + ShortUsage: "secrets verify [flags]", ShortHelp: "Verifies the Gno node secrets", LongHelp: "Verifies the Gno node secrets locally, including the validator key, validator state and node key", }, diff --git a/gno.land/cmd/secrets/verify_all.go b/gno.land/cmd/gnoland/secrets_verify_all.go similarity index 97% rename from gno.land/cmd/secrets/verify_all.go rename to gno.land/cmd/gnoland/secrets_verify_all.go index 1d3f9623c36..dcd85d7d6c7 100644 --- a/gno.land/cmd/secrets/verify_all.go +++ b/gno.land/cmd/gnoland/secrets_verify_all.go @@ -19,7 +19,7 @@ func newVerifyAllCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "all", - ShortUsage: "verify all [flags]", + ShortUsage: "secrets verify all [flags]", ShortHelp: "Verifies all Gno secrets in a common directory", LongHelp: "Verifies the validator private key, the node p2p key and the validator's last sign state", }, diff --git a/gno.land/cmd/secrets/verify_all_test.go b/gno.land/cmd/gnoland/secrets_verify_all_test.go similarity index 97% rename from gno.land/cmd/secrets/verify_all_test.go rename to gno.land/cmd/gnoland/secrets_verify_all_test.go index e42640d1521..af3bcc11cbe 100644 --- a/gno.land/cmd/secrets/verify_all_test.go +++ b/gno.land/cmd/gnoland/secrets_verify_all_test.go @@ -21,6 +21,7 @@ func TestSecrets_Verify_All(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "all", "--data-dir", @@ -50,6 +51,7 @@ func TestSecrets_Verify_All(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "all", "--data-dir", @@ -72,6 +74,7 @@ func TestSecrets_Verify_All(t *testing.T) { // Run the init command initArgs := []string{ + "secrets", "init", "all", "--data-dir", @@ -95,6 +98,7 @@ func TestSecrets_Verify_All(t *testing.T) { // Run the verify command verifyArgs := []string{ + "secrets", "verify", "all", "--data-dir", @@ -119,6 +123,7 @@ func TestSecrets_Verify_All(t *testing.T) { // Run the init command initArgs := []string{ + "secrets", "init", "all", "--data-dir", @@ -132,6 +137,7 @@ func TestSecrets_Verify_All(t *testing.T) { // Run the verify command verifyArgs := []string{ + "secrets", "verify", "all", "--data-dir", @@ -181,6 +187,7 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { // Run the init command initArgs := []string{ + "secrets", "init", "all", "--data-dir", @@ -197,6 +204,7 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { // Run the verify command verifyArgs := []string{ + "secrets", "verify", "all", "--data-dir", @@ -222,6 +230,7 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { // Run the init command initArgs := []string{ + "secrets", "init", "all", "--data-dir", @@ -238,6 +247,7 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { // Run the verify command verifyArgs := []string{ + "secrets", "verify", "all", "--data-dir", diff --git a/gno.land/cmd/secrets/verify_single.go b/gno.land/cmd/gnoland/secrets_verify_single.go similarity index 97% rename from gno.land/cmd/secrets/verify_single.go rename to gno.land/cmd/gnoland/secrets_verify_single.go index c2bbc62ff8f..e702dcabec3 100644 --- a/gno.land/cmd/secrets/verify_single.go +++ b/gno.land/cmd/gnoland/secrets_verify_single.go @@ -19,7 +19,7 @@ func newVerifySingleCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "single", - ShortUsage: "verify single [flags]", + ShortUsage: "secrets verify single [flags]", ShortHelp: "Verifies required Gno secrets individually", LongHelp: "Verifies the validator private key, the node p2p key and the validator's last sign state" + " at custom paths", diff --git a/gno.land/cmd/secrets/verify_single_test.go b/gno.land/cmd/gnoland/secrets_verify_single_test.go similarity index 97% rename from gno.land/cmd/secrets/verify_single_test.go rename to gno.land/cmd/gnoland/secrets_verify_single_test.go index 8b057e60cd2..a93c6de9818 100644 --- a/gno.land/cmd/secrets/verify_single_test.go +++ b/gno.land/cmd/gnoland/secrets_verify_single_test.go @@ -21,6 +21,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", } @@ -36,6 +37,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", "--validator-key-path", @@ -62,6 +64,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", "--validator-key-path", @@ -88,6 +91,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", "--validator-state-path", @@ -118,6 +122,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", "--validator-key-path", @@ -146,6 +151,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", "--node-key-path", @@ -176,6 +182,7 @@ func TestSecrets_Verify_Single(t *testing.T) { // Create the command cmd := newRootCmd(commands.NewTestIO()) args := []string{ + "secrets", "verify", "single", "--validator-key-path", From 5367c44f5f01ba46826dcf2927bcf98f99908f25 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 28 Mar 2024 00:09:28 +0900 Subject: [PATCH 09/18] Standardize gnoland secrets --- gno.land/cmd/gnoland/secrets.go | 63 +++--- .../{secrets_show.go => secrets_get.go} | 84 ++++++-- ...s_show_all_test.go => secrets_get_test.go} | 142 ++++++++++++- gno.land/cmd/gnoland/secrets_init.go | 89 ++++++-- gno.land/cmd/gnoland/secrets_init_all.go | 71 ------- gno.land/cmd/gnoland/secrets_init_single.go | 75 ------- .../cmd/gnoland/secrets_init_single_test.go | 89 -------- ..._init_all_test.go => secrets_init_test.go} | 119 ++++++++--- gno.land/cmd/gnoland/secrets_show_all.go | 64 ------ gno.land/cmd/gnoland/secrets_show_single.go | 72 ------- .../cmd/gnoland/secrets_show_single_test.go | 163 -------------- gno.land/cmd/gnoland/secrets_verify.go | 101 +++++++-- gno.land/cmd/gnoland/secrets_verify_all.go | 71 ------- gno.land/cmd/gnoland/secrets_verify_single.go | 90 -------- .../cmd/gnoland/secrets_verify_single_test.go | 199 ------------------ ...ify_all_test.go => secrets_verify_test.go} | 127 ++++++++++- 16 files changed, 609 insertions(+), 1010 deletions(-) rename gno.land/cmd/gnoland/{secrets_show.go => secrets_get.go} (56%) rename gno.land/cmd/gnoland/{secrets_show_all_test.go => secrets_get_test.go} (50%) delete mode 100644 gno.land/cmd/gnoland/secrets_init_all.go delete mode 100644 gno.land/cmd/gnoland/secrets_init_single.go delete mode 100644 gno.land/cmd/gnoland/secrets_init_single_test.go rename gno.land/cmd/gnoland/{secrets_init_all_test.go => secrets_init_test.go} (65%) delete mode 100644 gno.land/cmd/gnoland/secrets_show_all.go delete mode 100644 gno.land/cmd/gnoland/secrets_show_single.go delete mode 100644 gno.land/cmd/gnoland/secrets_show_single_test.go delete mode 100644 gno.land/cmd/gnoland/secrets_verify_all.go delete mode 100644 gno.land/cmd/gnoland/secrets_verify_single.go delete mode 100644 gno.land/cmd/gnoland/secrets_verify_single_test.go rename gno.land/cmd/gnoland/{secrets_verify_all_test.go => secrets_verify_test.go} (65%) diff --git a/gno.land/cmd/gnoland/secrets.go b/gno.land/cmd/gnoland/secrets.go index f1192327669..16f6b270173 100644 --- a/gno.land/cmd/gnoland/secrets.go +++ b/gno.land/cmd/gnoland/secrets.go @@ -16,23 +16,29 @@ const ( defaultValidatorStateName = "priv_validator_state.json" ) -// newSecretsCmd creates the new secrets root command +const ( + nodeKeyKey = "NodeKey" + validatorPrivateKeyKey = "ValidatorPrivateKey" + validatorStateKey = "ValidatorStateKey" +) + +// newSecretsCmd creates the secrets root command func newSecretsCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "secrets", - ShortUsage: "secrets [flags] [...]", - ShortHelp: "Gno secrets manipulation suite", - LongHelp: "Gno secrets manipulation suite, for managing the validator key, p2p key and validator state", + ShortUsage: "gnoland secrets [flags] [...]", + ShortHelp: "gno secrets manipulation suite", + LongHelp: "gno secrets manipulation suite, for managing the validator key, p2p key and validator state", }, commands.NewEmptyConfig(), commands.HelpExec, ) cmd.AddSubCommands( - newInitCmd(io), - newVerifyCmd(io), - newShowCmd(io), + newSecretsInitCmd(io), + newSecretsVerifyCmd(io), + newSecretsGetCmd(io), ) return cmd @@ -54,34 +60,23 @@ func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) { ) } -// commonSingleCfg is the common -// configuration for secrets commands -// that require individual secret path management -type commonSingleCfg struct { - validatorKeyPath string - validatorStatePath string - nodeKeyPath string -} +// verifySecretsKey verifies the secrets key value from the passed in arguments +func verifySecretsKey(args []string) error { + if len(args) == 0 { + return nil + } -func (c *commonSingleCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.validatorKeyPath, - "validator-key-path", - "", - "the path to the validator private key", - ) + if len(args) > 1 { + return errors.New("invalid number of secret key arguments") + } - fs.StringVar( - &c.validatorStatePath, - "validator-state-path", - "", - "the path to the last validator state", - ) + key := args[0] - fs.StringVar( - &c.nodeKeyPath, - "node-key-path", - "", - "the path to the node p2p key", - ) + if key != nodeKeyKey && + key != validatorPrivateKeyKey && + key != validatorStateKey { + return errors.New("invalid secrets key value") + } + + return nil } diff --git a/gno.land/cmd/gnoland/secrets_show.go b/gno.land/cmd/gnoland/secrets_get.go similarity index 56% rename from gno.land/cmd/gnoland/secrets_show.go rename to gno.land/cmd/gnoland/secrets_get.go index defc0ad7406..1645bca6ea3 100644 --- a/gno.land/cmd/gnoland/secrets_show.go +++ b/gno.land/cmd/gnoland/secrets_get.go @@ -1,7 +1,10 @@ package main import ( + "context" + "flag" "fmt" + "path/filepath" "text/tabwriter" "github.com/gnolang/gno/tm2/pkg/bft/privval" @@ -9,25 +12,82 @@ import ( "github.com/gnolang/gno/tm2/pkg/p2p" ) -// newShowCmd creates the new secrets show command -func newShowCmd(io commands.IO) *commands.Command { +type secretsGetCfg struct { + commonAllCfg +} + +// newSecretsGetCmd creates the secrets get command +func newSecretsGetCmd(io commands.IO) *commands.Command { + cfg := &secretsGetCfg{} + cmd := commands.NewCommand( commands.Metadata{ - Name: "show", - ShortUsage: "secrets show [flags]", - ShortHelp: "Shows the Gno node secrets", - LongHelp: "Shows the Gno node secrets locally, including the validator key, validator state and node key", + Name: "get", + ShortUsage: "secrets get [flags] []", + ShortHelp: "shows all Gno secrets present in a common directory", + LongHelp: "shows the validator private key, the node p2p key and the validator's last sign state", + }, + cfg, + func(_ context.Context, args []string) error { + return execSecretsGet(cfg, args, io) }, - commands.NewEmptyConfig(), - commands.HelpExec, ) - cmd.AddSubCommands( - newShowAllCmd(io), - newShowSingleCmd(io), + return cmd +} + +func (c *secretsGetCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonAllCfg.RegisterFlags(fs) +} + +func execSecretsGet(cfg *secretsGetCfg, args []string, io commands.IO) error { + // Make sure the directory is there + if cfg.dataDir == "" || !isValidDirectory(cfg.dataDir) { + return errInvalidDataDir + } + + // Verify the secrets key + if err := verifySecretsKey(args); err != nil { + return err + } + + var key string + + if len(args) > 0 { + key = args[0] + } + + // Construct the paths + var ( + validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) + validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) + nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) ) - return cmd + switch key { + case validatorPrivateKeyKey: + // Show the validator's key info + return readAndShowValidatorKey(validatorKeyPath, io) + case validatorStateKey: + // Show the validator's last sign state + return readAndShowValidatorState(validatorStatePath, io) + case nodeKeyKey: + // Show the node's p2p info + return readAndShowNodeKey(nodeKeyPath, io) + default: + // Show the node's p2p info + if err := readAndShowNodeKey(nodeKeyPath, io); err != nil { + return err + } + + // Show the validator's key info + if err := readAndShowValidatorKey(validatorKeyPath, io); err != nil { + return err + } + + // Show the validator's last sign state + return readAndShowValidatorState(validatorStatePath, io) + } } // readAndShowValidatorKey reads and shows the validator key from the given path diff --git a/gno.land/cmd/gnoland/secrets_show_all_test.go b/gno.land/cmd/gnoland/secrets_get_test.go similarity index 50% rename from gno.land/cmd/gnoland/secrets_show_all_test.go rename to gno.land/cmd/gnoland/secrets_get_test.go index ca70626fcb4..20f1eb2ef35 100644 --- a/gno.land/cmd/gnoland/secrets_show_all_test.go +++ b/gno.land/cmd/gnoland/secrets_get_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSecrets_Show_All(t *testing.T) { +func TestSecrets_Get_All(t *testing.T) { t.Parallel() t.Run("invalid data directory", func(t *testing.T) { @@ -25,8 +25,7 @@ func TestSecrets_Show_All(t *testing.T) { cmd := newRootCmd(commands.NewTestIO()) args := []string{ "secrets", - "show", - "all", + "get", "--data-dir", "", } @@ -49,7 +48,6 @@ func TestSecrets_Show_All(t *testing.T) { initArgs := []string{ "secrets", "init", - "all", "--data-dir", tempDir, } @@ -81,8 +79,7 @@ func TestSecrets_Show_All(t *testing.T) { // Run the show command showArgs := []string{ "secrets", - "show", - "all", + "get", "--data-dir", tempDir, } @@ -144,3 +141,136 @@ func TestSecrets_Show_All(t *testing.T) { ) }) } + +func TestSecrets_Get_Single(t *testing.T) { + t.Parallel() + + t.Run("validator key shown", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + keyPath := filepath.Join(dirPath, defaultValidatorKeyName) + + validKey := generateValidatorPrivateKey() + + require.NoError(t, saveSecretData(validKey, keyPath)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + // Create the command + cmd := newRootCmd(io) + args := []string{ + "secrets", + "get", + "--data-dir", + dirPath, + validatorPrivateKeyKey, + } + + // Run the command + require.NoError(t, cmd.ParseAndRun(context.Background(), args)) + + output := mockOutput.String() + + // Make sure the private key info is displayed + assert.Contains( + t, + output, + validKey.Address.String(), + ) + + assert.Contains( + t, + output, + validKey.PubKey.String(), + ) + }) + + t.Run("validator state shown", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + statePath := filepath.Join(dirPath, defaultValidatorStateName) + + validState := generateLastSignValidatorState() + + require.NoError(t, saveSecretData(validState, statePath)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + // Create the command + cmd := newRootCmd(io) + args := []string{ + "secrets", + "get", + "--data-dir", + dirPath, + validatorStateKey, + } + + // Run the command + require.NoError(t, cmd.ParseAndRun(context.Background(), args)) + + output := mockOutput.String() + + // Make sure the state info is displayed + assert.Contains( + t, + output, + fmt.Sprintf("%d", validState.Step), + ) + + assert.Contains( + t, + output, + fmt.Sprintf("%d", validState.Height), + ) + + assert.Contains( + t, + output, + strconv.Itoa(validState.Round), + ) + }) + + t.Run("node key shown", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + nodeKeyPath := filepath.Join(dirPath, defaultNodeKeyName) + + validNodeKey := generateNodeKey() + + require.NoError(t, saveSecretData(validNodeKey, nodeKeyPath)) + + mockOutput := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(mockOutput)) + + // Create the command + cmd := newRootCmd(io) + args := []string{ + "secrets", + "get", + "--data-dir", + dirPath, + nodeKeyKey, + } + + // Run the command + require.NoError(t, cmd.ParseAndRun(context.Background(), args)) + + output := mockOutput.String() + + // Make sure the node p2p key is displayed + assert.Contains( + t, + output, + validNodeKey.ID().String(), + ) + }) +} diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index 3220027e820..5e5dfa2c1b4 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -1,30 +1,95 @@ package main import ( + "context" + "flag" "fmt" + "os" + "path/filepath" "github.com/gnolang/gno/tm2/pkg/commands" ) -// newInitCmd creates the new secrets init command -func newInitCmd(io commands.IO) *commands.Command { - cmd := commands.NewCommand( +type secretsInitCfg struct { + commonAllCfg +} + +// newSecretsInitCmd creates the secrets init command +func newSecretsInitCmd(io commands.IO) *commands.Command { + cfg := &secretsInitCfg{} + + return commands.NewCommand( commands.Metadata{ Name: "init", - ShortUsage: "secrets init [flags]", - ShortHelp: "Initializes the Gno node secrets", - LongHelp: "Initializes the Gno node secrets locally, including the validator key, validator state and node key", + ShortUsage: "secrets init [flags] []", + ShortHelp: "initializes required Gno secrets in a common directory", + LongHelp: "initializes the validator private key, the node p2p key and the validator's last sign state", + }, + cfg, + func(_ context.Context, args []string) error { + return execSecretsInit(cfg, args, io) }, - commands.NewEmptyConfig(), - commands.HelpExec, ) +} + +func (c *secretsInitCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonAllCfg.RegisterFlags(fs) +} + +func execSecretsInit(cfg *secretsInitCfg, args []string, io commands.IO) error { + // Check the data output directory path + if cfg.dataDir == "" { + return errInvalidDataDir + } - cmd.AddSubCommands( - newInitAllCmd(io), - newInitSingleCmd(io), + // Verify the secrets key + if err := verifySecretsKey(args); err != nil { + return err + } + + var key string + + if len(args) > 0 { + key = args[0] + } + + // Make sure the directory is there + if err := os.MkdirAll(cfg.dataDir, 0o755); err != nil { + return fmt.Errorf("unable to create secrets dir, %w", err) + } + + // Construct the paths + var ( + validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) + validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) + nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) ) - return cmd + switch key { + case validatorPrivateKeyKey: + // Initialize and save the validator's private key + return initAndSaveValidatorKey(validatorKeyPath, io) + case nodeKeyKey: + // Initialize and save the node's p2p key + return initAndSaveNodeKey(nodeKeyPath, io) + case validatorStateKey: + // Initialize and save the validator's last sign state + return initAndSaveValidatorState(validatorStatePath, io) + default: + // No key provided, initialize everything + // Initialize and save the validator's private key + if err := initAndSaveValidatorKey(validatorKeyPath, io); err != nil { + return err + } + + // Initialize and save the validator's last sign state + if err := initAndSaveValidatorState(validatorStatePath, io); err != nil { + return err + } + + // Initialize and save the node's p2p key + return initAndSaveNodeKey(nodeKeyPath, io) + } } // initAndSaveValidatorKey generates a validator private key and saves it to the given path diff --git a/gno.land/cmd/gnoland/secrets_init_all.go b/gno.land/cmd/gnoland/secrets_init_all.go deleted file mode 100644 index 3da02b1e1b0..00000000000 --- a/gno.land/cmd/gnoland/secrets_init_all.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "os" - "path/filepath" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type initAllCfg struct { - commonAllCfg -} - -// newInitAllCmd creates the secrets init all command -func newInitAllCmd(io commands.IO) *commands.Command { - cfg := &initAllCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "all", - ShortUsage: "secrets init all [flags]", - ShortHelp: "Initializes required Gno secrets in a common directory", - LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state", - }, - cfg, - func(_ context.Context, _ []string) error { - return execInitAll(cfg, io) - }, - ) - - return cmd -} - -func (c *initAllCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonAllCfg.RegisterFlags(fs) -} - -func execInitAll(cfg *initAllCfg, io commands.IO) error { - // Check the data output directory path - if cfg.dataDir == "" { - return errInvalidDataDir - } - - // Make sure the directory is there - if err := os.MkdirAll(cfg.dataDir, 0o755); err != nil { - return fmt.Errorf("unable to create secrets dir, %w", err) - } - - // Construct the paths - var ( - validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) - validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) - nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) - ) - - // Initialize and save the validator's private key - if err := initAndSaveValidatorKey(validatorKeyPath, io); err != nil { - return err - } - - // Initialize and save the validator's last sign state - if err := initAndSaveValidatorState(validatorStatePath, io); err != nil { - return err - } - - // Initialize and save the node's p2p key - return initAndSaveNodeKey(nodeKeyPath, io) -} diff --git a/gno.land/cmd/gnoland/secrets_init_single.go b/gno.land/cmd/gnoland/secrets_init_single.go deleted file mode 100644 index 0a7e889db5c..00000000000 --- a/gno.land/cmd/gnoland/secrets_init_single.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -var errNoOutputSet = errors.New("no individual output path set") - -type initSingleCfg struct { - commonSingleCfg -} - -// newInitSingleCmd creates the secrets init single command -func newInitSingleCmd(io commands.IO) *commands.Command { - cfg := &initSingleCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "single", - ShortUsage: "secrets init single [flags]", - ShortHelp: "Initializes required Gno secrets individually", - LongHelp: "Initializes the validator private key, the node p2p key and the validator's last sign state" + - " at custom paths", - }, - cfg, - func(_ context.Context, _ []string) error { - return execInitSingle(cfg, io) - }, - ) - - return cmd -} - -func (c *initSingleCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonSingleCfg.RegisterFlags(fs) -} - -func execInitSingle(cfg *initSingleCfg, io commands.IO) error { - var ( - validatorKeyPathSet = cfg.validatorKeyPath != "" - validatorStatePathSet = cfg.validatorStatePath != "" - nodeKeyPathSet = cfg.nodeKeyPath != "" - ) - - if !validatorKeyPathSet && !validatorStatePathSet && !nodeKeyPathSet { - return errNoOutputSet - } - - // Save the validator private key, if any - if validatorKeyPathSet { - if err := initAndSaveValidatorKey(cfg.validatorKeyPath, io); err != nil { - return err - } - } - - // Save the last validator sign state, if any - if validatorStatePathSet { - if err := initAndSaveValidatorState(cfg.validatorStatePath, io); err != nil { - return err - } - } - - // Save the node key, if any - if nodeKeyPathSet { - if err := initAndSaveNodeKey(cfg.nodeKeyPath, io); err != nil { - return err - } - } - - return nil -} diff --git a/gno.land/cmd/gnoland/secrets_init_single_test.go b/gno.land/cmd/gnoland/secrets_init_single_test.go deleted file mode 100644 index 09af07857b6..00000000000 --- a/gno.land/cmd/gnoland/secrets_init_single_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "context" - "path/filepath" - "testing" - - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSecrets_Init_Single(t *testing.T) { - t.Parallel() - - t.Run("no individual path set", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "init", - "single", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoOutputSet.Error()) - }) - - t.Run("individual secrets initialized", func(t *testing.T) { - t.Parallel() - - testTable := []struct { - name string - flagValue string - verifyFn func(*testing.T, string) - }{ - { - "validator key initialized", - "--validator-key-path", - verifyValidatorKey, - }, - { - "validator state initialized", - "--validator-state-path", - verifyValidatorState, - }, - { - "node p2p initialized", - "--node-key-path", - verifyNodeKey, - }, - } - - for _, testCase := range testTable { - testCase := testCase - - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - var ( - tempDir = t.TempDir() - dataName = "data.json" - - expectedPath = filepath.Join(tempDir, dataName) - ) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "init", - "single", - testCase.flagValue, - expectedPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Verify the validator key is saved - testCase.verifyFn(t, expectedPath) - }) - } - }) -} diff --git a/gno.land/cmd/gnoland/secrets_init_all_test.go b/gno.land/cmd/gnoland/secrets_init_test.go similarity index 65% rename from gno.land/cmd/gnoland/secrets_init_all_test.go rename to gno.land/cmd/gnoland/secrets_init_test.go index 83ee67ad19a..83a31ba8a22 100644 --- a/gno.land/cmd/gnoland/secrets_init_all_test.go +++ b/gno.land/cmd/gnoland/secrets_init_test.go @@ -12,6 +12,37 @@ import ( "github.com/stretchr/testify/require" ) +func verifyValidatorKey(t *testing.T, path string) { + t.Helper() + + validatorKey, err := readSecretData[privval.FilePVKey](path) + require.NoError(t, err) + + assert.NoError(t, validateValidatorKey(validatorKey)) +} + +func verifyValidatorState(t *testing.T, path string) { + t.Helper() + + validatorState, err := readSecretData[privval.FilePVLastSignState](path) + require.NoError(t, err) + + assert.Zero(t, validatorState.Height) + assert.Zero(t, validatorState.Round) + assert.Zero(t, validatorState.Step) + assert.Nil(t, validatorState.Signature) + assert.Nil(t, validatorState.SignBytes) +} + +func verifyNodeKey(t *testing.T, path string) { + t.Helper() + + nodeKey, err := readSecretData[p2p.NodeKey](path) + require.NoError(t, err) + + assert.NoError(t, validateNodeKey(nodeKey)) +} + func TestSecrets_Init_All(t *testing.T) { t.Parallel() @@ -23,7 +54,6 @@ func TestSecrets_Init_All(t *testing.T) { args := []string{ "secrets", "init", - "all", "--data-dir", "", } @@ -44,7 +74,6 @@ func TestSecrets_Init_All(t *testing.T) { args := []string{ "secrets", "init", - "all", "--data-dir", tempDir, } @@ -64,33 +93,63 @@ func TestSecrets_Init_All(t *testing.T) { }) } -func verifyValidatorKey(t *testing.T, path string) { - t.Helper() - - validatorKey, err := readSecretData[privval.FilePVKey](path) - require.NoError(t, err) - - assert.NoError(t, validateValidatorKey(validatorKey)) -} - -func verifyValidatorState(t *testing.T, path string) { - t.Helper() - - validatorState, err := readSecretData[privval.FilePVLastSignState](path) - require.NoError(t, err) - - assert.Zero(t, validatorState.Height) - assert.Zero(t, validatorState.Round) - assert.Zero(t, validatorState.Step) - assert.Nil(t, validatorState.Signature) - assert.Nil(t, validatorState.SignBytes) -} - -func verifyNodeKey(t *testing.T, path string) { - t.Helper() - - nodeKey, err := readSecretData[p2p.NodeKey](path) - require.NoError(t, err) +func TestSecrets_Init_Single(t *testing.T) { + t.Parallel() - assert.NoError(t, validateNodeKey(nodeKey)) + testTable := []struct { + name string + keyValue string + expectedFile string + verifyFn func(*testing.T, string) + }{ + { + "validator key initialized", + validatorPrivateKeyKey, + defaultValidatorKeyName, + verifyValidatorKey, + }, + { + "validator state initialized", + validatorStateKey, + defaultValidatorStateName, + verifyValidatorState, + }, + { + "node p2p initialized", + nodeKeyKey, + defaultNodeKeyName, + verifyNodeKey, + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + tempDir = t.TempDir() + + expectedPath = filepath.Join(tempDir, testCase.expectedFile) + ) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "init", + "--data-dir", + tempDir, + testCase.keyValue, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the validator key is saved + testCase.verifyFn(t, expectedPath) + }) + } } diff --git a/gno.land/cmd/gnoland/secrets_show_all.go b/gno.land/cmd/gnoland/secrets_show_all.go deleted file mode 100644 index 15941a51f32..00000000000 --- a/gno.land/cmd/gnoland/secrets_show_all.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "context" - "flag" - "path/filepath" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type showAllCfg struct { - commonAllCfg -} - -// newShowAllCmd creates the secrets show all command -func newShowAllCmd(io commands.IO) *commands.Command { - cfg := &showAllCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "all", - ShortUsage: "secrets show all [flags]", - ShortHelp: "Shows all Gno secrets in a common directory", - LongHelp: "Shows the validator private key, the node p2p key and the validator's last sign state", - }, - cfg, - func(_ context.Context, _ []string) error { - return execShowAll(cfg, io) - }, - ) - - return cmd -} - -func (c *showAllCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonAllCfg.RegisterFlags(fs) -} - -func execShowAll(cfg *showAllCfg, io commands.IO) error { - // Make sure the directory is there - if cfg.dataDir == "" || !isValidDirectory(cfg.dataDir) { - return errInvalidDataDir - } - - // Construct the paths - var ( - validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) - validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) - nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) - ) - - // Show the node's p2p info - if err := readAndShowNodeKey(nodeKeyPath, io); err != nil { - return err - } - - // Show the validator's key info - if err := readAndShowValidatorKey(validatorKeyPath, io); err != nil { - return err - } - - // Show the validator's last sign state - return readAndShowValidatorState(validatorStatePath, io) -} diff --git a/gno.land/cmd/gnoland/secrets_show_single.go b/gno.land/cmd/gnoland/secrets_show_single.go deleted file mode 100644 index 1b4c68102e8..00000000000 --- a/gno.land/cmd/gnoland/secrets_show_single.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "context" - "flag" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type ShowSingleCfg struct { - commonSingleCfg -} - -// newShowSingleCmd creates the secrets show single command -func newShowSingleCmd(io commands.IO) *commands.Command { - cfg := &ShowSingleCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "single", - ShortUsage: "secrets show single [flags]", - ShortHelp: "Shows required Gno secrets individually", - LongHelp: "Shows the validator private key, the node p2p key and the validator's last sign state" + - " at custom paths", - }, - cfg, - func(_ context.Context, _ []string) error { - return execShowSingle(cfg, io) - }, - ) - - return cmd -} - -func (c *ShowSingleCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonSingleCfg.RegisterFlags(fs) -} - -func execShowSingle(cfg *ShowSingleCfg, io commands.IO) error { - var ( - validatorKeyPathSet = cfg.validatorKeyPath != "" - validatorStatePathSet = cfg.validatorStatePath != "" - nodeKeyPathSet = cfg.nodeKeyPath != "" - ) - - if !validatorKeyPathSet && !validatorStatePathSet && !nodeKeyPathSet { - return errNoOutputSet - } - - // Show the validator private key info, if any - if validatorKeyPathSet { - if err := readAndShowValidatorKey(cfg.validatorKeyPath, io); err != nil { - return err - } - } - - // Show the last validator sign state, if any - if validatorStatePathSet { - if err := readAndShowValidatorState(cfg.validatorStatePath, io); err != nil { - return err - } - } - - // Show the node key, if any - if nodeKeyPathSet { - if err := readAndShowNodeKey(cfg.nodeKeyPath, io); err != nil { - return err - } - } - - return nil -} diff --git a/gno.land/cmd/gnoland/secrets_show_single_test.go b/gno.land/cmd/gnoland/secrets_show_single_test.go deleted file mode 100644 index 0db7c87f70a..00000000000 --- a/gno.land/cmd/gnoland/secrets_show_single_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "path/filepath" - "strconv" - "testing" - - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSecrets_Show_Single(t *testing.T) { - t.Parallel() - - t.Run("no individual path set", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "show", - "single", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoOutputSet.Error()) - }) - - t.Run("validator key shown", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - keyPath := filepath.Join(dirPath, "key.json") - - validKey := generateValidatorPrivateKey() - - require.NoError(t, saveSecretData(validKey, keyPath)) - - mockOutput := bytes.NewBufferString("") - io := commands.NewTestIO() - io.SetOut(commands.WriteNopCloser(mockOutput)) - - // Create the command - cmd := newRootCmd(io) - args := []string{ - "secrets", - "show", - "single", - "--validator-key-path", - keyPath, - } - - // Run the command - require.NoError(t, cmd.ParseAndRun(context.Background(), args)) - - output := mockOutput.String() - - // Make sure the private key info is displayed - assert.Contains( - t, - output, - validKey.Address.String(), - ) - - assert.Contains( - t, - output, - validKey.PubKey.String(), - ) - }) - - t.Run("validator state shown", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - statePath := filepath.Join(dirPath, "state.json") - - validState := generateLastSignValidatorState() - - require.NoError(t, saveSecretData(validState, statePath)) - - mockOutput := bytes.NewBufferString("") - io := commands.NewTestIO() - io.SetOut(commands.WriteNopCloser(mockOutput)) - - // Create the command - cmd := newRootCmd(io) - args := []string{ - "secrets", - "show", - "single", - "--validator-state-path", - statePath, - } - - // Run the command - require.NoError(t, cmd.ParseAndRun(context.Background(), args)) - - output := mockOutput.String() - - // Make sure the state info is displayed - assert.Contains( - t, - output, - fmt.Sprintf("%d", validState.Step), - ) - - assert.Contains( - t, - output, - fmt.Sprintf("%d", validState.Height), - ) - - assert.Contains( - t, - output, - strconv.Itoa(validState.Round), - ) - }) - - t.Run("node key shown", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - nodeKeyPath := filepath.Join(dirPath, "p2p.json") - - validNodeKey := generateNodeKey() - - require.NoError(t, saveSecretData(validNodeKey, nodeKeyPath)) - - mockOutput := bytes.NewBufferString("") - io := commands.NewTestIO() - io.SetOut(commands.WriteNopCloser(mockOutput)) - - // Create the command - cmd := newRootCmd(io) - args := []string{ - "secrets", - "show", - "single", - "--node-key-path", - nodeKeyPath, - } - - // Run the command - require.NoError(t, cmd.ParseAndRun(context.Background(), args)) - - output := mockOutput.String() - - // Make sure the node p2p key is displayed - assert.Contains( - t, - output, - validNodeKey.ID().String(), - ) - }) -} diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go index 7b0e6eefe27..0f0fb1d7744 100644 --- a/gno.land/cmd/gnoland/secrets_verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -1,32 +1,109 @@ package main import ( + "context" + "flag" "fmt" + "path/filepath" "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/p2p" ) -// newVerifyCmd creates the new secrets verify command -func newVerifyCmd(io commands.IO) *commands.Command { - cmd := commands.NewCommand( +type secretsVerifyCfg struct { + commonAllCfg +} + +// newSecretsVerifyCmd creates the secrets verify command +func newSecretsVerifyCmd(io commands.IO) *commands.Command { + cfg := &secretsVerifyCfg{} + + return commands.NewCommand( commands.Metadata{ Name: "verify", - ShortUsage: "secrets verify [flags]", - ShortHelp: "Verifies the Gno node secrets", - LongHelp: "Verifies the Gno node secrets locally, including the validator key, validator state and node key", + ShortUsage: "secrets verify [flags] []", + ShortHelp: "verifies all Gno secrets in a common directory", + LongHelp: "verifies the validator private key, the node p2p key and the validator's last sign state", + }, + cfg, + func(_ context.Context, args []string) error { + return execSecretsVerify(cfg, args, io) }, - commands.NewEmptyConfig(), - commands.HelpExec, ) +} + +func (c *secretsVerifyCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonAllCfg.RegisterFlags(fs) +} + +func execSecretsVerify(cfg *secretsVerifyCfg, args []string, io commands.IO) error { + // Make sure the directory is there + if cfg.dataDir == "" || !isValidDirectory(cfg.dataDir) { + return errInvalidDataDir + } - cmd.AddSubCommands( - newVerifyAllCmd(io), - newVerifySingleCmd(io), + // Verify the secrets key + if err := verifySecretsKey(args); err != nil { + return err + } + + var key string + + if len(args) > 0 { + key = args[0] + } + + // Construct the paths + var ( + validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) + validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) + nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) ) - return cmd + switch key { + case validatorPrivateKeyKey: + // Validate the validator's private key + _, err := readAndVerifyValidatorKey(validatorKeyPath, io) + + return err + case validatorStateKey: + // Validate the validator's last sign state + validatorState, err := readAndVerifyValidatorState(validatorStatePath, io) + if err != nil { + return err + } + + // Attempt to read the validator key + if validatorKey, err := readAndVerifyValidatorKey(validatorKeyPath, io); validatorKey != nil && err == nil { + // Validate the signature bytes + return validateValidatorStateSignature(validatorState, validatorKey.PubKey) + } + + return nil + case nodeKeyKey: + return readAndVerifyNodeKey(nodeKeyPath, io) + default: + // Validate the validator's private key + validatorKey, err := readAndVerifyValidatorKey(validatorKeyPath, io) + if err != nil { + return err + } + + // Validate the validator's last sign state + validatorState, err := readAndVerifyValidatorState(validatorStatePath, io) + if err != nil { + return err + } + + // Validate the signature bytes + if err = validateValidatorStateSignature(validatorState, validatorKey.PubKey); err != nil { + return err + } + + // Validate the node's p2p key + return readAndVerifyNodeKey(nodeKeyPath, io) + } } // readAndVerifyValidatorKey reads the validator key from the given path and verifies it diff --git a/gno.land/cmd/gnoland/secrets_verify_all.go b/gno.land/cmd/gnoland/secrets_verify_all.go deleted file mode 100644 index dcd85d7d6c7..00000000000 --- a/gno.land/cmd/gnoland/secrets_verify_all.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "context" - "flag" - "path/filepath" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type verifyAllCfg struct { - commonAllCfg -} - -// newVerifyAllCmd creates the secrets verify all command -func newVerifyAllCmd(io commands.IO) *commands.Command { - cfg := &verifyAllCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "all", - ShortUsage: "secrets verify all [flags]", - ShortHelp: "Verifies all Gno secrets in a common directory", - LongHelp: "Verifies the validator private key, the node p2p key and the validator's last sign state", - }, - cfg, - func(_ context.Context, _ []string) error { - return execVerifyAll(cfg, io) - }, - ) - - return cmd -} - -func (c *verifyAllCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonAllCfg.RegisterFlags(fs) -} - -func execVerifyAll(cfg *verifyAllCfg, io commands.IO) error { - // Make sure the directory is there - if cfg.dataDir == "" || !isValidDirectory(cfg.dataDir) { - return errInvalidDataDir - } - - // Construct the paths - var ( - validatorKeyPath = filepath.Join(cfg.dataDir, defaultValidatorKeyName) - validatorStatePath = filepath.Join(cfg.dataDir, defaultValidatorStateName) - nodeKeyPath = filepath.Join(cfg.dataDir, defaultNodeKeyName) - ) - - // Validate the validator's private key - validatorKey, err := readAndVerifyValidatorKey(validatorKeyPath, io) - if err != nil { - return err - } - - // Validate the validator's last sign state - validatorState, err := readAndVerifyValidatorState(validatorStatePath, io) - if err != nil { - return err - } - - // Validate the signature bytes - if err = validateValidatorStateSignature(validatorState, validatorKey.PubKey); err != nil { - return err - } - - // Validate the node's p2p key - return readAndVerifyNodeKey(nodeKeyPath, io) -} diff --git a/gno.land/cmd/gnoland/secrets_verify_single.go b/gno.land/cmd/gnoland/secrets_verify_single.go deleted file mode 100644 index e702dcabec3..00000000000 --- a/gno.land/cmd/gnoland/secrets_verify_single.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -import ( - "context" - "flag" - - "github.com/gnolang/gno/tm2/pkg/bft/privval" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type verifySingleCfg struct { - commonSingleCfg -} - -// newVerifySingleCmd creates the secrets verify single command -func newVerifySingleCmd(io commands.IO) *commands.Command { - cfg := &verifySingleCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "single", - ShortUsage: "secrets verify single [flags]", - ShortHelp: "Verifies required Gno secrets individually", - LongHelp: "Verifies the validator private key, the node p2p key and the validator's last sign state" + - " at custom paths", - }, - cfg, - func(_ context.Context, _ []string) error { - return execVerifySingle(cfg, io) - }, - ) - - return cmd -} - -func (c *verifySingleCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonSingleCfg.RegisterFlags(fs) -} - -func execVerifySingle(cfg *verifySingleCfg, io commands.IO) error { - var ( - validatorKeyPathSet = cfg.validatorKeyPath != "" - validatorStatePathSet = cfg.validatorStatePath != "" - nodeKeyPathSet = cfg.nodeKeyPath != "" - ) - - var ( - validatorKey *privval.FilePVKey - validatorState *privval.FilePVLastSignState - - err error - ) - - if !validatorKeyPathSet && !validatorStatePathSet && !nodeKeyPathSet { - return errNoOutputSet - } - - // Verify the validator private key, if any - if validatorKeyPathSet { - validatorKey, err = readAndVerifyValidatorKey(cfg.validatorKeyPath, io) - if err != nil { - return err - } - } - - // Verify the last validator sign state, if any - if validatorStatePathSet { - validatorState, err = readAndVerifyValidatorState(cfg.validatorStatePath, io) - if err != nil { - return err - } - } - - // Verify the signature bytes if the key and validator state - // is provided - if validatorKey != nil && validatorState != nil { - if err = validateValidatorStateSignature(validatorState, validatorKey.PubKey); err != nil { - return err - } - } - - // Verify the node key, if any - if nodeKeyPathSet { - if err = readAndVerifyNodeKey(cfg.nodeKeyPath, io); err != nil { - return err - } - } - - return nil -} diff --git a/gno.land/cmd/gnoland/secrets_verify_single_test.go b/gno.land/cmd/gnoland/secrets_verify_single_test.go deleted file mode 100644 index a93c6de9818..00000000000 --- a/gno.land/cmd/gnoland/secrets_verify_single_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package main - -import ( - "context" - "path/filepath" - "testing" - - "github.com/gnolang/gno/tm2/pkg/bft/privval" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSecrets_Verify_Single(t *testing.T) { - t.Parallel() - - t.Run("no individual path set", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoOutputSet.Error()) - }) - - t.Run("invalid validator key path", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - "--validator-key-path", - "random path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, "unable to read validator key") - }) - - t.Run("invalid validator key", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - path := filepath.Join(dirPath, "data.json") - - invalidKey := &privval.FilePVKey{ - PrivKey: nil, // invalid - } - - require.NoError(t, saveSecretData(invalidKey, path)) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - "--validator-key-path", - path, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errInvalidPrivateKey) - }) - - t.Run("invalid validator state", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - path := filepath.Join(dirPath, "data.json") - - invalidState := &privval.FilePVLastSignState{ - Height: -1, // invalid - } - - require.NoError(t, saveSecretData(invalidState, path)) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - "--validator-state-path", - path, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errInvalidSignStateHeight) - }) - - t.Run("invalid validator state signature", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - keyPath := filepath.Join(dirPath, "key.json") - statePath := filepath.Join(dirPath, "state.json") - - validKey := generateValidatorPrivateKey() - validState := generateLastSignValidatorState() - - // Save an invalid signature - validState.Signature = []byte("totally valid signature") - - require.NoError(t, saveSecretData(validKey, keyPath)) - require.NoError(t, saveSecretData(validState, statePath)) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - "--validator-key-path", - keyPath, - "--validator-state-path", - statePath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errSignatureValuesMissing) - }) - - t.Run("invalid node key", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - path := filepath.Join(dirPath, "data.json") - - invalidNodeKey := &p2p.NodeKey{ - PrivKey: nil, // invalid - } - - require.NoError(t, saveSecretData(invalidNodeKey, path)) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - "--node-key-path", - path, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errInvalidNodeKey) - }) - - t.Run("all secrets set and valid", func(t *testing.T) { - t.Parallel() - - dirPath := t.TempDir() - keyPath := filepath.Join(dirPath, "key.json") - statePath := filepath.Join(dirPath, "state.json") - nodeKeyPath := filepath.Join(dirPath, "p2p.json") - - validKey := generateValidatorPrivateKey() - validState := generateLastSignValidatorState() - validNodeKey := generateNodeKey() - - require.NoError(t, saveSecretData(validKey, keyPath)) - require.NoError(t, saveSecretData(validState, statePath)) - require.NoError(t, saveSecretData(validNodeKey, nodeKeyPath)) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "secrets", - "verify", - "single", - "--validator-key-path", - keyPath, - "--validator-state-path", - statePath, - "--node-key-path", - nodeKeyPath, - } - - // Run the command - assert.NoError(t, cmd.ParseAndRun(context.Background(), args)) - }) -} diff --git a/gno.land/cmd/gnoland/secrets_verify_all_test.go b/gno.land/cmd/gnoland/secrets_verify_test.go similarity index 65% rename from gno.land/cmd/gnoland/secrets_verify_all_test.go rename to gno.land/cmd/gnoland/secrets_verify_test.go index af3bcc11cbe..77ab8523d40 100644 --- a/gno.land/cmd/gnoland/secrets_verify_all_test.go +++ b/gno.land/cmd/gnoland/secrets_verify_test.go @@ -8,6 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,7 +24,6 @@ func TestSecrets_Verify_All(t *testing.T) { args := []string{ "secrets", "verify", - "all", "--data-dir", "", } @@ -53,7 +53,6 @@ func TestSecrets_Verify_All(t *testing.T) { args := []string{ "secrets", "verify", - "all", "--data-dir", path, } @@ -76,7 +75,6 @@ func TestSecrets_Verify_All(t *testing.T) { initArgs := []string{ "secrets", "init", - "all", "--data-dir", tempDir, } @@ -100,7 +98,6 @@ func TestSecrets_Verify_All(t *testing.T) { verifyArgs := []string{ "secrets", "verify", - "all", "--data-dir", tempDir, } @@ -125,7 +122,6 @@ func TestSecrets_Verify_All(t *testing.T) { initArgs := []string{ "secrets", "init", - "all", "--data-dir", tempDir, } @@ -139,7 +135,6 @@ func TestSecrets_Verify_All(t *testing.T) { verifyArgs := []string{ "secrets", "verify", - "all", "--data-dir", tempDir, } @@ -189,7 +184,6 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { initArgs := []string{ "secrets", "init", - "all", "--data-dir", tempDir, } @@ -206,7 +200,6 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { verifyArgs := []string{ "secrets", "verify", - "all", "--data-dir", tempDir, } @@ -232,7 +225,6 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { initArgs := []string{ "secrets", "init", - "all", "--data-dir", tempDir, } @@ -249,7 +241,6 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { verifyArgs := []string{ "secrets", "verify", - "all", "--data-dir", tempDir, } @@ -261,3 +252,119 @@ func TestSecrets_Verify_All_Missing(t *testing.T) { ) }) } + +func TestSecrets_Verify_Single(t *testing.T) { + t.Parallel() + + t.Run("invalid validator key", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + path := filepath.Join(dirPath, defaultValidatorKeyName) + + invalidKey := &privval.FilePVKey{ + PrivKey: nil, // invalid + } + + require.NoError(t, saveSecretData(invalidKey, path)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "verify", + "--data-dir", + dirPath, + validatorPrivateKeyKey, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidPrivateKey) + }) + + t.Run("invalid validator state", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + path := filepath.Join(dirPath, defaultValidatorStateName) + + invalidState := &privval.FilePVLastSignState{ + Height: -1, // invalid + } + + require.NoError(t, saveSecretData(invalidState, path)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "verify", + "--data-dir", + dirPath, + validatorStateKey, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidSignStateHeight) + }) + + t.Run("invalid validator state signature", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + keyPath := filepath.Join(dirPath, defaultValidatorKeyName) + statePath := filepath.Join(dirPath, defaultValidatorStateName) + + validKey := generateValidatorPrivateKey() + validState := generateLastSignValidatorState() + + // Save an invalid signature + validState.Signature = []byte("totally valid signature") + + require.NoError(t, saveSecretData(validKey, keyPath)) + require.NoError(t, saveSecretData(validState, statePath)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "verify", + "--data-dir", + dirPath, + validatorStateKey, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errSignatureValuesMissing) + }) + + t.Run("invalid node key", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + path := filepath.Join(dirPath, defaultNodeKeyName) + + invalidNodeKey := &p2p.NodeKey{ + PrivKey: nil, // invalid + } + + require.NoError(t, saveSecretData(invalidNodeKey, path)) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "verify", + "--data-dir", + dirPath, + nodeKeyKey, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidNodeKey) + }) +} From 08660a5499b07c85fb209dc2031d82946eefaeb1 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 28 Mar 2024 00:22:00 +0900 Subject: [PATCH 10/18] Tidy error handling --- gno.land/cmd/gnoland/secrets.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gno.land/cmd/gnoland/secrets.go b/gno.land/cmd/gnoland/secrets.go index 16f6b270173..8d5cbec6d5d 100644 --- a/gno.land/cmd/gnoland/secrets.go +++ b/gno.land/cmd/gnoland/secrets.go @@ -3,11 +3,15 @@ package main import ( "errors" "flag" + "fmt" "github.com/gnolang/gno/tm2/pkg/commands" ) -var errInvalidDataDir = errors.New("invalid data directory provided") +var ( + errInvalidDataDir = errors.New("invalid data directory provided") + errInvalidSecretsKey = errors.New("invalid number of secret key arguments") +) const ( defaultSecretsDir = "./secrets" @@ -27,7 +31,7 @@ func newSecretsCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "secrets", - ShortUsage: "gnoland secrets [flags] [...]", + ShortUsage: "secrets [flags] [...]", ShortHelp: "gno secrets manipulation suite", LongHelp: "gno secrets manipulation suite, for managing the validator key, p2p key and validator state", }, @@ -62,20 +66,28 @@ func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) { // verifySecretsKey verifies the secrets key value from the passed in arguments func verifySecretsKey(args []string) error { + // Check if any key is set if len(args) == 0 { return nil } + // Check if more than 1 key is set if len(args) > 1 { - return errors.New("invalid number of secret key arguments") + return errInvalidSecretsKey } + // Verify the set key key := args[0] if key != nodeKeyKey && key != validatorPrivateKeyKey && key != validatorStateKey { - return errors.New("invalid secrets key value") + return fmt.Errorf( + "invalid secrets key value [%s, %s, %s]", + validatorPrivateKeyKey, + validatorStateKey, + nodeKeyKey, + ) } return nil From ea258b35e13b956cf5fbd5ca87332a42c99ec9ed Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 28 Mar 2024 11:20:46 +0900 Subject: [PATCH 11/18] Add available keys in the secrets long help --- gno.land/cmd/gnoland/secrets_common.go | 10 ++++++++++ gno.land/cmd/gnoland/secrets_get.go | 5 ++++- gno.land/cmd/gnoland/secrets_init.go | 5 ++++- gno.land/cmd/gnoland/secrets_verify.go | 5 ++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go index 8c50fbe5950..1ed479fcdef 100644 --- a/gno.land/cmd/gnoland/secrets_common.go +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -178,3 +178,13 @@ func validateNodeKey(key *p2p.NodeKey) error { return nil } + +// getAvailableSecretsKeys formats and returns the available secret keys (constants) +func getAvailableSecretsKeys() string { + return fmt.Sprintf( + "[%s, %s, %s]", + validatorPrivateKeyKey, + nodeKeyKey, + validatorStateKey, + ) +} diff --git a/gno.land/cmd/gnoland/secrets_get.go b/gno.land/cmd/gnoland/secrets_get.go index 1645bca6ea3..165a3ec47c8 100644 --- a/gno.land/cmd/gnoland/secrets_get.go +++ b/gno.land/cmd/gnoland/secrets_get.go @@ -25,7 +25,10 @@ func newSecretsGetCmd(io commands.IO) *commands.Command { Name: "get", ShortUsage: "secrets get [flags] []", ShortHelp: "shows all Gno secrets present in a common directory", - LongHelp: "shows the validator private key, the node p2p key and the validator's last sign state", + LongHelp: fmt.Sprintf( + "shows the validator private key, the node p2p key and the validator's last sign state. Available keys: %s", + getAvailableSecretsKeys(), + ), }, cfg, func(_ context.Context, args []string) error { diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index 5e5dfa2c1b4..fc513189dc3 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -23,7 +23,10 @@ func newSecretsInitCmd(io commands.IO) *commands.Command { Name: "init", ShortUsage: "secrets init [flags] []", ShortHelp: "initializes required Gno secrets in a common directory", - LongHelp: "initializes the validator private key, the node p2p key and the validator's last sign state", + LongHelp: fmt.Sprintf( + "initializes the validator private key, the node p2p key and the validator's last sign state. Available keys: %s", + getAvailableSecretsKeys(), + ), }, cfg, func(_ context.Context, args []string) error { diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go index 0f0fb1d7744..e0e6dfa5ba4 100644 --- a/gno.land/cmd/gnoland/secrets_verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -24,7 +24,10 @@ func newSecretsVerifyCmd(io commands.IO) *commands.Command { Name: "verify", ShortUsage: "secrets verify [flags] []", ShortHelp: "verifies all Gno secrets in a common directory", - LongHelp: "verifies the validator private key, the node p2p key and the validator's last sign state", + LongHelp: fmt.Sprintf( + "verifies the validator private key, the node p2p key and the validator's last sign state. Available keys: %s", + getAvailableSecretsKeys(), + ), }, cfg, func(_ context.Context, args []string) error { From 572c3b2fd7f8fa5f21bcc02f60b3bff560fe88a9 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:17:19 +0200 Subject: [PATCH 12/18] Swap indent character in amino marshal --- gno.land/cmd/gnoland/secrets_common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go index 1ed479fcdef..76aa6fda9bb 100644 --- a/gno.land/cmd/gnoland/secrets_common.go +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -55,7 +55,7 @@ func generateNodeKey() *p2p.NodeKey { // saveSecretData saves the given data as Amino JSON to the path func saveSecretData(data any, path string) error { // Get Amino JSON - marshalledData, err := amino.MarshalJSONIndent(data, "", " ") + marshalledData, err := amino.MarshalJSONIndent(data, "", "\t") if err != nil { return fmt.Errorf("unable to marshal data into JSON, %w", err) } From 889693bc6e44225040764d8d578307aeda5d823b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:19:40 +0200 Subject: [PATCH 13/18] Add additional explanation on single-key manipulation --- gno.land/cmd/gnoland/secrets_get.go | 3 ++- gno.land/cmd/gnoland/secrets_init.go | 3 ++- gno.land/cmd/gnoland/secrets_verify.go | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gno.land/cmd/gnoland/secrets_get.go b/gno.land/cmd/gnoland/secrets_get.go index 165a3ec47c8..699450702b4 100644 --- a/gno.land/cmd/gnoland/secrets_get.go +++ b/gno.land/cmd/gnoland/secrets_get.go @@ -26,7 +26,8 @@ func newSecretsGetCmd(io commands.IO) *commands.Command { ShortUsage: "secrets get [flags] []", ShortHelp: "shows all Gno secrets present in a common directory", LongHelp: fmt.Sprintf( - "shows the validator private key, the node p2p key and the validator's last sign state. Available keys: %s", + "shows the validator private key, the node p2p key and the validator's last sign state. "+ + "If a key is provided, it shows the specified key value. Available keys: %s", getAvailableSecretsKeys(), ), }, diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index fc513189dc3..97c68e29396 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -24,7 +24,8 @@ func newSecretsInitCmd(io commands.IO) *commands.Command { ShortUsage: "secrets init [flags] []", ShortHelp: "initializes required Gno secrets in a common directory", LongHelp: fmt.Sprintf( - "initializes the validator private key, the node p2p key and the validator's last sign state. Available keys: %s", + "initializes the validator private key, the node p2p key and the validator's last sign state. "+ + "If a key is provided, it initializes the specified key. Available keys: %s", getAvailableSecretsKeys(), ), }, diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go index e0e6dfa5ba4..0291c2ab3ed 100644 --- a/gno.land/cmd/gnoland/secrets_verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -25,7 +25,8 @@ func newSecretsVerifyCmd(io commands.IO) *commands.Command { ShortUsage: "secrets verify [flags] []", ShortHelp: "verifies all Gno secrets in a common directory", LongHelp: fmt.Sprintf( - "verifies the validator private key, the node p2p key and the validator's last sign state. Available keys: %s", + "verifies the validator private key, the node p2p key and the validator's last sign state. "+ + "If a key is provided, it verifies the specified key value. Available keys: %s", getAvailableSecretsKeys(), ), }, From bda224e9f9a6ba10ac52b1333fd6bedcad7ca6fe Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:21:15 +0200 Subject: [PATCH 14/18] Move out generation logic to secrets_init --- gno.land/cmd/gnoland/secrets_common.go | 26 ------------------------ gno.land/cmd/gnoland/secrets_init.go | 28 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go index 76aa6fda9bb..b496d5194bc 100644 --- a/gno.land/cmd/gnoland/secrets_common.go +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -8,7 +8,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" "github.com/gnolang/gno/tm2/pkg/p2p" ) @@ -27,31 +26,6 @@ var ( errInvalidNodeKey = errors.New("invalid node p2p key") ) -// generateValidatorPrivateKey generates the validator's private key -func generateValidatorPrivateKey() *privval.FilePVKey { - privKey := ed25519.GenPrivKey() - - return &privval.FilePVKey{ - Address: privKey.PubKey().Address(), - PubKey: privKey.PubKey(), - PrivKey: privKey, - } -} - -// generateLastSignValidatorState generates the empty last sign state -func generateLastSignValidatorState() *privval.FilePVLastSignState { - return &privval.FilePVLastSignState{} // Empty last sign state -} - -// generateNodeKey generates the p2p node key -func generateNodeKey() *p2p.NodeKey { - privKey := ed25519.GenPrivKey() - - return &p2p.NodeKey{ - PrivKey: privKey, - } -} - // saveSecretData saves the given data as Amino JSON to the path func saveSecretData(data any, path string) error { // Get Amino JSON diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index 97c68e29396..c7c15685ae0 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -7,7 +7,10 @@ import ( "os" "path/filepath" + "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/p2p" ) type secretsInitCfg struct { @@ -140,3 +143,28 @@ func initAndSaveNodeKey(path string, io commands.IO) error { return nil } + +// generateValidatorPrivateKey generates the validator's private key +func generateValidatorPrivateKey() *privval.FilePVKey { + privKey := ed25519.GenPrivKey() + + return &privval.FilePVKey{ + Address: privKey.PubKey().Address(), + PubKey: privKey.PubKey(), + PrivKey: privKey, + } +} + +// generateLastSignValidatorState generates the empty last sign state +func generateLastSignValidatorState() *privval.FilePVLastSignState { + return &privval.FilePVLastSignState{} // Empty last sign state +} + +// generateNodeKey generates the p2p node key +func generateNodeKey() *p2p.NodeKey { + privKey := ed25519.GenPrivKey() + + return &p2p.NodeKey{ + PrivKey: privKey, + } +} From 888712c87179a2ed46c761590e610269fd812145 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:23:08 +0200 Subject: [PATCH 15/18] Add warning about skipping validator signed state verification --- gno.land/cmd/gnoland/secrets_verify.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go index 0291c2ab3ed..7e6c154d1ac 100644 --- a/gno.land/cmd/gnoland/secrets_verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -82,6 +82,8 @@ func execSecretsVerify(cfg *secretsVerifyCfg, args []string, io commands.IO) err if validatorKey, err := readAndVerifyValidatorKey(validatorKeyPath, io); validatorKey != nil && err == nil { // Validate the signature bytes return validateValidatorStateSignature(validatorState, validatorKey.PubKey) + } else { + io.Println("WARN: Skipped verification of validator state, as validator key is not present") } return nil From 6965f122633f0360259820b72238ecb01c396851 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:24:26 +0200 Subject: [PATCH 16/18] Rename key value --- gno.land/cmd/gnoland/secrets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoland/secrets.go b/gno.land/cmd/gnoland/secrets.go index 8d5cbec6d5d..86d6ca05039 100644 --- a/gno.land/cmd/gnoland/secrets.go +++ b/gno.land/cmd/gnoland/secrets.go @@ -23,7 +23,7 @@ const ( const ( nodeKeyKey = "NodeKey" validatorPrivateKeyKey = "ValidatorPrivateKey" - validatorStateKey = "ValidatorStateKey" + validatorStateKey = "ValidatorState" ) // newSecretsCmd creates the secrets root command From c058f868005b15d4203678f57607f851e581ae32 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:26:44 +0200 Subject: [PATCH 17/18] Move common methods to secrets_common --- gno.land/cmd/gnoland/secrets.go | 30 -------------------------- gno.land/cmd/gnoland/secrets_common.go | 29 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/gno.land/cmd/gnoland/secrets.go b/gno.land/cmd/gnoland/secrets.go index 86d6ca05039..36113a3e207 100644 --- a/gno.land/cmd/gnoland/secrets.go +++ b/gno.land/cmd/gnoland/secrets.go @@ -3,7 +3,6 @@ package main import ( "errors" "flag" - "fmt" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -63,32 +62,3 @@ func (c *commonAllCfg) RegisterFlags(fs *flag.FlagSet) { "the secrets output directory", ) } - -// verifySecretsKey verifies the secrets key value from the passed in arguments -func verifySecretsKey(args []string) error { - // Check if any key is set - if len(args) == 0 { - return nil - } - - // Check if more than 1 key is set - if len(args) > 1 { - return errInvalidSecretsKey - } - - // Verify the set key - key := args[0] - - if key != nodeKeyKey && - key != validatorPrivateKeyKey && - key != validatorStateKey { - return fmt.Errorf( - "invalid secrets key value [%s, %s, %s]", - validatorPrivateKeyKey, - validatorStateKey, - nodeKeyKey, - ) - } - - return nil -} diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go index b496d5194bc..588307b9b8e 100644 --- a/gno.land/cmd/gnoland/secrets_common.go +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -153,6 +153,35 @@ func validateNodeKey(key *p2p.NodeKey) error { return nil } +// verifySecretsKey verifies the secrets key value from the passed in arguments +func verifySecretsKey(args []string) error { + // Check if any key is set + if len(args) == 0 { + return nil + } + + // Check if more than 1 key is set + if len(args) > 1 { + return errInvalidSecretsKey + } + + // Verify the set key + key := args[0] + + if key != nodeKeyKey && + key != validatorPrivateKeyKey && + key != validatorStateKey { + return fmt.Errorf( + "invalid secrets key value [%s, %s, %s]", + validatorPrivateKeyKey, + validatorStateKey, + nodeKeyKey, + ) + } + + return nil +} + // getAvailableSecretsKeys formats and returns the available secret keys (constants) func getAvailableSecretsKeys() string { return fmt.Sprintf( From cf5abb69186eceaadf3781cc624c0c75d557ce4e Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 1 Apr 2024 11:45:26 +0200 Subject: [PATCH 18/18] Add overwrite protection --- gno.land/cmd/gnoland/secrets_init.go | 40 ++++++++++ gno.land/cmd/gnoland/secrets_init_test.go | 89 ++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index c7c15685ae0..55be73c22fc 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "os" @@ -10,11 +11,16 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/p2p" ) +var errOverwriteNotEnabled = errors.New("overwrite not enabled") + type secretsInitCfg struct { commonAllCfg + + forceOverwrite bool } // newSecretsInitCmd creates the secrets init command @@ -41,6 +47,13 @@ func newSecretsInitCmd(io commands.IO) *commands.Command { func (c *secretsInitCfg) RegisterFlags(fs *flag.FlagSet) { c.commonAllCfg.RegisterFlags(fs) + + fs.BoolVar( + &c.forceOverwrite, + "force", + false, + "overwrite existing secrets, if any", + ) } func execSecretsInit(cfg *secretsInitCfg, args []string, io commands.IO) error { @@ -74,15 +87,42 @@ func execSecretsInit(cfg *secretsInitCfg, args []string, io commands.IO) error { switch key { case validatorPrivateKeyKey: + if osm.FileExists(validatorKeyPath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + // Initialize and save the validator's private key return initAndSaveValidatorKey(validatorKeyPath, io) case nodeKeyKey: + if osm.FileExists(nodeKeyPath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + // Initialize and save the node's p2p key return initAndSaveNodeKey(nodeKeyPath, io) case validatorStateKey: + if osm.FileExists(validatorStatePath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + // Initialize and save the validator's last sign state return initAndSaveValidatorState(validatorStatePath, io) default: + // Check if the validator key should be overwritten + if osm.FileExists(validatorKeyPath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + + // Check if the validator state should be overwritten + if osm.FileExists(validatorStatePath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + + // Check if the node key should be overwritten + if osm.FileExists(nodeKeyPath) && !cfg.forceOverwrite { + return errOverwriteNotEnabled + } + // No key provided, initialize everything // Initialize and save the validator's private key if err := initAndSaveValidatorKey(validatorKeyPath, io); err != nil { diff --git a/gno.land/cmd/gnoland/secrets_init_test.go b/gno.land/cmd/gnoland/secrets_init_test.go index 83a31ba8a22..4a707778cc6 100644 --- a/gno.land/cmd/gnoland/secrets_init_test.go +++ b/gno.land/cmd/gnoland/secrets_init_test.go @@ -91,6 +91,39 @@ func TestSecrets_Init_All(t *testing.T) { // Verify the node p2p key is saved verifyNodeKey(t, filepath.Join(tempDir, defaultNodeKeyName)) }) + + t.Run("no secrets overwritten", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "init", + "--data-dir", + tempDir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Verify the validator key is saved + verifyValidatorKey(t, filepath.Join(tempDir, defaultValidatorKeyName)) + + // Verify the last sign validator state is saved + verifyValidatorState(t, filepath.Join(tempDir, defaultValidatorStateName)) + + // Verify the node p2p key is saved + verifyNodeKey(t, filepath.Join(tempDir, defaultNodeKeyName)) + + // Attempt to reinitialize the secrets, without the overwrite permission + cmdErr = cmd.ParseAndRun(context.Background(), args) + require.ErrorIs(t, cmdErr, errOverwriteNotEnabled) + }) } func TestSecrets_Init_Single(t *testing.T) { @@ -115,7 +148,7 @@ func TestSecrets_Init_Single(t *testing.T) { verifyValidatorState, }, { - "node p2p initialized", + "node p2p key initialized", nodeKeyKey, defaultNodeKeyName, verifyNodeKey, @@ -153,3 +186,57 @@ func TestSecrets_Init_Single(t *testing.T) { }) } } + +func TestSecrets_Init_Single_Overwrite(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + keyValue string + expectedFile string + }{ + { + "validator key not overwritten", + validatorPrivateKeyKey, + defaultValidatorKeyName, + }, + { + "validator state not overwritten", + validatorStateKey, + defaultValidatorStateName, + }, + { + "node p2p key not overwritten", + nodeKeyKey, + defaultNodeKeyName, + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "init", + "--data-dir", + tempDir, + testCase.keyValue, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Attempt to reinitialize the secret, without the overwrite permission + cmdErr = cmd.ParseAndRun(context.Background(), args) + require.ErrorIs(t, cmdErr, errOverwriteNotEnabled) + }) + } +}