diff --git a/modules/apps/27-interchain-accounts/module.go b/modules/apps/27-interchain-accounts/module.go index 88679c3a4ee..954bd5ec497 100644 --- a/modules/apps/27-interchain-accounts/module.go +++ b/modules/apps/27-interchain-accounts/module.go @@ -2,6 +2,7 @@ package ica import ( "encoding/json" + "fmt" "github.com/gorilla/mux" "github.com/spf13/cobra" @@ -56,7 +57,12 @@ func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { // ValidateGenesis performs genesis state validation for the IBC interchain acounts module func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { - return nil // TODO: https://github.com/cosmos/ibc-go/issues/535 + var gs types.GenesisState + if err := cdc.UnmarshalJSON(bz, &gs); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) + } + + return gs.Validate() } // RegisterRESTRoutes implements AppModuleBasic interface diff --git a/modules/apps/27-interchain-accounts/types/genesis.go b/modules/apps/27-interchain-accounts/types/genesis.go index 9fbf9489dc1..84e489a4df6 100644 --- a/modules/apps/27-interchain-accounts/types/genesis.go +++ b/modules/apps/27-interchain-accounts/types/genesis.go @@ -1,5 +1,9 @@ package types +import ( + host "github.com/cosmos/ibc-go/v2/modules/core/24-host" +) + // DefaultGenesis creates and returns the interchain accounts GenesisState func DefaultGenesis() *GenesisState { return &GenesisState{ @@ -16,6 +20,19 @@ func NewGenesisState(controllerGenesisState ControllerGenesisState, hostGenesisS } } +// Validate performs basic validation of the interchain accounts GenesisState +func (gs GenesisState) Validate() error { + if err := gs.ControllerGenesisState.Validate(); err != nil { + return err + } + + if err := gs.HostGenesisState.Validate(); err != nil { + return err + } + + return nil +} + // DefaultControllerGenesis creates and returns the default interchain accounts ControllerGenesisState func DefaultControllerGenesis() ControllerGenesisState { return ControllerGenesisState{} @@ -30,6 +47,37 @@ func NewControllerGenesisState(channels []ActiveChannel, accounts []RegisteredIn } } +// Validate performs basic validation of the ControllerGenesisState +func (gs ControllerGenesisState) Validate() error { + for _, ch := range gs.ActiveChannels { + if err := host.ChannelIdentifierValidator(ch.ChannelId); err != nil { + return err + } + + if err := host.PortIdentifierValidator(ch.PortId); err != nil { + return err + } + } + + for _, acc := range gs.InterchainAccounts { + if err := host.PortIdentifierValidator(acc.PortId); err != nil { + return err + } + + if err := ValidateAccountAddress(acc.AccountAddress); err != nil { + return err + } + } + + for _, port := range gs.Ports { + if err := host.PortIdentifierValidator(port); err != nil { + return err + } + } + + return nil +} + // DefaultHostGenesis creates and returns the default interchain accounts HostGenesisState func DefaultHostGenesis() HostGenesisState { return HostGenesisState{ @@ -45,3 +93,32 @@ func NewHostGenesisState(channels []ActiveChannel, accounts []RegisteredIntercha Port: port, } } + +// Validate performs basic validation of the HostGenesisState +func (gs HostGenesisState) Validate() error { + for _, ch := range gs.ActiveChannels { + if err := host.ChannelIdentifierValidator(ch.ChannelId); err != nil { + return err + } + + if err := host.PortIdentifierValidator(ch.PortId); err != nil { + return err + } + } + + for _, acc := range gs.InterchainAccounts { + if err := host.PortIdentifierValidator(acc.PortId); err != nil { + return err + } + + if err := ValidateAccountAddress(acc.AccountAddress); err != nil { + return err + } + } + + if err := host.PortIdentifierValidator(gs.Port); err != nil { + return err + } + + return nil +} diff --git a/modules/apps/27-interchain-accounts/types/genesis_test.go b/modules/apps/27-interchain-accounts/types/genesis_test.go new file mode 100644 index 00000000000..939339f5fb3 --- /dev/null +++ b/modules/apps/27-interchain-accounts/types/genesis_test.go @@ -0,0 +1,311 @@ +package types_test + +import ( + "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/types" + ibctesting "github.com/cosmos/ibc-go/v2/testing" +) + +func (suite *TypesTestSuite) TestValidateGenesisState() { + var ( + genesisState types.GenesisState + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", + func() {}, + true, + }, + { + "failed to validate - empty value", + func() { + genesisState = types.GenesisState{} + }, + false, + }, + { + "failed to validate - invalid controller genesis", + func() { + genesisState = *types.NewGenesisState(types.ControllerGenesisState{Ports: []string{"invalid|port"}}, types.DefaultHostGenesis()) + }, + false, + }, + { + "failed to validate - invalid host genesis", + func() { + genesisState = *types.NewGenesisState(types.DefaultControllerGenesis(), types.HostGenesisState{}) + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + genesisState = *types.DefaultGenesis() + + tc.malleate() // malleate mutates test data + + err := genesisState.Validate() + + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + }) + } +} + +func (suite *TypesTestSuite) TestValidateControllerGenesisState() { + var ( + genesisState types.ControllerGenesisState + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", + func() {}, + true, + }, + { + "failed to validate active channel - invalid port identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: "invalid|port", + ChannelId: ibctesting.FirstChannelID, + }, + } + + genesisState = types.NewControllerGenesisState(activeChannels, []types.RegisteredInterchainAccount{}, []string{}) + }, + false, + }, + { + "failed to validate active channel - invalid channel identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: "invalid|channel", + }, + } + + genesisState = types.NewControllerGenesisState(activeChannels, []types.RegisteredInterchainAccount{}, []string{}) + }, + false, + }, + { + "failed to validate registered account - invalid port identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: ibctesting.FirstChannelID, + }, + } + + registeredAccounts := []types.RegisteredInterchainAccount{ + { + PortId: "invalid|port", + AccountAddress: TestOwnerAddress, + }, + } + + genesisState = types.NewControllerGenesisState(activeChannels, registeredAccounts, []string{}) + }, + false, + }, + { + "failed to validate registered account - invalid owner address", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: ibctesting.FirstChannelID, + }, + } + + registeredAccounts := []types.RegisteredInterchainAccount{ + { + PortId: TestPortID, + AccountAddress: "", + }, + } + + genesisState = types.NewControllerGenesisState(activeChannels, registeredAccounts, []string{}) + }, + false, + }, + { + "failed to validate controller ports - invalid port identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: ibctesting.FirstChannelID, + }, + } + + registeredAccounts := []types.RegisteredInterchainAccount{ + { + PortId: TestPortID, + AccountAddress: TestOwnerAddress, + }, + } + + genesisState = types.NewControllerGenesisState(activeChannels, registeredAccounts, []string{"invalid|port"}) + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + genesisState = types.DefaultControllerGenesis() + + tc.malleate() // malleate mutates test data + + err := genesisState.Validate() + + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + }) + } +} + +func (suite *TypesTestSuite) TestValidateHostGenesisState() { + var ( + genesisState types.HostGenesisState + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", + func() {}, + true, + }, + { + "failed to validate active channel - invalid port identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: "invalid|port", + ChannelId: ibctesting.FirstChannelID, + }, + } + + genesisState = types.NewHostGenesisState(activeChannels, []types.RegisteredInterchainAccount{}, types.PortID) + }, + false, + }, + { + "failed to validate active channel - invalid channel identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: "invalid|channel", + }, + } + + genesisState = types.NewHostGenesisState(activeChannels, []types.RegisteredInterchainAccount{}, types.PortID) + }, + false, + }, + { + "failed to validate registered account - invalid port identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: ibctesting.FirstChannelID, + }, + } + + registeredAccounts := []types.RegisteredInterchainAccount{ + { + PortId: "invalid|port", + AccountAddress: TestOwnerAddress, + }, + } + + genesisState = types.NewHostGenesisState(activeChannels, registeredAccounts, types.PortID) + }, + false, + }, + { + "failed to validate registered account - invalid owner address", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: ibctesting.FirstChannelID, + }, + } + + registeredAccounts := []types.RegisteredInterchainAccount{ + { + PortId: TestPortID, + AccountAddress: "", + }, + } + + genesisState = types.NewHostGenesisState(activeChannels, registeredAccounts, types.PortID) + }, + false, + }, + { + "failed to validate controller ports - invalid port identifier", + func() { + activeChannels := []types.ActiveChannel{ + { + PortId: TestPortID, + ChannelId: ibctesting.FirstChannelID, + }, + } + + registeredAccounts := []types.RegisteredInterchainAccount{ + { + PortId: TestPortID, + AccountAddress: TestOwnerAddress, + }, + } + + genesisState = types.NewHostGenesisState(activeChannels, registeredAccounts, "invalid|port") + }, + false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + genesisState = types.DefaultHostGenesis() + + tc.malleate() // malleate mutates test data + + err := genesisState.Validate() + + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + }) + } +} diff --git a/modules/apps/27-interchain-accounts/types/version.go b/modules/apps/27-interchain-accounts/types/version.go index 0cf8e2623cd..ffa33aa0017 100644 --- a/modules/apps/27-interchain-accounts/types/version.go +++ b/modules/apps/27-interchain-accounts/types/version.go @@ -45,7 +45,17 @@ func ValidateVersion(version string) error { return sdkerrors.Wrapf(ErrInvalidVersion, "expected %s, got %s", VersionPrefix, s[0]) } - if !IsValidAddr(s[1]) || len(s[1]) == 0 || len(s[1]) > DefaultMaxAddrLength { + if err := ValidateAccountAddress(s[1]); err != nil { + return err + } + + return nil +} + +// ValidateAccountAddress performs basic validation of interchain account addresses, enforcing constraints +// on address length and character set +func ValidateAccountAddress(addr string) error { + if !IsValidAddr(addr) || len(addr) == 0 || len(addr) > DefaultMaxAddrLength { return sdkerrors.Wrapf( ErrInvalidAccountAddress, "address must contain strictly alphanumeric characters, not exceeding %d characters in length",