From c128bf64da4dc194a7edc9b8f1065a74e29ab915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 19 Oct 2023 16:02:14 -0400 Subject: [PATCH] feat: add `genesis verify` (#1254) ## Description This PR adds the `genesis verify` subcommand. Closes #1229
Contributors' checklist... - [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).
--- gno.land/cmd/genesis/generate.go | 2 +- gno.land/cmd/genesis/main.go | 1 + gno.land/cmd/genesis/verify.go | 76 +++++++++++++++++ gno.land/cmd/genesis/verify_test.go | 124 ++++++++++++++++++++++++++++ tm2/pkg/bft/types/genesis.go | 64 +++++++++++++- tm2/pkg/bft/types/genesis_test.go | 118 ++++++++++++++++++++++++++ 6 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 gno.land/cmd/genesis/verify.go create mode 100644 gno.land/cmd/genesis/verify_test.go diff --git a/gno.land/cmd/genesis/generate.go b/gno.land/cmd/genesis/generate.go index 0da7b4a2ba0..7831d64e877 100644 --- a/gno.land/cmd/genesis/generate.go +++ b/gno.land/cmd/genesis/generate.go @@ -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) }, ) diff --git a/gno.land/cmd/genesis/main.go b/gno.land/cmd/genesis/main.go index 9d75f810465..8db7366f916 100644 --- a/gno.land/cmd/genesis/main.go +++ b/gno.land/cmd/genesis/main.go @@ -31,6 +31,7 @@ func newRootCmd(io *commands.IO) *commands.Command { cmd.AddSubCommands( newGenerateCmd(io), + newVerifyCmd(io), ) return cmd diff --git a/gno.land/cmd/genesis/verify.go b/gno.land/cmd/genesis/verify.go new file mode 100644 index 00000000000..cee8e4df6f8 --- /dev/null +++ b/gno.land/cmd/genesis/verify.go @@ -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 +} diff --git a/gno.land/cmd/genesis/verify_test.go b/gno.land/cmd/genesis/verify_test.go new file mode 100644 index 00000000000..fd4146119b1 --- /dev/null +++ b/gno.land/cmd/genesis/verify_test.go @@ -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) + }) +} diff --git a/tm2/pkg/bft/types/genesis.go b/tm2/pkg/bft/types/genesis.go index f881e068558..c03f7acc09e 100644 --- a/tm2/pkg/bft/types/genesis.go +++ b/tm2/pkg/bft/types/genesis.go @@ -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: @@ -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 { @@ -95,7 +153,7 @@ func (genDoc *GenesisDoc) ValidateAndComplete() error { return nil } -//------------------------------------------------------------ +// ------------------------------------------------------------ // Make genesis state from file // GenesisDocFromJSON unmarshalls JSON data into a GenesisDoc. @@ -126,7 +184,7 @@ func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) { return genDoc, nil } -//---------------------------------------- +// ---------------------------------------- // Mock AppState (for testing) type MockAppState struct { diff --git a/tm2/pkg/bft/types/genesis_test.go b/tm2/pkg/bft/types/genesis_test.go index c8886f9bf0a..a8816bed2e7 100644 --- a/tm2/pkg/bft/types/genesis_test.go +++ b/tm2/pkg/bft/types/genesis_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -127,3 +128,120 @@ func randomGenesisDoc() *GenesisDoc { ConsensusParams: DefaultConsensusParams(), } } + +func TestGenesis_Validate(t *testing.T) { + t.Parallel() + + getValidTestGenesis := func() *GenesisDoc { + key := randPubKey() + + return &GenesisDoc{ + GenesisTime: time.Now(), + ChainID: "valid-chain-id", + ConsensusParams: DefaultConsensusParams(), + Validators: []GenesisValidator{ + { + Address: key.Address(), + PubKey: key, + Power: 1, + Name: "valid validator", + }, + }, + } + } + + t.Run("valid genesis", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + + require.NoError(t, g.Validate()) + }) + + t.Run("invalid chain ID (zero)", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.ChainID = "" + + assert.ErrorIs(t, g.Validate(), ErrEmptyChainID) + }) + + t.Run("invalid chain ID (long)", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.ChainID = "thischainidisunusuallylongsoitwillcausethetesttofail" + + assert.ErrorIs(t, g.Validate(), ErrLongChainID) + }) + + t.Run("invalid genesis time", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.GenesisTime = time.Time{} + + assert.ErrorIs(t, g.Validate(), ErrInvalidGenesisTime) + }) + + t.Run("invalid consensus params", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.ConsensusParams.Block.MaxTxBytes = -1 // invalid value + + assert.ErrorContains(t, g.Validate(), "MaxTxBytes") + }) + + t.Run("no validators", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{} + + assert.ErrorIs(t, g.Validate(), ErrNoValidators) + }) + + t.Run("invalid validator, no voting power", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{ + { + Power: 0, // no voting power + }, + } + + assert.ErrorIs(t, g.Validate(), ErrInvalidValidatorVotingPower) + }) + + t.Run("invalid validator, zero address", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{ + { + Power: 1, + Address: Address{}, // zero address + }, + } + + assert.ErrorIs(t, g.Validate(), ErrInvalidValidatorAddress) + }) + + t.Run("invalid validator, public key mismatch", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{ + { + Power: 1, + Address: Address{1}, + PubKey: randPubKey(), + }, + } + + assert.ErrorIs(t, g.Validate(), ErrValidatorPubKeyMismatch) + }) +}