Skip to content

Commit

Permalink
feat: add genesis verify (#1254)
Browse files Browse the repository at this point in the history
## Description

This PR adds the `genesis verify` subcommand.

Closes #1229

<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>
  • Loading branch information
zivkovicmilos committed Oct 19, 2023
1 parent 5033232 commit c128bf6
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 4 deletions.
2 changes: 1 addition & 1 deletion gno.land/cmd/genesis/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func newGenerateCmd(io *commands.IO) *commands.Command {
LongHelp: "Generates a node's genesis.json",
},
cfg,
func(_ context.Context, args []string) error {
func(_ context.Context, _ []string) error {
return execGenerate(cfg, io)
},
)
Expand Down
1 change: 1 addition & 0 deletions gno.land/cmd/genesis/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func newRootCmd(io *commands.IO) *commands.Command {

cmd.AddSubCommands(
newGenerateCmd(io),
newVerifyCmd(io),
)

return cmd
Expand Down
76 changes: 76 additions & 0 deletions gno.land/cmd/genesis/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"context"
"flag"
"fmt"

"github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/std"
)

type verifyCfg struct {
genesisPath string
}

// newVerifyCmd creates the genesis verify subcommand
func newVerifyCmd(io *commands.IO) *commands.Command {
cfg := &verifyCfg{}

return commands.NewCommand(
commands.Metadata{
Name: "verify",
ShortUsage: "verify [flags]",
LongHelp: "Verifies a node's genesis.json",
},
cfg,
func(_ context.Context, _ []string) error {
return execVerify(cfg, io)
},
)
}

func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.genesisPath,
"genesis-path",
"./genesis.json",
"the path to the genesis.json",
)
}

func execVerify(cfg *verifyCfg, io *commands.IO) error {
// Load the genesis
genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath)
if loadErr != nil {
return fmt.Errorf("unable to load genesis, %w", loadErr)
}

// Verify it
if validateErr := genesis.Validate(); validateErr != nil {
return fmt.Errorf("unable to verify genesis, %w", validateErr)
}

// Validate the genesis state
state := genesis.AppState.(gnoland.GnoGenesisState)

// Validate the initial transactions
for _, tx := range state.Txs {
if validateErr := tx.ValidateBasic(); validateErr != nil {
return fmt.Errorf("invalid transacton, %w", validateErr)
}
}

// Validate the initial balances
for _, balance := range state.Balances {
if _, parseErr := std.ParseCoins(balance); parseErr != nil {
return fmt.Errorf("invalid balance %s, %w", balance, parseErr)
}
}

io.Printfln("Genesis at %s is valid", cfg.genesisPath)

return nil
}
124 changes: 124 additions & 0 deletions gno.land/cmd/genesis/verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"context"
"testing"
"time"

"github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/crypto/mock"
"github.com/gnolang/gno/tm2/pkg/std"
"github.com/gnolang/gno/tm2/pkg/testutils"
"github.com/stretchr/testify/require"
)

func TestGenesis_Verify(t *testing.T) {
t.Parallel()

getValidTestGenesis := func() *types.GenesisDoc {
key := mock.GenPrivKey().PubKey()

return &types.GenesisDoc{
GenesisTime: time.Now(),
ChainID: "valid-chain-id",
ConsensusParams: types.DefaultConsensusParams(),
Validators: []types.GenesisValidator{
{
Address: key.Address(),
PubKey: key,
Power: 1,
Name: "valid validator",
},
},
}
}

t.Run("invalid txs", func(t *testing.T) {
t.Parallel()

tempFile, cleanup := testutils.NewTestFile(t)
t.Cleanup(cleanup)

g := getValidTestGenesis()

g.AppState = gnoland.GnoGenesisState{
Balances: []string{},
Txs: []std.Tx{
{},
},
}

require.NoError(t, g.SaveAs(tempFile.Name()))

// Create the command
cmd := newRootCmd(commands.NewTestIO())
args := []string{
"verify",
"--genesis-path",
tempFile.Name(),
}

// Run the command
cmdErr := cmd.ParseAndRun(context.Background(), args)
require.Error(t, cmdErr)
})

t.Run("invalid balances", func(t *testing.T) {
t.Parallel()

tempFile, cleanup := testutils.NewTestFile(t)
t.Cleanup(cleanup)

g := getValidTestGenesis()

g.AppState = gnoland.GnoGenesisState{
Balances: []string{
"dummybalance",
},
Txs: []std.Tx{},
}

require.NoError(t, g.SaveAs(tempFile.Name()))

// Create the command
cmd := newRootCmd(commands.NewTestIO())
args := []string{
"verify",
"--genesis-path",
tempFile.Name(),
}

// Run the command
cmdErr := cmd.ParseAndRun(context.Background(), args)
require.Error(t, cmdErr)
})

t.Run("valid genesis", func(t *testing.T) {
t.Parallel()

tempFile, cleanup := testutils.NewTestFile(t)
t.Cleanup(cleanup)

g := getValidTestGenesis()
g.AppState = gnoland.GnoGenesisState{
Balances: []string{},
Txs: []std.Tx{},
}

require.NoError(t, g.SaveAs(tempFile.Name()))

// Create the command
cmd := newRootCmd(commands.NewTestIO())
args := []string{
"verify",
"--genesis-path",
tempFile.Name(),
}

// Run the command
cmdErr := cmd.ParseAndRun(context.Background(), args)
require.NoError(t, cmdErr)
})
}
64 changes: 61 additions & 3 deletions tm2/pkg/bft/types/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ const (
MaxChainIDLen = 50
)

//------------------------------------------------------------
var (
ErrEmptyChainID = errors.New("chain ID is empty")
ErrLongChainID = fmt.Errorf("chain ID cannot be longer than %d chars", MaxChainIDLen)
ErrInvalidGenesisTime = errors.New("invalid genesis time")
ErrNoValidators = errors.New("no validators in set")
ErrInvalidValidatorVotingPower = errors.New("validator has no voting power")
ErrInvalidValidatorAddress = errors.New("invalid validator address")
ErrValidatorPubKeyMismatch = errors.New("validator public key and address mismatch")
)

// ------------------------------------------------------------
// core types for a genesis definition
// NOTE: any changes to the genesis definition should
// be reflected in the documentation:
Expand Down Expand Up @@ -61,6 +71,54 @@ func (genDoc *GenesisDoc) ValidatorHash() []byte {
return vset.Hash()
}

// Validate validates the genesis doc
func (genDoc *GenesisDoc) Validate() error {
// Make sure the chain ID is not empty
if genDoc.ChainID == "" {
return ErrEmptyChainID
}

// Make sure the chain ID is < max chain ID length
if len(genDoc.ChainID) > MaxChainIDLen {
return ErrLongChainID
}

// Make sure the genesis time is valid
if genDoc.GenesisTime.IsZero() {
return ErrInvalidGenesisTime
}

// Validate the consensus params
if consensusParamsErr := ValidateConsensusParams(genDoc.ConsensusParams); consensusParamsErr != nil {
return consensusParamsErr
}

// Make sure there are validators in the set
if len(genDoc.Validators) == 0 {
return ErrNoValidators
}

// Make sure the validators are valid
for _, v := range genDoc.Validators {
// Check the voting power
if v.Power == 0 {
return fmt.Errorf("%w, %s", ErrInvalidValidatorVotingPower, v.Name)
}

// Check the address
if v.Address.IsZero() {
return fmt.Errorf("%w, %s", ErrInvalidValidatorAddress, v.Name)
}

// Check the pub key -> address matching
if v.PubKey.Address() != v.Address {
return fmt.Errorf("%w, %s", ErrValidatorPubKeyMismatch, v.Name)
}
}

return nil
}

// ValidateAndComplete checks that all necessary fields are present
// and fills in defaults for optional fields left empty
func (genDoc *GenesisDoc) ValidateAndComplete() error {
Expand Down Expand Up @@ -95,7 +153,7 @@ func (genDoc *GenesisDoc) ValidateAndComplete() error {
return nil
}

//------------------------------------------------------------
// ------------------------------------------------------------
// Make genesis state from file

// GenesisDocFromJSON unmarshalls JSON data into a GenesisDoc.
Expand Down Expand Up @@ -126,7 +184,7 @@ func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) {
return genDoc, nil
}

//----------------------------------------
// ----------------------------------------
// Mock AppState (for testing)

type MockAppState struct {
Expand Down
Loading

0 comments on commit c128bf6

Please sign in to comment.