diff --git a/x/ibc/02-client/alias.go b/x/ibc/02-client/alias.go index 762cb695ab2..a274af3cc80 100644 --- a/x/ibc/02-client/alias.go +++ b/x/ibc/02-client/alias.go @@ -37,6 +37,9 @@ var ( ErrRootNotFound = types.ErrRootNotFound ErrInvalidHeader = types.ErrInvalidHeader ErrInvalidEvidence = types.ErrInvalidEvidence + DefaultGenesisState = types.DefaultGenesisState + NewGenesisState = types.NewGenesisState + NewClientConsensusStates = types.NewClientConsensusStates // variable aliases SubModuleCdc = types.SubModuleCdc @@ -45,7 +48,10 @@ var ( AttributeValueCategory = types.AttributeValueCategory ) +// nolint type ( - Keeper = keeper.Keeper - StakingKeeper = types.StakingKeeper + Keeper = keeper.Keeper + StakingKeeper = types.StakingKeeper + GenesisState = types.GenesisState + ClientConsensusStates = types.ClientConsensusStates ) diff --git a/x/ibc/02-client/exported/exported.go b/x/ibc/02-client/exported/exported.go index a071d94a02f..90e8b7a0730 100644 --- a/x/ibc/02-client/exported/exported.go +++ b/x/ibc/02-client/exported/exported.go @@ -20,6 +20,7 @@ type ClientState interface { ClientType() ClientType GetLatestHeight() uint64 IsFrozen() bool + Validate() error // State verification functions diff --git a/x/ibc/02-client/genesis.go b/x/ibc/02-client/genesis.go new file mode 100644 index 00000000000..b16c2e9372e --- /dev/null +++ b/x/ibc/02-client/genesis.go @@ -0,0 +1,27 @@ +package client + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// InitGenesis initializes the ibc client submodule's state from a provided genesis +// state. +func InitGenesis(ctx sdk.Context, k Keeper, gs GenesisState) { + for _, client := range gs.Clients { + k.SetClientState(ctx, client) + k.SetClientType(ctx, client.GetID(), client.ClientType()) + } + for _, cs := range gs.ClientsConsensus { + for _, consState := range cs.ConsensusStates { + k.SetClientConsensusState(ctx, cs.ClientID, consState.GetHeight(), consState) + } + } +} + +// ExportGenesis returns the ibc client submodule's exported genesis. +func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { + return GenesisState{ + Clients: k.GetAllClients(ctx), + ClientsConsensus: k.GetAllConsensusStates(ctx), + } +} diff --git a/x/ibc/02-client/keeper/keeper.go b/x/ibc/02-client/keeper/keeper.go index 9578d15209f..3149e721b34 100644 --- a/x/ibc/02-client/keeper/keeper.go +++ b/x/ibc/02-client/keeper/keeper.go @@ -98,6 +98,67 @@ func (k Keeper) SetClientConsensusState(ctx sdk.Context, clientID string, height store.Set(ibctypes.KeyConsensusState(height), bz) } +// IterateConsensusStates provides an iterator over all stored consensus states. +// objects. For each State object, cb will be called. If the cb returns true, +// the iterator will close and stop. +func (k Keeper) IterateConsensusStates(ctx sdk.Context, cb func(clientID string, cs exported.ConsensusState) bool) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, ibctypes.KeyClientStorePrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + keySplit := strings.Split(string(iterator.Key()), "/") + // consensus key is in the format "clients//consensusState/" + if len(keySplit) != 4 || keySplit[2] != "consensusState" { + continue + } + clientID := keySplit[1] + var consensusState exported.ConsensusState + k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &consensusState) + + if cb(clientID, consensusState) { + break + } + } +} + +// GetAllConsensusStates returns all stored client consensus states. +// NOTE: non deterministic. +func (k Keeper) GetAllConsensusStates(ctx sdk.Context) (clientConsStates []types.ClientConsensusStates) { + var clientIDs []string + // create map to add consensus states to the existing clients + cons := make(map[string][]exported.ConsensusState) + + k.IterateConsensusStates(ctx, func(clientID string, cs exported.ConsensusState) bool { + consensusStates, ok := cons[clientID] + if !ok { + clientIDs = append(clientIDs, clientID) + cons[clientID] = []exported.ConsensusState{cs} + return false + } + + cons[clientID] = append(consensusStates, cs) + return false + }) + + // create ClientConsensusStates in the same order of iteration to prevent non-determinism + for len(clientIDs) > 0 { + id := clientIDs[len(clientIDs)-1] + consensusStates, ok := cons[id] + if !ok { + panic(fmt.Sprintf("consensus states from client id %s not found", id)) + } + + clientConsState := types.NewClientConsensusStates(id, consensusStates) + clientConsStates = append(clientConsStates, clientConsState) + + // remove the last element + clientIDs = clientIDs[:len(clientIDs)-1] + } + + return clientConsStates +} + // HasClientConsensusState returns if keeper has a ConsensusState for a particular // client at the given height func (k Keeper) HasClientConsensusState(ctx sdk.Context, clientID string, height uint64) bool { diff --git a/x/ibc/02-client/keeper/keeper_test.go b/x/ibc/02-client/keeper/keeper_test.go index 30f6bf405e7..4b5104b32fd 100644 --- a/x/ibc/02-client/keeper/keeper_test.go +++ b/x/ibc/02-client/keeper/keeper_test.go @@ -15,6 +15,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" "github.com/cosmos/cosmos-sdk/x/ibc/02-client/keeper" + "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" "github.com/cosmos/cosmos-sdk/x/staking" @@ -161,7 +162,9 @@ func (suite KeeperTestSuite) TestGetConsensusState() { func (suite KeeperTestSuite) TestConsensusStateHelpers() { // initial setup - clientState, _ := ibctmtypes.Initialize(testClientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header) + clientState, err := ibctmtypes.Initialize(testClientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header) + suite.Require().NoError(err) + suite.keeper.SetClientState(suite.ctx, clientState) suite.keeper.SetClientConsensusState(suite.ctx, testClientID, testClientHeight, suite.consensusState) @@ -192,3 +195,37 @@ func (suite KeeperTestSuite) TestConsensusStateHelpers() { suite.Require().True(ok) suite.Require().Equal(suite.consensusState, lte, "LTE helper function did not return latest client state below height: %d", testClientHeight+3) } + +func (suite KeeperTestSuite) TestGetAllConsensusStates() { + expConsensus := []types.ClientConsensusStates{ + types.NewClientConsensusStates( + testClientID, + []exported.ConsensusState{ + ibctmtypes.NewConsensusState( + suite.consensusState.Timestamp, commitmenttypes.NewMerkleRoot([]byte("hash")), suite.consensusState.GetHeight(), &tmtypes.ValidatorSet{}, + ), + ibctmtypes.NewConsensusState( + suite.consensusState.Timestamp.Add(time.Minute), commitmenttypes.NewMerkleRoot([]byte("app_hash")), suite.consensusState.GetHeight()+1, &tmtypes.ValidatorSet{}, + ), + }, + ), + types.NewClientConsensusStates( + testClientID2, + []exported.ConsensusState{ + ibctmtypes.NewConsensusState( + suite.consensusState.Timestamp.Add(2*time.Minute), commitmenttypes.NewMerkleRoot([]byte("app_hash_2")), suite.consensusState.GetHeight()+2, &tmtypes.ValidatorSet{}, + ), + }, + ), + } + + for i := range expConsensus { + for _, cons := range expConsensus[i].ConsensusStates { + suite.keeper.SetClientConsensusState(suite.ctx, expConsensus[i].ClientID, cons.GetHeight(), cons) + } + } + + consStates := suite.keeper.GetAllConsensusStates(suite.ctx) + suite.Require().Len(consStates, len(expConsensus)) + suite.Require().Equal(expConsensus, consStates) +} diff --git a/x/ibc/02-client/types/genesis.go b/x/ibc/02-client/types/genesis.go new file mode 100644 index 00000000000..aa41ee5fcd1 --- /dev/null +++ b/x/ibc/02-client/types/genesis.go @@ -0,0 +1,69 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" +) + +// ClientConsensusStates defines all the stored consensus states for a given client. +type ClientConsensusStates struct { + ClientID string `json:"client_id" yaml:"client_id"` + ConsensusStates []exported.ConsensusState `json:"consensus_states" yaml:"consensus_states"` +} + +// NewClientConsensusStates creates a new ClientConsensusStates instance. +func NewClientConsensusStates(id string, states []exported.ConsensusState) ClientConsensusStates { + return ClientConsensusStates{ + ClientID: id, + ConsensusStates: states, + } +} + +// GenesisState defines the ibc client submodule's genesis state. +type GenesisState struct { + Clients []exported.ClientState `json:"clients" yaml:"clients"` + ClientsConsensus []ClientConsensusStates `json:"clients_consensus" yaml:"clients_consensus"` +} + +// NewGenesisState creates a GenesisState instance. +func NewGenesisState( + clients []exported.ClientState, clientsConsensus []ClientConsensusStates, +) GenesisState { + return GenesisState{ + Clients: clients, + ClientsConsensus: clientsConsensus, + } +} + +// DefaultGenesisState returns the ibc client submodule's default genesis state. +func DefaultGenesisState() GenesisState { + return GenesisState{ + Clients: []exported.ClientState{}, + ClientsConsensus: []ClientConsensusStates{}, + } +} + +// Validate performs basic genesis state validation returning an error upon any +// failure. +func (gs GenesisState) Validate() error { + for i, client := range gs.Clients { + if err := client.Validate(); err != nil { + return fmt.Errorf("invalid client %d: %w", i, err) + } + } + + for i, cs := range gs.ClientsConsensus { + if err := host.DefaultClientIdentifierValidator(cs.ClientID); err != nil { + return fmt.Errorf("invalid client consensus state %d: %w", i, err) + } + for _, consensusState := range cs.ConsensusStates { + if err := consensusState.ValidateBasic(); err != nil { + return fmt.Errorf("invalid client consensus state %d: %w", i, err) + } + } + } + + return nil +} diff --git a/x/ibc/02-client/types/genesis_test.go b/x/ibc/02-client/types/genesis_test.go new file mode 100644 index 00000000000..ebc65ca44a2 --- /dev/null +++ b/x/ibc/02-client/types/genesis_test.go @@ -0,0 +1,135 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/store/cachekv" + "github.com/cosmos/cosmos-sdk/store/dbadapter" + "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" + "github.com/cosmos/cosmos-sdk/x/ibc/02-client/types" + ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" + localhosttypes "github.com/cosmos/cosmos-sdk/x/ibc/09-localhost/types" + commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" +) + +const ( + clientID = "ethbridge" + + trustingPeriod time.Duration = time.Hour * 24 * 7 * 2 + ubdPeriod time.Duration = time.Hour * 24 * 7 * 3 + maxClockDrift time.Duration = time.Second * 10 +) + +func TestValidateGenesis(t *testing.T) { + privVal := tmtypes.NewMockPV() + pubKey, err := privVal.GetPubKey() + require.NoError(t, err) + + now := time.Now().UTC() + + val := tmtypes.NewValidator(pubKey, 10) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{val}) + + mem := dbadapter.Store{DB: dbm.NewMemDB()} + store := cachekv.NewStore(mem) + header := ibctmtypes.CreateTestHeader("chainID", 10, now, valSet, []tmtypes.PrivValidator{privVal}) + + testCases := []struct { + name string + genState types.GenesisState + expPass bool + }{ + { + name: "default", + genState: types.DefaultGenesisState(), + expPass: true, + }, + { + name: "valid genesis", + genState: types.NewGenesisState( + []exported.ClientState{ + ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header), + localhosttypes.NewClientState(store, "chaindID", 10), + }, + []types.ClientConsensusStates{ + { + clientID, + []exported.ConsensusState{ + ibctmtypes.NewConsensusState( + header.Time, commitmenttypes.NewMerkleRoot(header.AppHash), header.GetHeight(), header.ValidatorSet, + ), + }, + }, + }, + ), + expPass: true, + }, + { + name: "invalid client", + genState: types.NewGenesisState( + []exported.ClientState{ + ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header), + localhosttypes.NewClientState(store, "chaindID", 0), + }, + nil, + ), + expPass: false, + }, + { + name: "invalid consensus state", + genState: types.NewGenesisState( + []exported.ClientState{ + ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header), + localhosttypes.NewClientState(store, "chaindID", 10), + }, + []types.ClientConsensusStates{ + { + "CLIENTID2", + []exported.ConsensusState{ + ibctmtypes.NewConsensusState( + header.Time, commitmenttypes.NewMerkleRoot(header.AppHash), 0, header.ValidatorSet, + ), + }, + }, + }, + ), + expPass: false, + }, + { + name: "invalid consensus state", + genState: types.NewGenesisState( + []exported.ClientState{ + ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, header), + localhosttypes.NewClientState(store, "chaindID", 10), + }, + []types.ClientConsensusStates{ + types.NewClientConsensusStates( + clientID, + []exported.ConsensusState{ + ibctmtypes.NewConsensusState( + header.Time, commitmenttypes.NewMerkleRoot(header.AppHash), 0, header.ValidatorSet, + ), + }, + ), + }, + ), + expPass: false, + }, + } + + for _, tc := range testCases { + tc := tc + err := tc.genState.Validate() + if tc.expPass { + require.NoError(t, err, tc.name) + } else { + require.Error(t, err, tc.name) + } + } +} diff --git a/x/ibc/07-tendermint/types/client_state.go b/x/ibc/07-tendermint/types/client_state.go index 2974072372a..8d93ba6bb89 100644 --- a/x/ibc/07-tendermint/types/client_state.go +++ b/x/ibc/07-tendermint/types/client_state.go @@ -15,6 +15,7 @@ import ( channeltypes "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/types" commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" ) @@ -87,7 +88,10 @@ func (cs ClientState) GetID() string { // GetChainID returns the chain-id from the last header func (cs ClientState) GetChainID() string { - return cs.LastHeader.ChainID + if cs.LastHeader.SignedHeader.Header == nil { + return "" + } + return cs.LastHeader.SignedHeader.Header.ChainID } // ClientType is tendermint. @@ -110,6 +114,23 @@ func (cs ClientState) IsFrozen() bool { return cs.FrozenHeight != 0 } +// Validate performs a basic validation of the client state fields. +func (cs ClientState) Validate() error { + if err := host.DefaultClientIdentifierValidator(cs.ID); err != nil { + return err + } + if cs.TrustingPeriod == 0 { + return errors.New("trusting period cannot be zero") + } + if cs.UnbondingPeriod == 0 { + return errors.New("unbonding period cannot be zero") + } + if cs.MaxClockDrift == 0 { + return errors.New("max clock drift cannot be zero") + } + return cs.LastHeader.ValidateBasic(cs.GetChainID()) +} + // VerifyClientConsensusState verifies a proof of the consensus state of the // Tendermint client stored on the target machine. func (cs ClientState) VerifyClientConsensusState( diff --git a/x/ibc/07-tendermint/types/client_state_test.go b/x/ibc/07-tendermint/types/client_state_test.go index d9ae9393b26..3b88105708f 100644 --- a/x/ibc/07-tendermint/types/client_state_test.go +++ b/x/ibc/07-tendermint/types/client_state_test.go @@ -5,17 +5,67 @@ import ( connectionexported "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/exported" channel "github.com/cosmos/cosmos-sdk/x/ibc/04-channel" channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported" + "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" ) const ( + testClientID = "clientidone" testConnectionID = "connectionid" testPortID = "testportid" testChannelID = "testchannelid" testSequence = 1 ) +func (suite *TendermintTestSuite) TestValidate() { + testCases := []struct { + name string + clientState types.ClientState + expPass bool + }{ + { + name: "valid client", + clientState: ibctmtypes.NewClientState(testClientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header), + expPass: true, + }, + { + name: "invalid client id", + clientState: ibctmtypes.NewClientState("testClientID", trustingPeriod, ubdPeriod, maxClockDrift, suite.header), + expPass: false, + }, + { + name: "invalid trusting period", + clientState: ibctmtypes.NewClientState(testClientID, 0, ubdPeriod, maxClockDrift, suite.header), + expPass: false, + }, + { + name: "invalid unbonding period", + clientState: ibctmtypes.NewClientState(testClientID, trustingPeriod, 0, maxClockDrift, suite.header), + expPass: false, + }, + { + name: "invalid max clock drift", + clientState: ibctmtypes.NewClientState(testClientID, trustingPeriod, ubdPeriod, 0, suite.header), + expPass: false, + }, + { + name: "invalid header", + clientState: ibctmtypes.NewClientState(testClientID, trustingPeriod, ubdPeriod, maxClockDrift, ibctmtypes.Header{}), + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.clientState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + } +} + func (suite *TendermintTestSuite) TestVerifyClientConsensusState() { testCases := []struct { name string diff --git a/x/ibc/07-tendermint/types/consensus_state.go b/x/ibc/07-tendermint/types/consensus_state.go index aecc7c981a8..2bfc29f2b2b 100644 --- a/x/ibc/07-tendermint/types/consensus_state.go +++ b/x/ibc/07-tendermint/types/consensus_state.go @@ -19,6 +19,19 @@ type ConsensusState struct { ValidatorSet *tmtypes.ValidatorSet `json:"validator_set" yaml:"validator_set"` } +// NewConsensusState creates a new ConsensusState instance. +func NewConsensusState( + timestamp time.Time, root commitmentexported.Root, height uint64, + valset *tmtypes.ValidatorSet, +) ConsensusState { + return ConsensusState{ + Timestamp: timestamp, + Root: root, + Height: height, + ValidatorSet: valset, + } +} + // ClientType returns Tendermint func (ConsensusState) ClientType() clientexported.ClientType { return clientexported.Tendermint diff --git a/x/ibc/07-tendermint/types/msgs.go b/x/ibc/07-tendermint/types/msgs.go index 07bbb9270d4..0095ba5b9fc 100644 --- a/x/ibc/07-tendermint/types/msgs.go +++ b/x/ibc/07-tendermint/types/msgs.go @@ -73,6 +73,9 @@ func (msg MsgCreateClient) ValidateBasic() error { if msg.Signer.Empty() { return sdkerrors.ErrInvalidAddress } + if msg.Header.SignedHeader.Header == nil { + return sdkerrors.Wrap(ErrInvalidHeader, "header cannot be nil") + } // ValidateBasic of provided header with self-attested chain-id if err := msg.Header.ValidateBasic(msg.Header.ChainID); err != nil { return sdkerrors.Wrapf(ErrInvalidHeader, "header failed validatebasic with its own chain-id: %v", err) diff --git a/x/ibc/07-tendermint/types/msgs_test.go b/x/ibc/07-tendermint/types/msgs_test.go index 5a36acd677f..3fdf897950d 100644 --- a/x/ibc/07-tendermint/types/msgs_test.go +++ b/x/ibc/07-tendermint/types/msgs_test.go @@ -19,18 +19,16 @@ func (suite *TendermintTestSuite) TestMsgCreateClientValidateBasic() { }{ {ibctmtypes.NewMsgCreateClient(exported.ClientTypeTendermint, suite.header, trustingPeriod, ubdPeriod, maxClockDrift, signer), true, "success msg should pass"}, {ibctmtypes.NewMsgCreateClient("BADCHAIN", suite.header, trustingPeriod, ubdPeriod, maxClockDrift, signer), false, "invalid client id passed"}, - {ibctmtypes.NewMsgCreateClient("goodchain", suite.header, trustingPeriod, ubdPeriod, maxClockDrift, signer), false, "unregistered client type passed"}, - {ibctmtypes.NewMsgCreateClient("goodchain", suite.header, trustingPeriod, ubdPeriod, maxClockDrift, signer), false, "invalid Consensus State in msg passed"}, - {ibctmtypes.NewMsgCreateClient("goodchain", suite.header, 0, ubdPeriod, maxClockDrift, signer), false, "zero trusting period passed"}, - {ibctmtypes.NewMsgCreateClient("goodchain", suite.header, trustingPeriod, 0, maxClockDrift, signer), false, "zero unbonding period passed"}, - {ibctmtypes.NewMsgCreateClient("goodchain", suite.header, trustingPeriod, ubdPeriod, maxClockDrift, nil), false, "Empty address passed"}, - {ibctmtypes.NewMsgCreateClient("goodchain", suite.header, trustingPeriod, ubdPeriod, maxClockDrift, nil), false, "Empty chain ID"}, + {ibctmtypes.NewMsgCreateClient(exported.ClientTypeTendermint, suite.header, 0, ubdPeriod, maxClockDrift, signer), false, "zero trusting period passed"}, + {ibctmtypes.NewMsgCreateClient(exported.ClientTypeTendermint, suite.header, trustingPeriod, 0, maxClockDrift, signer), false, "zero unbonding period passed"}, + {ibctmtypes.NewMsgCreateClient(exported.ClientTypeTendermint, suite.header, trustingPeriod, ubdPeriod, maxClockDrift, nil), false, "Empty address passed"}, + {ibctmtypes.NewMsgCreateClient(exported.ClientTypeTendermint, ibctmtypes.Header{}, trustingPeriod, ubdPeriod, maxClockDrift, signer), false, "nil header"}, } for i, tc := range cases { err := tc.msg.ValidateBasic() if tc.expPass { - suite.Require().NoError(err, "Msg %d failed: %v", i, err) + suite.Require().NoError(err, "Msg %d failed: %v", i, tc.errMsg) } else { suite.Require().Error(err, "Invalid Msg %d passed: %s", i, tc.errMsg) } diff --git a/x/ibc/09-localhost/types/client_state.go b/x/ibc/09-localhost/types/client_state.go index fa49b4c329d..b325d30f694 100644 --- a/x/ibc/09-localhost/types/client_state.go +++ b/x/ibc/09-localhost/types/client_state.go @@ -3,7 +3,9 @@ package types import ( "bytes" "encoding/binary" + "errors" "fmt" + "strings" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -15,6 +17,7 @@ import ( channelexported "github.com/cosmos/cosmos-sdk/x/ibc/04-channel/exported" commitmentexported "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/exported" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" + host "github.com/cosmos/cosmos-sdk/x/ibc/24-host" ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" ) @@ -63,6 +66,23 @@ func (cs ClientState) IsFrozen() bool { return false } +// Validate performs a basic validation of the client state fields. +func (cs ClientState) Validate() error { + if err := host.DefaultClientIdentifierValidator(cs.ID); err != nil { + return err + } + if strings.TrimSpace(cs.ChainID) == "" { + return errors.New("chain id cannot be blank") + } + if cs.Height <= 0 { + return fmt.Errorf("height must be positive: %d", cs.Height) + } + if cs.store == nil { + return errors.New("KVStore cannot be nil") + } + return nil +} + // VerifyClientConsensusState verifies a proof of the consensus // state of the loop-back client. // VerifyClientConsensusState verifies a proof of the consensus state of the diff --git a/x/ibc/09-localhost/types/client_state_test.go b/x/ibc/09-localhost/types/client_state_test.go index a36d0087d90..ed3063a97e8 100644 --- a/x/ibc/09-localhost/types/client_state_test.go +++ b/x/ibc/09-localhost/types/client_state_test.go @@ -16,6 +16,44 @@ const ( testSequence = 1 ) +func (suite *LocalhostTestSuite) TestValidate() { + testCases := []struct { + name string + clientState types.ClientState + expPass bool + }{ + { + name: "valid client", + clientState: types.NewClientState(suite.store, "chainID", 10), + expPass: true, + }, + { + name: "invalid chain id", + clientState: types.NewClientState(suite.store, " ", 10), + expPass: false, + }, + { + name: "invalid height", + clientState: types.NewClientState(suite.store, "chainID", 0), + expPass: false, + }, + { + name: "invalid store", + clientState: types.NewClientState(nil, "chainID", 10), + expPass: false, + }, + } + + for _, tc := range testCases { + err := tc.clientState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + } +} + func (suite *LocalhostTestSuite) TestVerifyClientConsensusState() { testCases := []struct { name string diff --git a/x/ibc/24-host/validate.go b/x/ibc/24-host/validate.go index e75e63e6c2d..927add342c6 100644 --- a/x/ibc/24-host/validate.go +++ b/x/ibc/24-host/validate.go @@ -21,7 +21,7 @@ func defaultIdentifierValidator(id string, min, max int) error { if strings.Contains(id, "/") { return sdkerrors.Wrapf(ErrInvalidID, "identifier %s cannot contain separator '/'", id) } - // valid id must be between 10 and 20 characters + // valid id must be between 9 and 20 characters if len(id) < min || len(id) > max { return sdkerrors.Wrapf(ErrInvalidID, "identifier %s has invalid length: %d, must be between %d-%d characters", id, len(id), min, max) } @@ -33,10 +33,10 @@ func defaultIdentifierValidator(id string, min, max int) error { } // DefaultClientIdentifierValidator is the default validator function for Client identifiers -// A valid Identifier must be between 10-20 characters and only contain lowercase +// A valid Identifier must be between 9-20 characters and only contain lowercase // alphabetic characters, func DefaultClientIdentifierValidator(id string) error { - return defaultIdentifierValidator(id, 10, 20) + return defaultIdentifierValidator(id, 9, 20) } // DefaultConnectionIdentifierValidator is the default validator function for Connection identifiers diff --git a/x/ibc/genesis.go b/x/ibc/genesis.go index 9be12b9aa66..3d2fc367649 100644 --- a/x/ibc/genesis.go +++ b/x/ibc/genesis.go @@ -2,17 +2,20 @@ package ibc import ( sdk "github.com/cosmos/cosmos-sdk/types" + client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" ) // GenesisState defines the ibc module's genesis state. type GenesisState struct { + ClientGenesis client.GenesisState `json:"client_genesis" yaml:"client_genesis"` ConnectionGenesis connection.GenesisState `json:"connection_genesis" yaml:"connection_genesis"` } // DefaultGenesisState returns the ibc module's default genesis state. func DefaultGenesisState() GenesisState { return GenesisState{ + ClientGenesis: client.DefaultGenesisState(), ConnectionGenesis: connection.DefaultGenesisState(), } } @@ -20,18 +23,24 @@ func DefaultGenesisState() GenesisState { // Validate performs basic genesis state validation returning an error upon any // failure. func (gs GenesisState) Validate() error { + if err := gs.ClientGenesis.Validate(); err != nil { + return err + } + return gs.ConnectionGenesis.Validate() } -// InitGenesis initializes the ibc connection submodule's state from a provided genesis +// InitGenesis initializes the ibc state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k Keeper, gs GenesisState) { + client.InitGenesis(ctx, k.ClientKeeper, gs.ClientGenesis) connection.InitGenesis(ctx, k.ConnectionKeeper, gs.ConnectionGenesis) } -// ExportGenesis returns the ibc connection submodule's exported genesis. +// ExportGenesis returns the ibc exported genesis. func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { return GenesisState{ + ClientGenesis: client.ExportGenesis(ctx, k.ClientKeeper), ConnectionGenesis: connection.ExportGenesis(ctx, k.ConnectionKeeper), } } diff --git a/x/ibc/genesis_test.go b/x/ibc/genesis_test.go index 1957857ec24..e1ea2abc987 100644 --- a/x/ibc/genesis_test.go +++ b/x/ibc/genesis_test.go @@ -1,38 +1,47 @@ -package ibc +package ibc_test import ( - "testing" - - "github.com/stretchr/testify/require" - + "github.com/cosmos/cosmos-sdk/x/ibc" + client "github.com/cosmos/cosmos-sdk/x/ibc/02-client" + "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" connection "github.com/cosmos/cosmos-sdk/x/ibc/03-connection" connectionexported "github.com/cosmos/cosmos-sdk/x/ibc/03-connection/exported" + ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" + localhosttypes "github.com/cosmos/cosmos-sdk/x/ibc/09-localhost/types" commitmenttypes "github.com/cosmos/cosmos-sdk/x/ibc/23-commitment/types" ibctypes "github.com/cosmos/cosmos-sdk/x/ibc/types" ) -var ( - connectionID = "connectionidone" - clientID = "clientidone" - connectionID2 = "connectionidtwo" - clientID2 = "clientidtwo" -) - -func TestValidateGenesis(t *testing.T) { - +func (suite *IBCTestSuite) TestValidateGenesis() { testCases := []struct { name string - genState GenesisState + genState ibc.GenesisState expPass bool }{ { name: "default", - genState: DefaultGenesisState(), + genState: ibc.DefaultGenesisState(), expPass: true, }, { name: "valid genesis", - genState: GenesisState{ + genState: ibc.GenesisState{ + ClientGenesis: client.NewGenesisState( + []exported.ClientState{ + ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header), + localhosttypes.NewClientState(suite.store, "chaindID", 10), + }, + []client.ClientConsensusStates{ + client.NewClientConsensusStates( + clientID, + []exported.ConsensusState{ + ibctmtypes.NewConsensusState( + suite.header.Time, commitmenttypes.NewMerkleRoot(suite.header.AppHash), suite.header.GetHeight(), suite.header.ValidatorSet, + ), + }, + ), + }, + ), ConnectionGenesis: connection.NewGenesisState( []connection.ConnectionEnd{ connection.NewConnectionEnd(connectionexported.INIT, connectionID, clientID, connection.NewCounterparty(clientID2, connectionID2, commitmenttypes.NewMerklePrefix([]byte("prefix"))), []string{"1.0.0"}), @@ -44,14 +53,31 @@ func TestValidateGenesis(t *testing.T) { }, expPass: true, }, + { + name: "invalid client genesis", + genState: ibc.GenesisState{ + ClientGenesis: client.NewGenesisState( + []exported.ClientState{ + ibctmtypes.NewClientState(clientID, trustingPeriod, ubdPeriod, maxClockDrift, suite.header), + localhosttypes.NewClientState(suite.store, "chaindID", 0), + }, + nil, + ), + ConnectionGenesis: connection.DefaultGenesisState(), + }, + expPass: false, + }, { name: "invalid connection genesis", - genState: GenesisState{ + genState: ibc.GenesisState{ + ClientGenesis: client.DefaultGenesisState(), ConnectionGenesis: connection.NewGenesisState( []connection.ConnectionEnd{ connection.NewConnectionEnd(connectionexported.INIT, connectionID, "CLIENTIDONE", connection.NewCounterparty(clientID, connectionID2, commitmenttypes.NewMerklePrefix([]byte("prefix"))), []string{"1.0.0"}), }, - nil, + []connection.ConnectionPaths{ + connection.NewConnectionPaths(clientID, []string{ibctypes.ConnectionPath(connectionID)}), + }, ), }, expPass: false, @@ -62,9 +88,9 @@ func TestValidateGenesis(t *testing.T) { tc := tc err := tc.genState.Validate() if tc.expPass { - require.NoError(t, err, tc.name) + suite.Require().NoError(err, tc.name) } else { - require.Error(t, err, tc.name) + suite.Require().Error(err, tc.name) } } } diff --git a/x/ibc/ibc_test.go b/x/ibc/ibc_test.go new file mode 100644 index 00000000000..00dcb348274 --- /dev/null +++ b/x/ibc/ibc_test.go @@ -0,0 +1,65 @@ +package ibc_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/store/cachekv" + "github.com/cosmos/cosmos-sdk/store/dbadapter" + sdk "github.com/cosmos/cosmos-sdk/types" + ibctmtypes "github.com/cosmos/cosmos-sdk/x/ibc/07-tendermint/types" +) + +const ( + connectionID = "connectionidone" + clientID = "clientidone" + connectionID2 = "connectionidtwo" + clientID2 = "clientidtwo" + + trustingPeriod time.Duration = time.Hour * 24 * 7 * 2 + ubdPeriod time.Duration = time.Hour * 24 * 7 * 3 + maxClockDrift time.Duration = time.Second * 10 +) + +type IBCTestSuite struct { + suite.Suite + + cdc *codec.Codec + ctx sdk.Context + app *simapp.SimApp + store sdk.KVStore + header ibctmtypes.Header +} + +func (suite *IBCTestSuite) SetupTest() { + isCheckTx := false + suite.app = simapp.Setup(isCheckTx) + + privVal := tmtypes.NewMockPV() + pubKey, err := privVal.GetPubKey() + suite.Require().NoError(err) + + now := time.Now().UTC() + + val := tmtypes.NewValidator(pubKey, 10) + valSet := tmtypes.NewValidatorSet([]*tmtypes.Validator{val}) + + mem := dbadapter.Store{DB: dbm.NewMemDB()} + suite.store = cachekv.NewStore(mem) + suite.header = ibctmtypes.CreateTestHeader("chainID", 10, now, valSet, []tmtypes.PrivValidator{privVal}) + + suite.cdc = suite.app.Codec() + suite.ctx = suite.app.BaseApp.NewContext(isCheckTx, abci.Header{}) +} + +func TestIBCTestSuite(t *testing.T) { + suite.Run(t, new(IBCTestSuite)) +}