diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index 9565c4fe08f..acef192e0c6 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -33,6 +33,7 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newStartCmd(io), + newSecretsCmd(io), newConfigCmd(io), ) diff --git a/gno.land/cmd/gnoland/secrets.go b/gno.land/cmd/gnoland/secrets.go new file mode 100644 index 00000000000..36113a3e207 --- /dev/null +++ b/gno.land/cmd/gnoland/secrets.go @@ -0,0 +1,64 @@ +package main + +import ( + "errors" + "flag" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var ( + errInvalidDataDir = errors.New("invalid data directory provided") + errInvalidSecretsKey = errors.New("invalid number of secret key arguments") +) + +const ( + defaultSecretsDir = "./secrets" + defaultValidatorKeyName = "priv_validator_key.json" + defaultNodeKeyName = "node_key.json" + defaultValidatorStateName = "priv_validator_state.json" +) + +const ( + nodeKeyKey = "NodeKey" + validatorPrivateKeyKey = "ValidatorPrivateKey" + validatorStateKey = "ValidatorState" +) + +// 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", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newSecretsInitCmd(io), + newSecretsVerifyCmd(io), + newSecretsGetCmd(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", + ) +} diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go new file mode 100644 index 00000000000..588307b9b8e --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -0,0 +1,193 @@ +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/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") +) + +// 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, "", "\t") + if err != nil { + return fmt.Errorf("unable to marshal data into JSON, %w", err) + } + + // Save the data to disk + if err := os.WriteFile(path, marshalledData, 0o644); err != nil { + 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 +} + +// 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( + "[%s, %s, %s]", + validatorPrivateKeyKey, + nodeKeyKey, + validatorStateKey, + ) +} diff --git a/gno.land/cmd/gnoland/secrets_common_test.go b/gno.land/cmd/gnoland/secrets_common_test.go new file mode 100644 index 00000000000..34592c3bd8f --- /dev/null +++ b/gno.land/cmd/gnoland/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/gnoland/secrets_get.go b/gno.land/cmd/gnoland/secrets_get.go new file mode 100644 index 00000000000..699450702b4 --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_get.go @@ -0,0 +1,203 @@ +package main + +import ( + "context" + "flag" + "fmt" + "path/filepath" + "text/tabwriter" + + "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/p2p" +) + +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: "get", + 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. "+ + "If a key is provided, it shows the specified key value. Available keys: %s", + getAvailableSecretsKeys(), + ), + }, + cfg, + func(_ context.Context, args []string) error { + return execSecretsGet(cfg, args, 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) + ) + + 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 +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.Fprintf(w, "[Validator Key Info]\n\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.Fprintf(w, "[Last Validator Sign State Info]\n\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.Fprintf(w, "[Node P2P Info]\n\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/gnoland/secrets_get_test.go b/gno.land/cmd/gnoland/secrets_get_test.go new file mode 100644 index 00000000000..20f1eb2ef35 --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_get_test.go @@ -0,0 +1,276 @@ +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_Get_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{ + "secrets", + "get", + "--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{ + "secrets", + "init", + "--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{ + "secrets", + "get", + "--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), + ) + }) +} + +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 new file mode 100644 index 00000000000..55be73c22fc --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -0,0 +1,210 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "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" + 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 +func newSecretsInitCmd(io commands.IO) *commands.Command { + cfg := &secretsInitCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "init", + 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. "+ + "If a key is provided, it initializes the specified key. Available keys: %s", + getAvailableSecretsKeys(), + ), + }, + cfg, + func(_ context.Context, args []string) error { + return execSecretsInit(cfg, args, io) + }, + ) +} + +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 { + // Check the data output directory path + if 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] + } + + // 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) + ) + + 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 { + 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 +func initAndSaveValidatorKey(path string, io commands.IO) error { + // Initialize the validator's private key + privateKey := generateValidatorPrivateKey() + + // Save the key + if err := saveSecretData(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 := saveSecretData(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 := saveSecretData(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 +} + +// 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, + } +} diff --git a/gno.land/cmd/gnoland/secrets_init_test.go b/gno.land/cmd/gnoland/secrets_init_test.go new file mode 100644 index 00000000000..4a707778cc6 --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_init_test.go @@ -0,0 +1,242 @@ +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 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() + + t.Run("invalid data directory", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "secrets", + "init", + "--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{ + "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)) + }) + + 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) { + t.Parallel() + + 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 key 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) + }) + } +} + +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) + }) + } +} diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go new file mode 100644 index 00000000000..7e6c154d1ac --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -0,0 +1,161 @@ +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" +) + +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 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. "+ + "If a key is provided, it verifies the specified key value. Available keys: %s", + getAvailableSecretsKeys(), + ), + }, + cfg, + func(_ context.Context, args []string) error { + return execSecretsVerify(cfg, args, io) + }, + ) +} + +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 + } + + // 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) + ) + + 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) + } else { + io.Println("WARN: Skipped verification of validator state, as validator key is not present") + } + + 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 +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 +} + +// 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 { + 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 +} + +// 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 { + 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/gnoland/secrets_verify_test.go b/gno.land/cmd/gnoland/secrets_verify_test.go new file mode 100644 index 00000000000..77ab8523d40 --- /dev/null +++ b/gno.land/cmd/gnoland/secrets_verify_test.go @@ -0,0 +1,370 @@ +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/gnolang/gno/tm2/pkg/p2p" + "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{ + "secrets", + "verify", + "--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{ + "secrets", + "verify", + "--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{ + "secrets", + "init", + "--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{ + "secrets", + "verify", + "--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{ + "secrets", + "init", + "--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{ + "secrets", + "verify", + "--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{ + "secrets", + "init", + "--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{ + "secrets", + "verify", + "--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{ + "secrets", + "init", + "--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{ + "secrets", + "verify", + "--data-dir", + tempDir, + } + + assert.ErrorContains( + t, + cmd.ParseAndRun(context.Background(), verifyArgs), + "unable to read validator key", + ) + }) +} + +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) + }) +} 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.