Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add genesis verify #1254

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading