diff --git a/cmd/babylond/cmd/root.go b/cmd/babylond/cmd/root.go index cd79e2801..90de25524 100644 --- a/cmd/babylond/cmd/root.go +++ b/cmd/babylond/cmd/root.go @@ -141,7 +141,7 @@ 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), diff --git a/cmd/babylond/cmd/validate_genesis.go b/cmd/babylond/cmd/validate_genesis.go new file mode 100644 index 000000000..71832d4ff --- /dev/null +++ b/cmd/babylond/cmd/validate_genesis.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/babylonchain/babylon/x/checkpointing/types" + "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" + "github.com/spf13/cobra" + tmtypes "github.com/tendermint/tendermint/types" +) + +const chainUpgradeGuide = "https://github.com/cosmos/cosmos-sdk/blob/main/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 "github.com/cosmos/cosmos-sdk/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) + for _, gk := range gks { + addresses[gk.ValidatorAddress] = struct{}{} + } + if len(addresses) != len(gks) { + return errors.New("duplicate genesis BLS keys") + } + if len(addresses) != len(genTxState.GenTxs) { + return errors.New("genesis txs and genesis BLS keys do not match") + } + + 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("genesis txs and genesis BLS keys do not match") + } + } + + return nil +} diff --git a/cmd/babylond/cmd/validate_genesis_test.go b/cmd/babylond/cmd/validate_genesis_test.go new file mode 100644 index 000000000..4beaa8817 --- /dev/null +++ b/cmd/babylond/cmd/validate_genesis_test.go @@ -0,0 +1,308 @@ +package cmd_test + +import ( + "github.com/babylonchain/babylon/app" + "github.com/babylonchain/babylon/cmd/babylond/cmd" + "github.com/cosmos/cosmos-sdk/client" + types2 "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/types" + "testing" +) + +var misMatchGenesis = ` +{ + "chain_id": "chain-test", + "app_state": { + "checkpointing": { + "params": {}, + "genesis_keys": [ + { + "validator_address": "bbnvaloper18unlvcpj9kaa5y27ghgjtmmkcsm4gk075cz2sv", + "bls_key": { + "pubkey": "swnOf6PuVF1YDXeShKCx1M3RpNsN+rTyzUoNm9O7UJVseYbmIbqZ3WlAhHA+1bCFBImr3+bjKu0S1RZY8bOhfbRpNBNOIiSKoyGPDqKj5+BSwmIFU4IgKOd10KvYfb/J", + "pop": { + "ed25519_sig": "VAjE+l9R2ilrZxTZrmpYvEO0IIi7Y8VQacltHeAtau8MoXnxeBUbLJPIuENUBPRVzObGPpU0QMmmzJkpexdWBw==", + "bls_sig": "rF6wt1ZOVYM/xhWvh5RrIT3Lpwwtx6qRxuQh84fEInl2x5dNDSyrrA/60MIEMmm8" + } + }, + "val_pubkey": { + "key": "PUoM/ErXICyPaiByrt7X/7/AgbP0URmtC7foTECOmoc=" + } + } + ] + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "node0", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "1.000000000000000000", + "max_rate": "1.000000000000000000", + "max_change_rate": "1.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "bbn1qck5qppfs7wkj20u94q60s8j5lsqy772rfktkm", + "validator_address": "bbnvaloper1qck5qppfs7wkj20u94q60s8j5lsqy7727tuk66", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "ICl5MC/3coQYKSLGQqhIgU2Qr09fBv4tkYJ/j0d41As=" + }, + "value": { + "denom": "ubbn", + "amount": "100000000" + } + } + ], + "memo": "f9f9f5613f2010edbb6c6ed01633efadad8af269@192.168.10.2:26656", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [ + { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsOXpLQmKg88CzhYa4+c9LHKX0tlUlwyW1Lr0rf52KOp" + }, + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "sequence": "0" + } + ], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [ + "CvhDhApWLoK/Hl7PmAfXh8sG8ZOzZI4KGKvwWF/65yxTwJYFnfb43u8sa3hKkpKEIZWJpiem662yTdR6mKZWmQ==" + ] + } + ] + } + } +} +` + +var validGenesis = ` +{ + "chain_id": "chain-test", + "app_state": { + "checkpointing": { + "params": {}, + "genesis_keys": [ + { + "validator_address": "bbnvaloper1qck5qppfs7wkj20u94q60s8j5lsqy7727tuk66", + "bls_key": { + "pubkey": "qOS3pHu3OQWJAqjlFG18T+9OaQx/uY1cQ9OClmGUknL2CrO+VPpveRne7SKZojYFFuifNmpjN4bUGiRYmea7hdixpeIwFkArjxKcg264MqEcKM/UthduM+1o+lNjoxN5", + "pop": { + "ed25519_sig": "rgfes5KUA3B4lF5JjG6HRHIMb3kL+VJnMyIx4v08nBSjy+sqKvPqpxvNv6Wn+UfTXuWZ3yqRzKQyMWGsA6kPCQ==", + "bls_sig": "l/BmZn7fvctenvPqq1MB0emwKtcUfgpjvQuy+gI/AvUR27TyZNhlKcWAq+GRz/n3" + } + }, + "val_pubkey": { + "key": "ICl5MC/3coQYKSLGQqhIgU2Qr09fBv4tkYJ/j0d41As=" + } + }, + { + "validator_address": "bbnvaloper18unlvcpj9kaa5y27ghgjtmmkcsm4gk075cz2sv", + "bls_key": { + "pubkey": "swnOf6PuVF1YDXeShKCx1M3RpNsN+rTyzUoNm9O7UJVseYbmIbqZ3WlAhHA+1bCFBImr3+bjKu0S1RZY8bOhfbRpNBNOIiSKoyGPDqKj5+BSwmIFU4IgKOd10KvYfb/J", + "pop": { + "ed25519_sig": "VAjE+l9R2ilrZxTZrmpYvEO0IIi7Y8VQacltHeAtau8MoXnxeBUbLJPIuENUBPRVzObGPpU0QMmmzJkpexdWBw==", + "bls_sig": "rF6wt1ZOVYM/xhWvh5RrIT3Lpwwtx6qRxuQh84fEInl2x5dNDSyrrA/60MIEMmm8" + } + }, + "val_pubkey": { + "key": "PUoM/ErXICyPaiByrt7X/7/AgbP0URmtC7foTECOmoc=" + } + } + ] + }, + "genutil": { + "gen_txs": [ + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "node0", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "1.000000000000000000", + "max_rate": "1.000000000000000000", + "max_change_rate": "1.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "bbn1qck5qppfs7wkj20u94q60s8j5lsqy772rfktkm", + "validator_address": "bbnvaloper1qck5qppfs7wkj20u94q60s8j5lsqy7727tuk66", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "ICl5MC/3coQYKSLGQqhIgU2Qr09fBv4tkYJ/j0d41As=" + }, + "value": { + "denom": "ubbn", + "amount": "100000000" + } + } + ], + "memo": "f9f9f5613f2010edbb6c6ed01633efadad8af269@192.168.10.2:26656", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [ + { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsOXpLQmKg88CzhYa4+c9LHKX0tlUlwyW1Lr0rf52KOp" + }, + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "sequence": "0" + } + ], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [ + "CvhDhApWLoK/Hl7PmAfXh8sG8ZOzZI4KGKvwWF/65yxTwJYFnfb43u8sa3hKkpKEIZWJpiem662yTdR6mKZWmQ==" + ] + }, + { + "body": { + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "description": { + "moniker": "node1", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "1.000000000000000000", + "max_rate": "1.000000000000000000", + "max_change_rate": "1.000000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "bbn18unlvcpj9kaa5y27ghgjtmmkcsm4gk07f6ghud", + "validator_address": "bbnvaloper18unlvcpj9kaa5y27ghgjtmmkcsm4gk075cz2sv", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "PUoM/ErXICyPaiByrt7X/7/AgbP0URmtC7foTECOmoc=" + }, + "value": { + "denom": "ubbn", + "amount": "100000000" + } + } + ], + "memo": "b06768d9d68a3d5a0631b0540fb901559ab89964@192.168.10.3:26656", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [ + { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AsgcC0fVVoNoQ70AJgw/7N3exxGUHVAJ+97EMPpkL+nP" + }, + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "sequence": "0" + } + ], + "fee": { + "amount": [], + "gas_limit": "0", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [ + "SAgaaPAXNofY0wbbc9CQDcAW4HPU827i/ufD8MJup0x/aYUSMLhOr600EPtTtvoLk5sUp9o3kDDMvscb/nYdFQ==" + ] + } + ] + } + } +} +` + +func TestCheckCorrespondence(t *testing.T) { + + encodingCft := app.MakeTestEncodingConfig() + clientCtx := client.Context{}.WithCodec(encodingCft.Marshaler).WithTxConfig(encodingCft.TxConfig) + + testCases := []struct { + name string + genesis string + expErr bool + }{ + { + "valid genesis gentx and BLS key pair", + validGenesis, + false, + }, + { + "mismatched genesis state", + misMatchGenesis, + true, + }, + } + + for _, tc := range testCases { + genDoc, err := types.GenesisDocFromJSON([]byte(tc.genesis)) + require.NoError(t, err) + require.NotEmpty(t, genDoc) + genesisState, err := types2.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) + } + } +} diff --git a/x/checkpointing/types/genesis.go b/x/checkpointing/types/genesis.go index ac6ba20c5..f1518f396 100644 --- a/x/checkpointing/types/genesis.go +++ b/x/checkpointing/types/genesis.go @@ -2,6 +2,7 @@ package types import ( "encoding/json" + "errors" "io/ioutil" "github.com/cosmos/cosmos-sdk/codec" @@ -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