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 validate-genesis cmd #187

Merged
merged 3 commits into from
Nov 4, 2022
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
4 changes: 2 additions & 2 deletions cmd/babylond/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) {
genutilcli.CollectGenTxsCmd(banktypes.GenesisBalancesIterator{}, app.DefaultNodeHome),
genutilcli.MigrateGenesisCmd(),
genutilcli.GenTxCmd(app.ModuleBasics, encodingConfig.TxConfig, banktypes.GenesisBalancesIterator{}, app.DefaultNodeHome),
genutilcli.ValidateGenesisCmd(app.ModuleBasics),
ValidateGenesisCmd(app.ModuleBasics),
PrepareGenesisCmd(app.DefaultNodeHome, app.ModuleBasics),
AddGenesisAccountCmd(app.DefaultNodeHome),
tmcli.NewCompletionCmd(rootCmd, true),
testnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{}),
TestnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{}),
CreateBlsKeyCmd(),
GenBlsCmd(),
AddGenBlsCmd(),
Expand Down
4 changes: 2 additions & 2 deletions cmd/babylond/cmd/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ var (
flagAdditionalSenderAccount = "additional-sender-account"
)

// get cmd to initialize all files for tendermint testnet and application
func testnetCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator) *cobra.Command {
// TestnetCmd initializes all files for tendermint testnet and application
func TestnetCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator) *cobra.Command {
cmd := &cobra.Command{
Use: "testnet",
Short: "Initialize files for a babylon testnet",
Expand Down
2 changes: 1 addition & 1 deletion cmd/babylond/cmd/testnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func Test_TestnetCmd(t *testing.T) {
ctx := context.Background()
ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx)
ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx)
cmd := testnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{})
cmd := TestnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{})
cmd.SetArgs([]string{fmt.Sprintf("--%s=test", flags.FlagKeyringBackend), fmt.Sprintf("--output-dir=%s", home)})
err = cmd.ExecuteContext(ctx)
require.NoError(t, err)
Expand Down
123 changes: 123 additions & 0 deletions cmd/babylond/cmd/validate_genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"

"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/server"
"github.com/cosmos/cosmos-sdk/types/module"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
tmtypes "github.com/tendermint/tendermint/types"

"github.com/babylonchain/babylon/x/checkpointing/types"
)

const chainUpgradeGuide = "https://github.com/cosmos/cosmos-sdk/blob/a51aa517c46c70df04a06f586c67fb765e45322a/UPGRADING.md"

// ValidateGenesisCmd takes a genesis file, and makes sure that it is valid.
// 1. genesis state of each module should be valid according to each module's
// validation rule
// 2. each genesis BLS key or gentx should have a corresponding gentx or genesis
// BLS key
// modified based on "https://github.com/cosmos/cosmos-sdk/blob/6d32debf1aca4b7f1ed1429d87be1d02c315f02d/x/genutil/client/cli/validate_genesis.go"
func ValidateGenesisCmd(mbm module.BasicManager) *cobra.Command {
return &cobra.Command{
Use: "validate-genesis [file]",
Args: cobra.RangeArgs(0, 1),
Short: "validates the genesis file at the default location or at the location passed as an arg",
RunE: func(cmd *cobra.Command, args []string) (err error) {
serverCtx := server.GetServerContextFromCmd(cmd)
clientCtx := client.GetClientContextFromCmd(cmd)

cdc := clientCtx.Codec

// Load default if passed no args, otherwise load passed file
var genesis string
if len(args) == 0 {
genesis = serverCtx.Config.GenesisFile()
} else {
genesis = args[0]
}

genDoc, err := validateGenDoc(genesis)
if err != nil {
return err
}

var genState map[string]json.RawMessage
if err = json.Unmarshal(genDoc.AppState, &genState); err != nil {
return fmt.Errorf("error unmarshalling genesis doc %s: %s", genesis, err.Error())
}

if err = mbm.ValidateGenesis(cdc, clientCtx.TxConfig, genState); err != nil {
return fmt.Errorf("error validating genesis file %s: %s", genesis, err.Error())
}

if err = CheckCorrespondence(clientCtx, genState); err != nil {
return fmt.Errorf("error validating genesis file correspondence %s: %s", genesis, err.Error())
}

fmt.Printf("File at %s is a valid genesis file\n", genesis)
return nil
},
}
}

// validateGenDoc reads a genesis file and validates that it is a correct
// Tendermint GenesisDoc. This function does not do any cosmos-related
// validation.
func validateGenDoc(importGenesisFile string) (*tmtypes.GenesisDoc, error) {
genDoc, err := tmtypes.GenesisDocFromFile(importGenesisFile)
if err != nil {
return nil, fmt.Errorf("%s. Make sure that"+
" you have correctly migrated all Tendermint consensus params, please see the"+
" chain migration guide at %s for more info",
err.Error(), chainUpgradeGuide,
)
}

return genDoc, nil
}

// CheckCorrespondence checks that each genesis tx/BLS key should have one
// corresponding BLS key/genesis tx
func CheckCorrespondence(ctx client.Context, genesis map[string]json.RawMessage) error {
checkpointingGenState := types.GetGenesisStateFromAppState(ctx.Codec, genesis)
gks := checkpointingGenState.GetGenesisKeys()
genTxState := genutiltypes.GetGenesisStateFromAppState(ctx.Codec, genesis)
addresses := make(map[string]struct{}, 0)
// ensure no duplicate BLS keys
for _, gk := range gks {
addresses[gk.ValidatorAddress] = struct{}{}
}
if len(addresses) != len(gks) {
return errors.New("duplicate genesis BLS keys")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you check that on the Validate function of the checkpointing module, you don't need to do it again here as the ValidateFunction already gets called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is true, but I just thought CheckCorrespondence is rather independent. It does not hurt if we do the length checking here again. What if some day we accidentally move CheckCorrespondence before the module validation?

// ensure the number of BLS keys and gentxs are the same so that we
// don't need to do reverse checking
if len(addresses) != len(genTxState.GenTxs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check means that you don't have to do a reverse loop later to check whether all BLS sigs have a matching gentx, right? Maybe worth adding a comment about your checking algorithm.

return errors.New("genesis txs and genesis BLS keys do not match")
}
// ensure every gentx has a match with BLS key by address
for _, genTx := range genTxState.GenTxs {
tx, err := genutiltypes.ValidateAndGetGenTx(genTx, ctx.TxConfig.TxJSONDecoder())
if err != nil {
return err
}
msgs := tx.GetMsgs()
if len(msgs) == 0 {
return errors.New("invalid genesis transaction")
}
msgCreateValidator := msgs[0].(*stakingtypes.MsgCreateValidator)
if _, exists := addresses[msgCreateValidator.ValidatorAddress]; !exists {
return errors.New("cannot find a corresponding BLS key for a genesis tx")
}
}

return nil
}
114 changes: 114 additions & 0 deletions cmd/babylond/cmd/validate_genesis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cmd_test

import (
"context"
"encoding/json"
"fmt"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/require"

"github.com/babylonchain/babylon/app"
"github.com/babylonchain/babylon/cmd/babylond/cmd"
"github.com/babylonchain/babylon/x/checkpointing/types"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/server"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
tmjson "github.com/tendermint/tendermint/libs/json"
"github.com/tendermint/tendermint/libs/log"
tmtypes "github.com/tendermint/tendermint/types"
)

func TestCheckCorrespondence(t *testing.T) {
homePath := t.TempDir()
encodingCft := app.MakeTestEncodingConfig()
clientCtx := client.Context{}.WithCodec(encodingCft.Marshaler).WithTxConfig(encodingCft.TxConfig)

// generate valid genesis doc
validGenState, genDoc := generateTestGenesisState(homePath, 2)
validGenDocJSON, err := tmjson.MarshalIndent(genDoc, "", " ")
require.NoError(t, err)

// generate mismatched genesis doc by deleting one item from gentx and genKeys in different positions
gentxs := genutiltypes.GetGenesisStateFromAppState(clientCtx.Codec, validGenState)
genKeys := types.GetGenesisStateFromAppState(clientCtx.Codec, validGenState)
gentxs.GenTxs = gentxs.GenTxs[:1]
genKeys.GenesisKeys = genKeys.GenesisKeys[1:]
genTxsBz, err := clientCtx.Codec.MarshalJSON(gentxs)
require.NoError(t, err)
genKeysBz, err := clientCtx.Codec.MarshalJSON(&genKeys)
require.NoError(t, err)
validGenState[genutiltypes.ModuleName] = genTxsBz
validGenState[types.ModuleName] = genKeysBz
misMatchedGenStateBz, err := json.Marshal(validGenState)
require.NoError(t, err)
genDoc.AppState = misMatchedGenStateBz
misMatchedGenDocJSON, err := tmjson.MarshalIndent(genDoc, "", " ")
require.NoError(t, err)

testCases := []struct {
name string
genesis []byte
expErr bool
}{
{
"valid genesis gentx and BLS key pair",
validGenDocJSON,
false,
},
{
"mismatched genesis state",
misMatchedGenDocJSON,
true,
},
}

for _, tc := range testCases {
genDoc, err := tmtypes.GenesisDocFromJSON(tc.genesis)
require.NoError(t, err)
require.NotEmpty(t, genDoc)
genesisState, err := genutiltypes.GenesisStateFromGenDoc(*genDoc)
require.NoError(t, err)
require.NotEmpty(t, genesisState)
err = cmd.CheckCorrespondence(clientCtx, genesisState)
if tc.expErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
}
}

func generateTestGenesisState(home string, n int) (map[string]json.RawMessage, *tmtypes.GenesisDoc) {
encodingConfig := app.MakeTestEncodingConfig()
logger := log.NewNopLogger()
cfg, _ := genutiltest.CreateDefaultTendermintConfig(home)

_ = genutiltest.ExecInitCmd(app.ModuleBasics, home, encodingConfig.Marshaler)

serverCtx := server.NewContext(viper.New(), cfg, logger)
clientCtx := client.Context{}.
WithCodec(encodingConfig.Marshaler).
WithHomeDir(home).
WithTxConfig(encodingConfig.TxConfig)

ctx := context.Background()
ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx)
ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx)
testnetCmd := cmd.TestnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{})
testnetCmd.SetArgs([]string{
fmt.Sprintf("--%s=test", flags.FlagKeyringBackend),
fmt.Sprintf("--v=%v", n),
fmt.Sprintf("--output-dir=%s", home),
})
_ = testnetCmd.ExecuteContext(ctx)

genFile := cfg.GenesisFile()
appState, gendoc, _ := genutiltypes.GenesisStateFromGenFile(genFile)
return appState, gendoc
}
6 changes: 6 additions & 0 deletions x/checkpointing/types/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package types

import (
"encoding/json"
"errors"
"io/ioutil"

"github.com/cosmos/cosmos-sdk/codec"
Expand All @@ -26,7 +27,12 @@ func DefaultGenesis() *GenesisState {
// Validate performs basic genesis state validation returning an error upon any
// failure.
func (gs GenesisState) Validate() error {
addresses := make(map[string]struct{}, 0)
for _, gk := range gs.GenesisKeys {
if _, exists := addresses[gk.ValidatorAddress]; exists {
return errors.New("duplicate genesis key")
}
addresses[gk.ValidatorAddress] = struct{}{}
err := gk.Validate()
if err != nil {
return err
Expand Down