Skip to content

Commit

Permalink
feat: add gnoland secrets command suite (#1593)
Browse files Browse the repository at this point in the history
## Description

This PR introduces the secret management command suite as a the
`gnoland` subcommand -- `gnoland secrets`.

It is part of a series of PRs I plan to do on improving the chain
initialization flow, with subsequent PRs focusing on the `config` file
and its manipulation.

Secrets being managed:
- Validator private key (consensus)
- Node p2p key (networking)
- Validator last signed state (consensus optimization)

Available commands:
- `secrets init` - Initializes the Gno node secrets locally, including
the validator key, validator state and node key
- `secrets verify` - Verifies the Gno node secrets locally, including
the validator key, validator state and node key
- `secrets get` - Shows the Gno node secrets locally, including the
validator key, validator state and node key

```mermaid
---
title: secrets command suite
---
flowchart LR
    subgraph init
        A[init] --> B[--data-dir]
        
        B -.-> C1[ValidatorPrivateKey]
        B -.-> C2[ValidatorState]
        B -.-> C3[NodeKey]
    end
    subgraph verify
        D[verify] --> E[--data-dir]

        E -.-> E1[ValidatorPrivateKey]
        E -.-> E2[ValidatorState]
        E -.-> E3[NodeKey]
    end
    subgraph get
        G[get] --> H[--data-dir]

        H -.-> H1[ValidatorPrivateKey]
        H -.-> H2[ValidatorState]
        H -.-> H3[NodeKey]
    end
```

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Guilhem Fanton <[email protected]>
  • Loading branch information
zivkovicmilos and gfanton authored Apr 2, 2024
1 parent c474f1c commit 831bb6f
Show file tree
Hide file tree
Showing 11 changed files with 1,984 additions and 5 deletions.
1 change: 1 addition & 0 deletions gno.land/cmd/gnoland/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func newRootCmd(io commands.IO) *commands.Command {

cmd.AddSubCommands(
newStartCmd(io),
newSecretsCmd(io),
newConfigCmd(io),
)

Expand Down
64 changes: 64 additions & 0 deletions gno.land/cmd/gnoland/secrets.go
Original file line number Diff line number Diff line change
@@ -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 <subcommand> [flags] [<arg>...]",
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",
)
}
193 changes: 193 additions & 0 deletions gno.land/cmd/gnoland/secrets_common.go
Original file line number Diff line number Diff line change
@@ -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,
)
}
Loading

0 comments on commit 831bb6f

Please sign in to comment.