diff --git a/go.mod b/go.mod index 15e2b7af1b..2ebb536dbc 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( require ( github.com/golang/mock v1.6.0 + github.com/oxyno-zeta/gomock-extra-matcher v1.1.0 github.com/regen-network/cosmos-proto v0.3.1 ) diff --git a/go.sum b/go.sum index 94eb27ca8a..6c55c682da 100644 --- a/go.sum +++ b/go.sum @@ -863,6 +863,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/oxyno-zeta/gomock-extra-matcher v1.1.0 h1:Yyk5ov0ZPKBXtVEeIWtc4J2XVrHuNoIK+0F2BUJgtsc= +github.com/oxyno-zeta/gomock-extra-matcher v1.1.0/go.mod h1:UMGTHYEmJ1dRq8LDZ7VTAYO4nqM3GD1UGC3RJEUxEz0= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= diff --git a/tests/e2e/channel_init_test.go b/tests/e2e/channel_init_test.go index 0edf55d77d..0698b9ad73 100644 --- a/tests/e2e/channel_init_test.go +++ b/tests/e2e/channel_init_test.go @@ -6,39 +6,16 @@ import ( app "github.com/cosmos/interchain-security/app/consumer" - "fmt" - - ibctypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types" - clienttmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" - - consumertypes "github.com/cosmos/interchain-security/x/ccv/consumer/types" - providerkeeper "github.com/cosmos/interchain-security/x/ccv/provider/keeper" - providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" tmtypes "github.com/tendermint/tendermint/types" - "github.com/cosmos/interchain-security/x/ccv/utils" - channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" - host "github.com/cosmos/ibc-go/v3/modules/core/24-host" - - "encoding/json" - "time" appConsumer "github.com/cosmos/interchain-security/app/consumer" - "github.com/cosmos/interchain-security/x/ccv/consumer" ccv "github.com/cosmos/interchain-security/x/ccv/types" abci "github.com/tendermint/tendermint/abci/types" - crypto "github.com/tendermint/tendermint/proto/tendermint/crypto" - sdk "github.com/cosmos/cosmos-sdk/types" - distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" - ibctesting "github.com/cosmos/ibc-go/v3/testing" - appProvider "github.com/cosmos/interchain-security/app/provider" - "github.com/cosmos/interchain-security/x/ccv/provider" - "github.com/cosmos/interchain-security/x/ccv/provider/types" ) func (suite *ConsumerKeeperTestSuite) TestConsumerGenesis() { @@ -110,324 +87,6 @@ func (suite *ConsumerKeeperTestSuite) TestConsumerGenesis() { suite.consumerChain.App.(*app.App).ConsumerKeeper.InitGenesis(suite.consumerChain.GetContext(), restartGenesis) }) } -func (suite *ConsumerTestSuite) TestOnChanOpenInit() { - var ( - channel *channeltypes.Channel - ) - - testCases := []struct { - name string - malleate func() - expPass bool - }{ - - { - "success", func() {}, true, - }, - { - "invalid: provider channel already established", func() { - suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper.SetProviderChannel(suite.ctx, "channel-2") - }, false, - }, - { - "invalid: UNORDERED channel", func() { - channel.Ordering = channeltypes.UNORDERED - }, false, - }, - { - "invalid port ID", func() { - suite.path.EndpointA.ChannelConfig.PortID = ibctesting.MockPort - }, false, - }, - { - "invalid version", func() { - channel.Version = "version" - }, false, - }, - { - "invalid counter party port ID", func() { - channel.Counterparty.PortId = ibctesting.MockPort - }, false, - }, - { - "invalid: verify provider chain failed", func() { - // setup a new path with provider client on consumer chain being different from genesis client - path := ibctesting.NewPath(suite.consumerChain, suite.providerChain) - // - channel config - path.EndpointA.ChannelConfig.PortID = ccv.ConsumerPortID - path.EndpointB.ChannelConfig.PortID = ccv.ProviderPortID - path.EndpointA.ChannelConfig.Version = ccv.Version - path.EndpointB.ChannelConfig.Version = ccv.Version - path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED - path.EndpointB.ChannelConfig.Order = channeltypes.ORDERED - - // create consumer client on provider chain, and provider client on consumer chain - providerUnbondingPeriod := suite.providerChain.App.(*appProvider.App).GetStakingKeeper().UnbondingTime(suite.providerChain.GetContext()) - consumerUnbondingPeriod := utils.ComputeConsumerUnbondingPeriod(providerUnbondingPeriod) - err := suite.createCustomClient(path.EndpointB, consumerUnbondingPeriod) - suite.Require().NoError(err) - err = suite.createCustomClient(path.EndpointA, providerUnbondingPeriod) - suite.Require().NoError(err) - - suite.coordinator.CreateConnections(path) - suite.path = path - channel.ConnectionHops = []string{suite.path.EndpointA.ConnectionID} - }, false, - }, - } - - for _, tc := range testCases { - tc := tc - - suite.Run(tc.name, func() { - suite.SetupTest() // reset - - suite.path.EndpointA.ChannelID = ibctesting.FirstChannelID - - counterparty := channeltypes.NewCounterparty(suite.path.EndpointB.ChannelConfig.PortID, "") - channel = &channeltypes.Channel{ - State: channeltypes.INIT, - Ordering: channeltypes.ORDERED, - Counterparty: counterparty, - ConnectionHops: []string{suite.path.EndpointA.ConnectionID}, - Version: ccv.Version, - } - - consumerModule := consumer.NewAppModule(suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper) - chanCap, err := suite.consumerChain.App.GetScopedIBCKeeper().NewCapability( - suite.ctx, - host.ChannelCapabilityPath( - ccv.ConsumerPortID, - suite.path.EndpointA.ChannelID, - ), - ) - suite.Require().NoError(err) - - tc.malleate() // explicitly change fields in channel and testChannel - - err = consumerModule.OnChanOpenInit( - suite.ctx, - channel.Ordering, - channel.GetConnectionHops(), - suite.path.EndpointA.ChannelConfig.PortID, - suite.path.EndpointA.ChannelID, - chanCap, - channel.Counterparty, - channel.GetVersion(), - ) - - if tc.expPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - - }) - } -} - -func (suite *ConsumerTestSuite) TestOnChanOpenTry() { - // OnOpenTry must error even with correct arguments - consumerModule := consumer.NewAppModule(suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper) - _, err := consumerModule.OnChanOpenTry( - suite.ctx, - channeltypes.ORDERED, - []string{"connection-1"}, - ccv.ConsumerPortID, - "channel-1", - nil, - channeltypes.NewCounterparty(ccv.ProviderPortID, "channel-1"), - ccv.Version, - ) - suite.Require().Error(err, "OnChanOpenTry callback must error on consumer chain") -} - -// TestOnChanOpenAck tests the consumer module's OnChanOpenAck implementation against the spec: -// https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-ccf-coack1 -func (suite *ConsumerTestSuite) TestOnChanOpenAck() { - - var ( - portID string - channelID string - metadataBz []byte - metadata providertypes.HandshakeMetadata - err error - ) - testCases := []struct { - name string - malleate func() - expPass bool - }{ - { - "success", func() {}, true, - }, - { - "invalid: provider channel already established", - func() { - suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper.SetProviderChannel(suite.ctx, "channel-2") - }, false, - }, - { - "invalid: cannot unmarshal ack metadata ", - func() { - metadataBz = []byte{78, 89, 20} - }, false, - }, - { - "invalid: mismatched versions", - func() { - // Set counter party version to an invalid value, passed as marshaled metadata - metadata.Version = "invalidVersion" - metadataBz, err = (&metadata).Marshal() - suite.Require().NoError(err) - }, false, - }, - // See ConsumerKeeper.GetConnectionHops as to why portID and channelID must be correct - { - "invalid: portID ", - func() { - portID = "invalidPort" - }, false, - }, - { - "invalid: channelID ", - func() { - channelID = "invalidChan" - }, false, - }, - } - - for _, tc := range testCases { - tc := tc - suite.Run(fmt.Sprintf("Case: %s", tc.name), func() { - suite.SetupTest() // reset - portID = ccv.ConsumerPortID - channelID = "channel-1" - counterChannelID := "channel-2" // per spec this is not required by onChanOpenAck() - suite.path.EndpointA.ChannelID = channelID - - // Set INIT channel on consumer chain - suite.consumerChain.App.GetIBCKeeper().ChannelKeeper.SetChannel( - suite.ctx, - ccv.ConsumerPortID, - channelID, - channeltypes.NewChannel( - channeltypes.INIT, - channeltypes.ORDERED, - channeltypes.NewCounterparty(ccv.ProviderPortID, ""), - []string{suite.path.EndpointA.ConnectionID}, - suite.path.EndpointA.ChannelConfig.Version, - ), - ) - - consumerModule := consumer.NewAppModule( - suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper) - - metadata := providertypes.HandshakeMetadata{ - ProviderFeePoolAddr: "", // dummy address used - Version: suite.path.EndpointB.ChannelConfig.Version, - } - - metadataBz, err = (&metadata).Marshal() - suite.Require().NoError(err) - - tc.malleate() // Explicitly change fields already defined - - err = consumerModule.OnChanOpenAck( - suite.ctx, - portID, - channelID, - counterChannelID, - string(metadataBz), - ) - - if tc.expPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} - -func (suite *ConsumerTestSuite) TestOnChanOpenConfirm() { - consumerModule := consumer.NewAppModule(suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper) - err := consumerModule.OnChanOpenConfirm(suite.ctx, ccv.ConsumerPortID, "channel-1") - suite.Require().Error(err, "OnChanOpenConfirm callback must error on consumer chain") -} - -func (suite *ConsumerTestSuite) TestOnChanCloseInit() { - channelID := "channel-1" - testCases := []struct { - name string - setup func(suite *ConsumerTestSuite) - expError bool - }{ - { - name: "can close duplicate in-progress channel once provider channel is established", - setup: func(suite *ConsumerTestSuite) { - // Set INIT channel on consumer chain - suite.consumerChain.App.GetIBCKeeper().ChannelKeeper.SetChannel(suite.ctx, ccv.ConsumerPortID, channelID, - channeltypes.NewChannel( - channeltypes.INIT, channeltypes.ORDERED, channeltypes.NewCounterparty(ccv.ProviderPortID, ""), - []string{suite.path.EndpointA.ConnectionID}, suite.path.EndpointA.ChannelConfig.Version), - ) - suite.path.EndpointA.ChannelID = channelID - suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper.SetProviderChannel(suite.ctx, "different-channel") - }, - expError: false, - }, - { - name: "can close duplicate open channel once provider channel is established", - setup: func(suite *ConsumerTestSuite) { - // create open channel - suite.coordinator.CreateChannels(suite.path) - suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper.SetProviderChannel(suite.ctx, "different-channel") - }, - expError: false, - }, - { - name: "cannot close in-progress channel, no established channel yet", - setup: func(suite *ConsumerTestSuite) { - // Set INIT channel on consumer chain - suite.consumerChain.App.GetIBCKeeper().ChannelKeeper.SetChannel(suite.ctx, ccv.ConsumerPortID, channelID, - channeltypes.NewChannel( - channeltypes.INIT, channeltypes.ORDERED, channeltypes.NewCounterparty(ccv.ProviderPortID, ""), - []string{suite.path.EndpointA.ConnectionID}, suite.path.EndpointA.ChannelConfig.Version), - ) - suite.path.EndpointA.ChannelID = channelID - }, - expError: true, - }, - { - name: "cannot close provider channel", - setup: func(suite *ConsumerTestSuite) { - // create open channel - suite.coordinator.CreateChannels(suite.path) - suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper.SetProviderChannel(suite.ctx, suite.path.EndpointA.ChannelID) - }, - expError: true, - }, - } - - for _, tc := range testCases { - tc := tc - suite.Run(fmt.Sprintf("Case: %s", tc.name), func() { - suite.SetupTest() // reset suite - tc.setup(suite) - - consumerModule := consumer.NewAppModule(suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper) - - err := consumerModule.OnChanCloseInit(suite.ctx, ccv.ConsumerPortID, suite.path.EndpointA.ChannelID) - - if tc.expError { - suite.Require().Error(err) - } else { - suite.Require().NoError(err) - } - }) - } -} // TestProviderClientMatches tests that the provider client managed by the consumer keeper matches the client keeper's client state func (suite *ConsumerKeeperTestSuite) TestProviderClientMatches() { @@ -437,419 +96,3 @@ func (suite *ConsumerKeeperTestSuite) TestProviderClientMatches() { clientState, _ := suite.consumerChain.App.GetIBCKeeper().ClientKeeper.GetClientState(suite.ctx, providerClientID) suite.Require().Equal(suite.providerClient, clientState, "stored client state does not match genesis provider client") } - -// TestVerifyProviderChain tests the VerifyProviderChain method for the consumer keeper -func (suite *ConsumerKeeperTestSuite) TestVerifyProviderChain() { - var connectionHops []string - channelID := "channel-0" - testCases := []struct { - name string - setup func(suite *ConsumerKeeperTestSuite) - connectionHops []string - expError bool - }{ - { - name: "success", - setup: func(suite *ConsumerKeeperTestSuite) { - // create consumer client on provider chain - providerUnbondingPeriod := suite.providerChain.App.(*appProvider.App).GetStakingKeeper().UnbondingTime(suite.providerChain.GetContext()) - consumerUnbondingPeriod := utils.ComputeConsumerUnbondingPeriod(providerUnbondingPeriod) - suite.CreateCustomClient(suite.path.EndpointB, consumerUnbondingPeriod) - err := suite.path.EndpointB.CreateClient() - suite.Require().NoError(err) - - suite.coordinator.CreateConnections(suite.path) - - // set connection hops to be connection hop from path endpoint - connectionHops = []string{suite.path.EndpointA.ConnectionID} - }, - connectionHops: []string{suite.path.EndpointA.ConnectionID}, - expError: false, - }, - { - name: "connection hops is not length 1", - setup: func(suite *ConsumerKeeperTestSuite) { - // create consumer client on provider chain - providerUnbondingPeriod := suite.providerChain.App.(*appProvider.App).GetStakingKeeper().UnbondingTime(suite.providerChain.GetContext()) - consumerUnbondingPeriod := utils.ComputeConsumerUnbondingPeriod(providerUnbondingPeriod) - suite.CreateCustomClient(suite.path.EndpointB, consumerUnbondingPeriod) - - suite.coordinator.CreateConnections(suite.path) - - // set connection hops to be connection hop from path endpoint - connectionHops = []string{suite.path.EndpointA.ConnectionID, "connection-2"} - }, - expError: true, - }, - { - name: "connection does not exist", - setup: func(suite *ConsumerKeeperTestSuite) { - // set connection hops to be connection hop from path endpoint - connectionHops = []string{"connection-dne"} - }, - expError: true, - }, - { - name: "clientID does not match", - setup: func(suite *ConsumerKeeperTestSuite) { - // create consumer client on provider chain - providerUnbondingPeriod := suite.providerChain.App.(*appProvider.App).GetStakingKeeper().UnbondingTime(suite.providerChain.GetContext()) - consumerUnbondingPeriod := utils.ComputeConsumerUnbondingPeriod(providerUnbondingPeriod) - suite.CreateCustomClient(suite.path.EndpointB, consumerUnbondingPeriod) - - // create a new provider client on consumer chain that is different from the one in genesis - suite.CreateCustomClient(suite.path.EndpointA, providerUnbondingPeriod) - - suite.coordinator.CreateConnections(suite.path) - - // set connection hops to be connection hop from path endpoint - connectionHops = []string{suite.path.EndpointA.ConnectionID} - }, - expError: true, - }, - } - - for _, tc := range testCases { - tc := tc - suite.Run(fmt.Sprintf("Case: %s", tc.name), func() { - suite.SetupTest() // reset suite - - tc.setup(suite) - - // Verify ProviderChain on consumer chain using path returned by setup - err := suite.consumerChain.App.(*appConsumer.App).ConsumerKeeper.VerifyProviderChain(suite.ctx, channelID, connectionHops) - - if tc.expError { - suite.Require().Error(err, "invalid case did not return error") - } else { - suite.Require().NoError(err, "valid case returned error") - } - }) - } -} - -// TestOnChanOpenTry validates the provider's OnChanOpenTry implementation against the spec: -// https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-cotry1 -func (suite *ProviderTestSuite) TestOnChanOpenTry() { - var ( - channel *channeltypes.Channel - counterpartyVersion string - providerKeeper *providerkeeper.Keeper - ) - - testCases := []struct { - name string - malleate func() - expPass bool - }{ - { - "success", func() {}, true, - }, - { - "invalid order", func() { - channel.Ordering = channeltypes.UNORDERED - }, false, - }, - { - "invalid port ID", func() { - suite.path.EndpointA.ChannelConfig.PortID = ibctesting.MockPort - }, false, - }, - { - "invalid counter party port ID", func() { - channel.Counterparty.PortId = ibctesting.MockPort - }, false, - }, - { - "invalid counter party version", func() { - counterpartyVersion = "invalidVersion" - }, false, - }, - { - "unexpected client ID mapped to chain ID", func() { - providerKeeper.SetConsumerClientId( - suite.providerCtx(), - suite.path.EndpointA.Chain.ChainID, - "invalidClientID", - ) - }, false, - }, - { - "other CCV channel exists for this consumer chain", func() { - providerKeeper.SetChainToChannel( - suite.providerCtx(), - suite.path.EndpointA.Chain.ChainID, - "some existing channel ID", - ) - }, false, - }, - } - - for _, tc := range testCases { - tc := tc - - suite.Run(tc.name, func() { - suite.SetupTest() // reset - - suite.path.EndpointA.ChannelConfig.PortID = ccv.ProviderPortID - suite.path.EndpointA.ChannelID = "providerChanID" - suite.path.EndpointB.ChannelConfig.PortID = ccv.ConsumerPortID - suite.path.EndpointB.ChannelID = "consumerChanID" - suite.path.EndpointA.ConnectionID = "ConnID" - suite.path.EndpointA.ClientID = "ClientID" - suite.path.EndpointA.Chain.ChainID = "ChainID" - - counterparty := channeltypes.NewCounterparty( - suite.path.EndpointB.ChannelConfig.PortID, - suite.path.EndpointA.ChannelID, - ) - counterpartyVersion = ccv.Version - - channel = &channeltypes.Channel{ - State: channeltypes.INIT, - Ordering: channeltypes.ORDERED, - Counterparty: counterparty, - ConnectionHops: []string{suite.path.EndpointA.ConnectionID}, - Version: counterpartyVersion, - } - - providerKeeper = &suite.providerChain.App.(*appProvider.App).ProviderKeeper - providerModule := provider.NewAppModule(providerKeeper) - chanCap, err := suite.providerChain.App.GetScopedIBCKeeper().NewCapability( - suite.providerCtx(), - host.ChannelCapabilityPath( - suite.path.EndpointA.ChannelConfig.PortID, - suite.path.EndpointA.ChannelID, - ), - ) - suite.Require().NoError(err) - - // Manual keeper setup - connKeeper := suite.providerChain.App.GetIBCKeeper().ConnectionKeeper - connKeeper.SetConnection( - suite.providerCtx(), - suite.path.EndpointA.ConnectionID, - ibctypes.ConnectionEnd{ - ClientId: suite.path.EndpointA.ClientID, - }, - ) - clientKeeper := suite.providerChain.App.GetIBCKeeper().ClientKeeper - clientKeeper.SetClientState( - suite.providerCtx(), - suite.path.EndpointA.ClientID, - &clienttmtypes.ClientState{ - ChainId: suite.path.EndpointA.Chain.ChainID, - }, - ) - providerKeeper.SetConsumerClientId( - suite.providerCtx(), - suite.path.EndpointA.Chain.ChainID, - suite.path.EndpointA.ClientID, - ) - - tc.malleate() // explicitly change fields - - metadata, err := providerModule.OnChanOpenTry( - suite.providerCtx(), - channel.Ordering, - channel.GetConnectionHops(), - suite.path.EndpointA.ChannelConfig.PortID, - suite.path.EndpointA.ChannelID, - chanCap, - channel.Counterparty, - counterpartyVersion, - ) - - if tc.expPass { - suite.Require().NoError(err) - md := &providertypes.HandshakeMetadata{} - err = md.Unmarshal([]byte(metadata)) - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} - -func (suite *ProviderTestSuite) TestOnChanOpenInit() { - // OnChanOpenInit must error for provider even with correct arguments - providerModule := provider.NewAppModule(&suite.providerChain.App.(*appProvider.App).ProviderKeeper) - - err := providerModule.OnChanOpenInit( - suite.providerCtx(), - channeltypes.ORDERED, - []string{"connection-1"}, - ccv.ProviderPortID, - "channel-1", - nil, - channeltypes.NewCounterparty(ccv.ConsumerPortID, "channel-1"), - ccv.Version, - ) - suite.Require().Error(err, "OnChanOpenInit must error on provider chain") -} - -// TestConsumerChainProposalHandler tests the highest level handler -// for both ConsumerAdditionProposals and ConsumerRemovalProposals -// -// TODO: Determine if it's possible to make this a unit test -func (suite *ProviderTestSuite) TestConsumerChainProposalHandler() { - var ( - ctx sdk.Context - content govtypes.Content - err error - ) - - testCases := []struct { - name string - malleate func(*ProviderTestSuite) - expPass bool - }{ - { - "valid consumer addition proposal", func(suite *ProviderTestSuite) { - initialHeight := clienttypes.NewHeight(2, 3) - // ctx blocktime is after proposal's spawn time - ctx = suite.providerChain.GetContext().WithBlockTime(time.Now().Add(time.Hour)) - content = types.NewConsumerAdditionProposal("title", "description", "chainID", initialHeight, []byte("gen_hash"), []byte("bin_hash"), time.Now()) - }, true, - }, - { - "valid consumer removal proposal", func(suite *ProviderTestSuite) { - ctx = suite.providerChain.GetContext().WithBlockTime(time.Now().Add(time.Hour)) - content = types.NewConsumerRemovalProposal("title", "description", "chainID", time.Now()) - }, true, - }, - { - "nil proposal", func(suite *ProviderTestSuite) { - ctx = suite.providerChain.GetContext() - content = nil - }, false, - }, - { - "unsupported proposal type", func(suite *ProviderTestSuite) { - ctx = suite.providerChain.GetContext() - content = distributiontypes.NewCommunityPoolSpendProposal(ibctesting.Title, ibctesting.Description, suite.providerChain.SenderAccount.GetAddress(), sdk.NewCoins(sdk.NewCoin("communityfunds", sdk.NewInt(10)))) - }, false, - }, - } - - for _, tc := range testCases { - tc := tc - - suite.Run(tc.name, func() { - suite.SetupTest() // reset - - tc.malleate(suite) - - proposalHandler := provider.NewConsumerChainProposalHandler(suite.providerChain.App.(*appProvider.App).ProviderKeeper) - - err = proposalHandler(ctx, content) - - if tc.expPass { - suite.Require().NoError(err) - } else { - suite.Require().Error(err) - } - }) - } -} - -func (suite *ProviderKeeperTestSuite) TestMakeConsumerGenesis() { - suite.SetupTest() - - actualGenesis, err := suite.providerChain.App.(*appProvider.App).ProviderKeeper.MakeConsumerGenesis(suite.providerChain.GetContext()) - suite.Require().NoError(err) - - jsonString := `{"params":{"enabled":true, "blocks_per_distribution_transmission":1000, "lock_unbonding_on_timeout": false},"new_chain":true,"provider_client_state":{"chain_id":"testchain1","trust_level":{"numerator":1,"denominator":3},"trusting_period":907200000000000,"unbonding_period":1814400000000000,"max_clock_drift":10000000000,"frozen_height":{},"latest_height":{"revision_height":5},"proof_specs":[{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":33,"min_prefix_length":4,"max_prefix_length":12,"hash":1}},{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":32,"min_prefix_length":1,"max_prefix_length":1,"hash":1}}],"upgrade_path":["upgrade","upgradedIBCState"],"allow_update_after_expiry":true,"allow_update_after_misbehaviour":true},"provider_consensus_state":{"timestamp":"2020-01-02T00:00:10Z","root":{"hash":"LpGpeyQVLUo9HpdsgJr12NP2eCICspcULiWa5u9udOA="},"next_validators_hash":"E30CE736441FB9101FADDAF7E578ABBE6DFDB67207112350A9A904D554E1F5BE"},"unbonding_sequences":null,"initial_val_set":[{"pub_key":{"type":"tendermint/PubKeyEd25519","value":"dcASx5/LIKZqagJWN0frOlFtcvz91frYmj/zmoZRWro="},"power":1}]}` - - var expectedGenesis consumertypes.GenesisState - err = json.Unmarshal([]byte(jsonString), &expectedGenesis) - suite.Require().NoError(err) - - // Zero out differing fields- TODO: figure out how to get the test suite to - // keep these deterministic - actualGenesis.ProviderConsensusState.NextValidatorsHash = []byte{} - expectedGenesis.ProviderConsensusState.NextValidatorsHash = []byte{} - - // set valset to one empty validator because SetupTest() creates 4 validators per chain - actualGenesis.InitialValSet = []abci.ValidatorUpdate{{PubKey: crypto.PublicKey{}, Power: actualGenesis.InitialValSet[0].Power}} - expectedGenesis.InitialValSet[0].PubKey = crypto.PublicKey{} - - actualGenesis.ProviderConsensusState.Root.Hash = []byte{} - expectedGenesis.ProviderConsensusState.Root.Hash = []byte{} - - suite.Require().Equal(actualGenesis, expectedGenesis, "consumer chain genesis created incorrectly") -} - -func (suite *ProviderKeeperTestSuite) TestHandleConsumerAdditionProposal() { - var ( - ctx sdk.Context - proposal *types.ConsumerAdditionProposal - ok bool - ) - - chainID := "chainID" - initialHeight := clienttypes.NewHeight(2, 3) - lockUbdOnTimeout := false - - testCases := []struct { - name string - malleate func(*ProviderKeeperTestSuite) - expPass bool - spawnReached bool - }{ - { - "valid consumer addition proposal: spawn time reached", func(suite *ProviderKeeperTestSuite) { - // ctx blocktime is after proposal's spawn time - ctx = suite.providerChain.GetContext().WithBlockTime(time.Now().Add(time.Hour)) - content := types.NewConsumerAdditionProposal("title", "description", chainID, initialHeight, []byte("gen_hash"), []byte("bin_hash"), time.Now()) - proposal, ok = content.(*types.ConsumerAdditionProposal) - suite.Require().True(ok) - proposal.LockUnbondingOnTimeout = lockUbdOnTimeout - }, true, true, - }, - { - "valid proposal: spawn time has not yet been reached", func(suite *ProviderKeeperTestSuite) { - // ctx blocktime is before proposal's spawn time - ctx = suite.providerChain.GetContext().WithBlockTime(time.Now()) - content := types.NewConsumerAdditionProposal("title", "description", chainID, initialHeight, []byte("gen_hash"), []byte("bin_hash"), time.Now().Add(time.Hour)) - proposal, ok = content.(*types.ConsumerAdditionProposal) - suite.Require().True(ok) - proposal.LockUnbondingOnTimeout = lockUbdOnTimeout - }, true, false, - }, - } - - for _, tc := range testCases { - tc := tc - - suite.Run(tc.name, func() { - suite.SetupTest() - - tc.malleate(suite) - - err := suite.providerChain.App.(*appProvider.App).ProviderKeeper.HandleConsumerAdditionProposal(ctx, proposal) - if tc.expPass { - suite.Require().NoError(err, "error returned on valid case") - if tc.spawnReached { - clientId, found := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetConsumerClientId(ctx, chainID) - suite.Require().True(found, "consumer client not found") - consumerGenesis, ok := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetConsumerGenesis(ctx, chainID) - suite.Require().True(ok) - - expectedGenesis, err := suite.providerChain.App.(*appProvider.App).ProviderKeeper.MakeConsumerGenesis(ctx) - suite.Require().NoError(err) - - suite.Require().Equal(expectedGenesis, consumerGenesis) - suite.Require().NotEqual("", clientId, "consumer client was not created after spawn time reached") - } else { - gotProposal := suite.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingConsumerAdditionProp(ctx, proposal.SpawnTime, chainID) - suite.Require().Equal(initialHeight, gotProposal.InitialHeight, "unexpected pending proposal (InitialHeight)") - suite.Require().Equal(lockUbdOnTimeout, gotProposal.LockUnbondingOnTimeout, "unexpected pending proposal (LockUnbondingOnTimeout)") - } - } else { - suite.Require().Error(err, "did not return error on invalid case") - } - }) - } -} diff --git a/tests/e2e/common_test.go b/tests/e2e/common_test.go index 97a0c8fdd0..0532a89cef 100644 --- a/tests/e2e/common_test.go +++ b/tests/e2e/common_test.go @@ -386,42 +386,3 @@ func (suite *ConsumerKeeperTestSuite) CreateCustomClient(endpoint *ibctesting.En endpoint.ClientID, err = ibctesting.ParseClientIDFromEvents(res.GetEvents()) require.NoError(endpoint.Chain.T, err) } - -// createCustomClient creates an IBC client on the endpoint -// using the given unbonding period. -// It will update the clientID for the endpoint if the message -// is successfully executed. -func (suite *ConsumerTestSuite) createCustomClient(endpoint *ibctesting.Endpoint, unbondingPeriod time.Duration) (err error) { - // ensure counterparty has committed state - endpoint.Chain.Coordinator.CommitBlock(endpoint.Counterparty.Chain) - - suite.Require().Equal(exported.Tendermint, endpoint.ClientConfig.GetClientType(), "only Tendermint client supported") - - tmConfig, ok := endpoint.ClientConfig.(*ibctesting.TendermintConfig) - require.True(endpoint.Chain.T, ok) - tmConfig.UnbondingPeriod = unbondingPeriod - tmConfig.TrustingPeriod = unbondingPeriod / utils.TrustingPeriodFraction - - height := endpoint.Counterparty.Chain.LastHeader.GetHeight().(clienttypes.Height) - UpgradePath := []string{"upgrade", "upgradedIBCState"} - clientState := ibctmtypes.NewClientState( - endpoint.Counterparty.Chain.ChainID, tmConfig.TrustLevel, tmConfig.TrustingPeriod, tmConfig.UnbondingPeriod, tmConfig.MaxClockDrift, - height, commitmenttypes.GetSDKSpecs(), UpgradePath, tmConfig.AllowUpdateAfterExpiry, tmConfig.AllowUpdateAfterMisbehaviour, - ) - consensusState := endpoint.Counterparty.Chain.LastHeader.ConsensusState() - - msg, err := clienttypes.NewMsgCreateClient( - clientState, consensusState, endpoint.Chain.SenderAccount.GetAddress().String(), - ) - require.NoError(endpoint.Chain.T, err) - - res, err := endpoint.Chain.SendMsgs(msg) - if err != nil { - return err - } - - endpoint.ClientID, err = ibctesting.ParseClientIDFromEvents(res.GetEvents()) - require.NoError(endpoint.Chain.T, err) - - return nil -} diff --git a/tests/e2e/stop_consumer_test.go b/tests/e2e/stop_consumer_test.go index b2c4452544..4ae150367c 100644 --- a/tests/e2e/stop_consumer_test.go +++ b/tests/e2e/stop_consumer_test.go @@ -1,19 +1,17 @@ package e2e_test import ( - "time" - sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" appConsumer "github.com/cosmos/interchain-security/app/consumer" appProvider "github.com/cosmos/interchain-security/app/provider" - providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" ccv "github.com/cosmos/interchain-security/x/ccv/types" abci "github.com/tendermint/tendermint/abci/types" ) +// Tests the functionality of stopping a consumer chain at a higher level than unit tests func (s *ProviderTestSuite) TestStopConsumerChain() { // default consumer chain ID @@ -97,102 +95,6 @@ func (s *ProviderTestSuite) TestStopConsumerChain() { s.checkConsumerChainIsRemoved(consumerChainID, false) } -func (s *ProviderTestSuite) TestConsumerRemovalProposal() { - var ( - ctx sdk.Context - proposal *providertypes.ConsumerRemovalProposal - ok bool - ) - - chainID := s.consumerChain.ChainID - - testCases := []struct { - name string - malleate func(*ProviderTestSuite) - expPass bool - stopReached bool - }{ - { - "valid consumer removal proposal: stop time reached", func(suite *ProviderTestSuite) { - - // ctx blocktime is after proposal's stop time - ctx = s.providerCtx().WithBlockTime(time.Now().Add(time.Hour)) - content := providertypes.NewConsumerRemovalProposal("title", "description", chainID, time.Now()) - proposal, ok = content.(*providertypes.ConsumerRemovalProposal) - s.Require().True(ok) - }, true, true, - }, - { - "valid proposal: stop time has not yet been reached", func(suite *ProviderTestSuite) { - - // ctx blocktime is before proposal's stop time - ctx = s.providerCtx().WithBlockTime(time.Now()) - content := providertypes.NewConsumerRemovalProposal("title", "description", chainID, time.Now().Add(time.Hour)) - proposal, ok = content.(*providertypes.ConsumerRemovalProposal) - s.Require().True(ok) - }, true, false, - }, - { - "valid proposal: fail due to an invalid unbonding index", func(suite *ProviderTestSuite) { - - // ctx blocktime is after proposal's stop time - ctx = s.providerCtx().WithBlockTime(time.Now().Add(time.Hour)) - - // set invalid unbonding op index - s.providerChain.App.(*appProvider.App).ProviderKeeper.SetUnbondingOpIndex(ctx, chainID, 0, []uint64{0}) - - content := providertypes.NewConsumerRemovalProposal("title", "description", chainID, time.Now()) - proposal, ok = content.(*providertypes.ConsumerRemovalProposal) - s.Require().True(ok) - }, false, true, - }, - } - - for _, tc := range testCases { - tc := tc - - s.Run(tc.name, func() { - s.SetupTest() - s.SetupCCVChannel() - - tc.malleate(s) - - err := s.providerChain.App.(*appProvider.App).ProviderKeeper.HandleConsumerRemovalProposal(ctx, proposal) - if tc.expPass { - s.Require().NoError(err, "error returned on valid case") - if tc.stopReached { - // check that the pending consumer removal proposal is deleted - found := s.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingConsumerRemovalProp(ctx, chainID, proposal.StopTime) - s.Require().False(found, "pending consumer removal proposal wasn't deleted") - - // check that the consumer chain is removed - s.checkConsumerChainIsRemoved(chainID, false) - - } else { - found := s.providerChain.App.(*appProvider.App).ProviderKeeper.GetPendingConsumerRemovalProp(ctx, chainID, proposal.StopTime) - s.Require().True(found, "pending stop consumer was not found for chain ID %s", chainID) - - // check that the consumer chain client exists - _, found = s.providerChain.App.(*appProvider.App).ProviderKeeper.GetConsumerClientId(s.providerCtx(), chainID) - s.Require().True(found) - - // check that the chainToChannel and channelToChain exist for the consumer chain ID - _, found = s.providerChain.App.(*appProvider.App).ProviderKeeper.GetChainToChannel(s.providerCtx(), chainID) - s.Require().True(found) - - _, found = s.providerChain.App.(*appProvider.App).ProviderKeeper.GetChannelToChain(s.providerCtx(), s.path.EndpointB.ChannelID) - s.Require().True(found) - - // check that channel is in OPEN state - s.Require().Equal(channeltypes.OPEN, s.path.EndpointB.GetChannel().State) - } - } else { - s.Require().Error(err, "did not return error on invalid case") - } - }) - } -} - // TODO Simon: implement OnChanCloseConfirm in IBC-GO testing to close the consumer chain's channel end func (s *ProviderTestSuite) TestStopConsumerOnChannelClosed() { // init the CCV channel states diff --git a/testutil/keeper/expectations.go b/testutil/keeper/expectations.go new file mode 100644 index 0000000000..7390115c7a --- /dev/null +++ b/testutil/keeper/expectations.go @@ -0,0 +1,93 @@ +package keeper + +import ( + time "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + conntypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" + ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + "github.com/golang/mock/gomock" + + ccv "github.com/cosmos/interchain-security/x/ccv/types" + + extra "github.com/oxyno-zeta/gomock-extra-matcher" +) + +// +// A file containing groups of commonly used mock expectations. +// Note: Each group of mock expectations is associated with a single method +// that may be called during unit tests. +// + +// GetMocksForCreateConsumerClient returns mock expectations needed to call CreateConsumerClient(). +func GetMocksForCreateConsumerClient(ctx sdk.Context, mocks *MockedKeepers, + expectedChainID string, expectedLatestHeight clienttypes.Height) []*gomock.Call { + + expectations := []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT().UnbondingTime(ctx).Return(time.Hour).Times( + 1, // called once in CreateConsumerClient + ), + + mocks.MockClientKeeper.EXPECT().CreateClient( + ctx, + // Allows us to expect a match by field. These are the only two client state values + // that are dependant on parameters passed to CreateConsumerClient. + extra.StructMatcher().Field( + "ChainId", expectedChainID).Field( + "LatestHeight", expectedLatestHeight, + ), + gomock.Any(), + ).Return("clientID", nil).Times(1), + } + + expectations = append(expectations, GetMocksForMakeConsumerGenesis(ctx, mocks, time.Hour)...) + return expectations +} + +// GetMocksForMakeConsumerGenesis returns mock expectations needed to call MakeConsumerGenesis(). +func GetMocksForMakeConsumerGenesis(ctx sdk.Context, mocks *MockedKeepers, + unbondingTimeToInject time.Duration) []*gomock.Call { + return []*gomock.Call{ + mocks.MockStakingKeeper.EXPECT().UnbondingTime(ctx).Return(unbondingTimeToInject).Times(1), + + mocks.MockClientKeeper.EXPECT().GetSelfConsensusState(ctx, + clienttypes.GetSelfHeight(ctx)).Return(&ibctmtypes.ConsensusState{}, nil).Times(1), + + mocks.MockStakingKeeper.EXPECT().IterateLastValidatorPowers(ctx, gomock.Any()).Times(1), + } +} + +// GetMocksForSetConsumerChain returns mock expectations needed to call SetConsumerChain(). +func GetMocksForSetConsumerChain(ctx sdk.Context, mocks *MockedKeepers, + chainIDToInject string) []*gomock.Call { + return []*gomock.Call{ + mocks.MockChannelKeeper.EXPECT().GetChannel(ctx, ccv.ProviderPortID, gomock.Any()).Return( + channeltypes.Channel{ + State: channeltypes.OPEN, + ConnectionHops: []string{"connectionID"}, + }, + true, + ).Times(1), + mocks.MockConnectionKeeper.EXPECT().GetConnection(ctx, "connectionID").Return( + conntypes.ConnectionEnd{ClientId: "clientID"}, true, + ).Times(1), + mocks.MockClientKeeper.EXPECT().GetClientState(ctx, "clientID").Return( + &ibctmtypes.ClientState{ChainId: chainIDToInject}, true, + ).Times(1), + } +} + +// GetMocksForStopConsumerChain returns mock expectations needed to call StopConsumerChain(). +func GetMocksForStopConsumerChain(ctx sdk.Context, mocks *MockedKeepers) []*gomock.Call { + dummyCap := &capabilitytypes.Capability{} + return []*gomock.Call{ + mocks.MockChannelKeeper.EXPECT().GetChannel(ctx, ccv.ProviderPortID, "channelID").Return( + channeltypes.Channel{State: channeltypes.OPEN}, true, + ).Times(1), + mocks.MockScopedKeeper.EXPECT().GetCapability(ctx, gomock.Any()).Return(dummyCap, true).Times(1), + mocks.MockChannelKeeper.EXPECT().ChanCloseInit(ctx, ccv.ProviderPortID, "channelID", dummyCap).Times(1), + } +} diff --git a/testutil/keeper/unit_test_helpers.go b/testutil/keeper/unit_test_helpers.go index 4bb221ce28..fef396eb72 100644 --- a/testutil/keeper/unit_test_helpers.go +++ b/testutil/keeper/unit_test_helpers.go @@ -140,51 +140,56 @@ func NewInMemConsumerKeeper(params InMemKeeperParams, mocks MockedKeepers) consu ) } -// The minimum boilerplate way to obtain an in-memory provider keeper, context, and controller. +// Returns an in-memory provider keeper, context, controller, and mocks, given a test instance and parameters. // // Note: Calling ctrl.Finish() at the end of a test function ensures that // no unexpected calls to external keepers are made. -func GetProviderKeeperAndCtx(t *testing.T) (providerkeeper.Keeper, sdk.Context, *gomock.Controller) { - params := NewInMemKeeperParams(t) +func GetProviderKeeperAndCtx(t *testing.T, params InMemKeeperParams) ( + providerkeeper.Keeper, sdk.Context, *gomock.Controller, MockedKeepers) { + ctrl := gomock.NewController(t) mocks := NewMockedKeepers(ctrl) - return NewInMemProviderKeeper(params, mocks), params.Ctx, ctrl + return NewInMemProviderKeeper(params, mocks), params.Ctx, ctrl, mocks } -// The minimum boilerplate way to obtain an-in memory consumer keeper, context, and controller. +// Return an in-memory consumer keeper, context, controller, and mocks, given a test instance and parameters. // // Note: Calling ctrl.Finish() at the end of a test function ensures that // no unexpected calls to external keepers are made. -func GetConsumerKeeperAndCtx(t *testing.T) (consumerkeeper.Keeper, sdk.Context, *gomock.Controller) { - params := NewInMemKeeperParams(t) +func GetConsumerKeeperAndCtx(t *testing.T, params InMemKeeperParams) ( + consumerkeeper.Keeper, sdk.Context, *gomock.Controller, MockedKeepers) { + ctrl := gomock.NewController(t) mocks := NewMockedKeepers(ctrl) - return NewInMemConsumerKeeper(params, mocks), params.Ctx, ctrl + return NewInMemConsumerKeeper(params, mocks), params.Ctx, ctrl, mocks } // Sets a template client state for a params subspace so that the provider's // GetTemplateClient method will be satisfied. -func SetTemplateClientState(ctx sdk.Context, subspace *paramstypes.Subspace) { +func (params *InMemKeeperParams) SetTemplateClientState(customState *ibctmtypes.ClientState) { keyTable := paramstypes.NewKeyTable(paramstypes.NewParamSetPair( providertypes.KeyTemplateClient, &ibctmtypes.ClientState{}, func(value interface{}) error { return nil })) - *subspace = subspace.WithKeyTable(keyTable) + newSubspace := params.ParamsSubspace.WithKeyTable(keyTable) + params.ParamsSubspace = &newSubspace - templateClientState := - ibctmtypes.NewClientState("", ibctmtypes.DefaultTrustLevel, 0, 0, + // Default template client state if none provided + if customState == nil { + customState = ibctmtypes.NewClientState("", ibctmtypes.DefaultTrustLevel, 0, 0, time.Second*10, clienttypes.Height{}, commitmenttypes.GetSDKSpecs(), []string{"upgrade", "upgradedIBCState"}, true, true) + } - subspace.Set(ctx, providertypes.KeyTemplateClient, templateClientState) + params.ParamsSubspace.Set(params.Ctx, providertypes.KeyTemplateClient, customState) } // Registers proto interfaces for params.Cdc // // For now, we explicitly force certain unit tests to register sdk crypto interfaces. // TODO: This function will be executed automatically once https://github.com/cosmos/interchain-security/issues/273 is solved. -func RegisterSdkCryptoCodecInterfaces(params *InMemKeeperParams) { +func (params *InMemKeeperParams) RegisterSdkCryptoCodecInterfaces() { ir := codectypes.NewInterfaceRegistry() // Public key implementation registered here cryptocodec.RegisterInterfaces(ir) @@ -201,3 +206,21 @@ func GenPubKey() (crypto.PubKey, error) { privKey := PrivateKey{ed25519.GenPrivKey()} return cryptocodec.ToTmPubKeyInterface(privKey.PrivKey.PubKey()) } + +// SetupForStoppingConsumerChain registers expected mock calls and corresponding state setup +// which asserts that a consumer chain was properly stopped from StopConsumerChain(). +func SetupForStoppingConsumerChain(t *testing.T, ctx sdk.Context, + providerKeeper *providerkeeper.Keeper, mocks MockedKeepers) { + + expectations := GetMocksForCreateConsumerClient(ctx, &mocks, + "chainID", clienttypes.NewHeight(2, 3)) + expectations = append(expectations, GetMocksForSetConsumerChain(ctx, &mocks, "chainID")...) + expectations = append(expectations, GetMocksForStopConsumerChain(ctx, &mocks)...) + + gomock.InOrder(expectations...) + + err := providerKeeper.CreateConsumerClient(ctx, "chainID", clienttypes.NewHeight(2, 3), false) + require.NoError(t, err) + err = providerKeeper.SetConsumerChain(ctx, "channelID") + require.NoError(t, err) +} diff --git a/x/ccv/consumer/ibc_module.go b/x/ccv/consumer/ibc_module.go index 77527c34f2..b16457e2fb 100644 --- a/x/ccv/consumer/ibc_module.go +++ b/x/ccv/consumer/ibc_module.go @@ -55,7 +55,7 @@ func (am AppModule) OnChanOpenInit( return err } - return am.keeper.VerifyProviderChain(ctx, channelID, connectionHops) + return am.keeper.VerifyProviderChain(ctx, connectionHops) } // validateCCVChannelParams validates a ccv channel diff --git a/x/ccv/consumer/ibc_module_test.go b/x/ccv/consumer/ibc_module_test.go new file mode 100644 index 0000000000..67eed2568c --- /dev/null +++ b/x/ccv/consumer/ibc_module_test.go @@ -0,0 +1,374 @@ +package consumer_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" + conntypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v3/modules/core/24-host" + testkeeper "github.com/cosmos/interchain-security/testutil/keeper" + "github.com/cosmos/interchain-security/x/ccv/consumer" + consumerkeeper "github.com/cosmos/interchain-security/x/ccv/consumer/keeper" + providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" + ccv "github.com/cosmos/interchain-security/x/ccv/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +// TestOnChanOpenInit validates the consumer's OnChanOpenInit implementation against the spec. +// Additional validation for VerifyProviderChain can be found in it's unit test. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-ccf-coinit1 +// Spec tag: [CCV-CCF-COINIT.1] +func TestOnChanOpenInit(t *testing.T) { + + // Params for the OnChanOpenInit method + type params struct { + ctx sdk.Context + order channeltypes.Order + connectionHops []string + portID string + channelID string + chanCap *capabilitytypes.Capability + counterparty channeltypes.Counterparty + version string + } + + testCases := []struct { + name string + // Test-case specific function that mutates method parameters and setups expected mock calls + setup func(*consumerkeeper.Keeper, *params, testkeeper.MockedKeepers) + expPass bool + }{ + { + "success", func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + gomock.InOrder( + mocks.MockScopedKeeper.EXPECT().ClaimCapability( + params.ctx, params.chanCap, host.ChannelCapabilityPath( + params.portID, params.channelID)).Return(nil).Times(1), + mocks.MockConnectionKeeper.EXPECT().GetConnection( + params.ctx, "connectionIDToProvider").Return( + conntypes.ConnectionEnd{ClientId: "clientIDToProvider"}, true).Times(1), + ) + }, true, + }, + { + "invalid: channel to provider already established", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + keeper.SetProviderChannel(params.ctx, "existingProviderChanID") + }, false, + }, + { + "invalid: UNORDERED channel", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + params.order = channeltypes.UNORDERED + }, false, + }, + { + "invalid port ID, not CCV port", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + params.portID = "someDingusPortID" + }, false, + }, + { + "invalid version", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + params.version = "someDingusVer" + }, false, + }, + { + "invalid counterparty port ID", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + params.counterparty.PortId = "someOtherDingusPortID" + }, false, + }, + { + "invalid clientID to provider", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + gomock.InOrder( + mocks.MockScopedKeeper.EXPECT().ClaimCapability( + params.ctx, params.chanCap, host.ChannelCapabilityPath( + params.portID, params.channelID)).Return(nil).Times(1), + mocks.MockConnectionKeeper.EXPECT().GetConnection( + params.ctx, "connectionIDToProvider").Return( + conntypes.ConnectionEnd{ClientId: "unexpectedClientID"}, true).Times(1), // unexpected clientID + ) + }, false, + }, + } + + for _, tc := range testCases { + + // Common setup + consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + consumerModule := consumer.NewAppModule(consumerKeeper) + + consumerKeeper.SetPort(ctx, ccv.ConsumerPortID) + consumerKeeper.SetProviderClientID(ctx, "clientIDToProvider") + + // Instantiate valid params as default. Individual test cases mutate these as needed. + params := params{ + ctx: ctx, + order: channeltypes.ORDERED, + connectionHops: []string{"connectionIDToProvider"}, + portID: ccv.ConsumerPortID, + channelID: "consumerChannelID", + chanCap: &capabilitytypes.Capability{}, + counterparty: channeltypes.NewCounterparty(ccv.ProviderPortID, "providerChannelID"), + version: ccv.Version, + } + + tc.setup(&consumerKeeper, ¶ms, mocks) + + err := consumerModule.OnChanOpenInit( + params.ctx, + params.order, + params.connectionHops, + params.portID, + params.channelID, + params.chanCap, + params.counterparty, + params.version, + ) + + if tc.expPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + // Confirm there are no unexpected external keeper calls + ctrl.Finish() + } +} + +// TestOnChanOpenTry validates the consumer's OnChanOpenTry implementation against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-ccf-cotry1 +// Spec tag: [CCV-CCF-COTRY.1] +func TestOnChanOpenTry(t *testing.T) { + + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + // No external keeper methods should be called + defer ctrl.Finish() + consumerModule := consumer.NewAppModule(consumerKeeper) + + // OnOpenTry must error even with correct arguments + _, err := consumerModule.OnChanOpenTry( + ctx, + channeltypes.ORDERED, + []string{"connection-1"}, + ccv.ConsumerPortID, + "channel-1", + nil, + channeltypes.NewCounterparty(ccv.ProviderPortID, "channel-1"), + ccv.Version, + ) + require.Error(t, err, "OnChanOpenTry callback must error on consumer chain") +} + +// TestOnChanOpenAck validates the consumer's OnChanOpenAck implementation against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-ccf-coack1 +// Spec tag: [CCV-CCF-COACK.1] +func TestOnChanOpenAck(t *testing.T) { + + // Params for the OnChanOpenAck method + type params struct { + ctx sdk.Context + portID string + channelID string + counterpartyChannelID string + counterpartyMetadata string + } + + testCases := []struct { + name string + // Test-case specific function that mutates method parameters and setups expected mock calls + setup func(*consumerkeeper.Keeper, *params, testkeeper.MockedKeepers) + expPass bool + }{ + { + "success", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + // Expected msg + distrTransferMsg := channeltypes.NewMsgChannelOpenInit( + transfertypes.PortID, + transfertypes.Version, + channeltypes.UNORDERED, + []string{"connectionID"}, + transfertypes.PortID, + "", // signer unused + ) + + // Expected mock calls + gomock.InOrder( + mocks.MockChannelKeeper.EXPECT().GetChannel( + params.ctx, params.portID, params.channelID).Return(channeltypes.Channel{ + ConnectionHops: []string{"connectionID"}, + }, true).Times(1), + mocks.MockIBCCoreKeeper.EXPECT().ChannelOpenInit( + sdk.WrapSDKContext(params.ctx), distrTransferMsg).Return( + &channeltypes.MsgChannelOpenInitResponse{}, nil, + ).Times(1), + ) + }, + true, + }, + { + "invalid: provider channel already established", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + keeper.SetProviderChannel(params.ctx, "existingProviderChannelID") + }, false, + }, + { + "invalid: cannot unmarshal ack metadata ", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + params.counterpartyMetadata = "bunkData" + }, false, + }, + { + "invalid: mismatched serialized version", + func(keeper *consumerkeeper.Keeper, params *params, mocks testkeeper.MockedKeepers) { + md := providertypes.HandshakeMetadata{ + ProviderFeePoolAddr: "", // dummy address used + Version: "bunkVersion", + } + metadataBz, err := md.Marshal() + require.NoError(t, err) + params.counterpartyMetadata = string(metadataBz) + }, false, + }, + } + + for _, tc := range testCases { + // Common setup + consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + consumerModule := consumer.NewAppModule(consumerKeeper) + + // Instantiate valid params as default. Individual test cases mutate these as needed. + params := params{ + ctx: ctx, + portID: ccv.ConsumerPortID, + channelID: "consumerCCVChannelID", + counterpartyChannelID: "providerCCVChannelID", + } + + metadata := providertypes.HandshakeMetadata{ + ProviderFeePoolAddr: "someAcct", + Version: ccv.Version, + } + + metadataBz, err := metadata.Marshal() + require.NoError(t, err) + + params.counterpartyMetadata = string(metadataBz) + + tc.setup(&consumerKeeper, ¶ms, mocks) + + err = consumerModule.OnChanOpenAck( + params.ctx, + params.portID, + params.channelID, + params.counterpartyChannelID, + params.counterpartyMetadata, + ) + + if tc.expPass { + require.NoError(t, err) + // Confirm address of the distribution module account (on provider) was persisted on consumer + distModuleAcct := consumerKeeper.GetProviderFeePoolAddrStr(ctx) + require.Equal(t, "someAcct", distModuleAcct) + } else { + require.Error(t, err) + } + // Confirm there are no unexpected external keeper calls + ctrl.Finish() + } +} + +// TestOnChanOpenConfirm validates the consumer's OnChanOpenConfirm implementation against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-ccf-coconfirm1 +// Spec tag: [CCV-CCF-COCONFIRM.1] +func TestOnChanOpenConfirm(t *testing.T) { + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + consumerModule := consumer.NewAppModule(consumerKeeper) + + err := consumerModule.OnChanOpenConfirm(ctx, ccv.ConsumerPortID, "channel-1") + require.Error(t, err, "OnChanOpenConfirm callback must error on consumer chain") +} + +// TestOnChanCloseInit validates the consumer's OnChanCloseInit implementation against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-ccf-ccinit1 +// Spec tag: [CCV-CCF-CCINIT.1] +func TestOnChanCloseInit(t *testing.T) { + + testCases := []struct { + name string + channelToClose string + establishedProviderExists bool + expPass bool + }{ + { + name: "No established provider channel, error returned disallowing closing of channel", + channelToClose: "someChannelID", + establishedProviderExists: false, + expPass: false, + }, + { + name: "Provider channel is established, User CANNOT close established provider channel", + channelToClose: "provider", + establishedProviderExists: true, + expPass: false, + }, + { + name: "User CAN close duplicate channel that is NOT established provider", + channelToClose: "someChannelID", + establishedProviderExists: true, + expPass: true, + }, + } + + for _, tc := range testCases { + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + consumerModule := consumer.NewAppModule(consumerKeeper) + + if tc.establishedProviderExists { + consumerKeeper.SetProviderChannel(ctx, "provider") + } + + err := consumerModule.OnChanCloseInit(ctx, "portID", tc.channelToClose) + + if tc.expPass { + require.NoError(t, err) + } else { + require.Error(t, err) + } + ctrl.Finish() + } +} + +// TestOnChanCloseConfirm validates the consumer's OnChanCloseConfirm implementation against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-ccconfirm1// Spec tag: [CCV-CCF-CCINIT.1] +// Spec tag: [CCV-PCF-CCCONFIRM.1] +func TestOnChanCloseConfirm(t *testing.T) { + + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + + // No external keeper methods should be called + defer ctrl.Finish() + + consumerModule := consumer.NewAppModule(consumerKeeper) + + // Nothing happens, no error returned + err := consumerModule.OnChanCloseConfirm(ctx, "portID", "channelID") + require.NoError(t, err) +} diff --git a/x/ccv/consumer/keeper/keeper.go b/x/ccv/consumer/keeper/keeper.go index 4bf624ba20..aff3a648e7 100644 --- a/x/ccv/consumer/keeper/keeper.go +++ b/x/ccv/consumer/keeper/keeper.go @@ -120,7 +120,7 @@ func (k Keeper) GetPort(ctx sdk.Context) string { return string(store.Get(types.PortKey())) } -// SetPort sets the portID for the transfer module. Used in InitGenesis +// SetPort sets the portID for the CCV module. Used in InitGenesis func (k Keeper) SetPort(ctx sdk.Context, portID string) { store := ctx.KVStore(k.storeKey) store.Set(types.PortKey(), []byte(portID)) @@ -131,8 +131,7 @@ func (k Keeper) AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Cap return k.scopedKeeper.AuthenticateCapability(ctx, cap, name) } -// ClaimCapability allows the transfer module that can claim a capability that IBC module -// passes to it +// ClaimCapability claims a capability that the IBC module passes to it func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error { return k.scopedKeeper.ClaimCapability(ctx, cap, name) } @@ -161,14 +160,14 @@ func (k Keeper) DeleteUnbondingTime(ctx sdk.Context) { store.Delete(types.UnbondingTimeKey()) } -// SetProviderClientID sets the provider clientID that is validating the chain. +// SetProviderClientID sets the clientID for the client to the provider. // Set in InitGenesis func (k Keeper) SetProviderClientID(ctx sdk.Context, clientID string) { store := ctx.KVStore(k.storeKey) store.Set(types.ProviderClientIDKey(), []byte(clientID)) } -// GetProviderClientID gets the provider clientID that is validating the chain. +// GetProviderClientID gets the clientID for the client to the provider. func (k Keeper) GetProviderClientID(ctx sdk.Context) (string, bool) { store := ctx.KVStore(k.storeKey) clientIdBytes := store.Get(types.ProviderClientIDKey()) @@ -178,13 +177,13 @@ func (k Keeper) GetProviderClientID(ctx sdk.Context) (string, bool) { return string(clientIdBytes), true } -// SetProviderChannel sets the provider channelID that is validating the chain. +// SetProviderChannel sets the channelID for the channel to the provider. func (k Keeper) SetProviderChannel(ctx sdk.Context, channelID string) { store := ctx.KVStore(k.storeKey) store.Set(types.ProviderChannelKey(), []byte(channelID)) } -// GetProviderChannel gets the provider channelID that is validating the chain. +// GetProviderChannel gets the channelID for the channel to the provider. func (k Keeper) GetProviderChannel(ctx sdk.Context) (string, bool) { store := ctx.KVStore(k.storeKey) channelIdBytes := store.Get(types.ProviderChannelKey()) @@ -194,7 +193,7 @@ func (k Keeper) GetProviderChannel(ctx sdk.Context) (string, bool) { return string(channelIdBytes), true } -// DeleteProviderChannel deletes the provider channel ID that is validating the chain. +// DeleteProviderChannel deletes the channelID for the channel to the provider. func (k Keeper) DeleteProviderChannel(ctx sdk.Context) { store := ctx.KVStore(k.storeKey) store.Delete(types.ProviderChannelKey()) @@ -276,7 +275,7 @@ func (k Keeper) DeletePacketMaturityTime(ctx sdk.Context, vscId uint64) { // VerifyProviderChain verifies that the chain trying to connect on the channel handshake // is the expected provider chain. -func (k Keeper) VerifyProviderChain(ctx sdk.Context, channelID string, connectionHops []string) error { +func (k Keeper) VerifyProviderChain(ctx sdk.Context, connectionHops []string) error { if len(connectionHops) != 1 { return sdkerrors.Wrap(channeltypes.ErrTooManyConnectionHops, "must have direct connection to provider chain") } diff --git a/x/ccv/consumer/keeper/keeper_test.go b/x/ccv/consumer/keeper/keeper_test.go index a9c457f29e..4a11a84e30 100644 --- a/x/ccv/consumer/keeper/keeper_test.go +++ b/x/ccv/consumer/keeper/keeper_test.go @@ -5,6 +5,8 @@ import ( "time" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + conntypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types" testkeeper "github.com/cosmos/interchain-security/testutil/keeper" "github.com/cosmos/interchain-security/x/ccv/consumer/types" ccv "github.com/cosmos/interchain-security/x/ccv/types" @@ -18,7 +20,7 @@ import ( // TestUnbondingTime tests getter and setter functionality for the unbonding period of a consumer chain func TestUnbondingTime(t *testing.T) { - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() _, ok := consumerKeeper.GetUnbondingTime(ctx) @@ -33,7 +35,7 @@ func TestUnbondingTime(t *testing.T) { // TestProviderClientID tests getter and setter functionality for the client ID stored on consumer keeper func TestProviderClientID(t *testing.T) { - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() _, ok := consumerKeeper.GetProviderClientID(ctx) @@ -47,7 +49,7 @@ func TestProviderClientID(t *testing.T) { // TestProviderChannel tests getter and setter functionality for the channel ID stored on consumer keeper func TestProviderChannel(t *testing.T) { - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() _, ok := consumerKeeper.GetProviderChannel(ctx) @@ -80,7 +82,7 @@ func TestPendingChanges(t *testing.T) { nil, ) - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() err = consumerKeeper.SetPendingChanges(ctx, pd) @@ -97,7 +99,7 @@ func TestPendingChanges(t *testing.T) { // TestPacketMaturityTime tests getter, setter, and iterator functionality for the packet maturity time of a received VSC packet func TestPacketMaturityTime(t *testing.T) { - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerKeeper.SetPacketMaturityTime(ctx, 1, 10) @@ -128,14 +130,11 @@ func TestPacketMaturityTime(t *testing.T) { // TestCrossChainValidator tests the getter, setter, and deletion method for cross chain validator records func TestCrossChainValidator(t *testing.T) { - // Construct a keeper with a custom codec keeperParams := testkeeper.NewInMemKeeperParams(t) - // Explicitly register public key interface - testkeeper.RegisterSdkCryptoCodecInterfaces(&keeperParams) - ctrl := gomock.NewController(t) + // Explicitly register codec with public key interface + keeperParams.RegisterSdkCryptoCodecInterfaces() + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, keeperParams) defer ctrl.Finish() - consumerKeeper := testkeeper.NewInMemConsumerKeeper(keeperParams, testkeeper.NewMockedKeepers(ctrl)) - ctx := keeperParams.Ctx // should return false _, found := consumerKeeper.GetCCValidator(ctx, ed25519.GenPrivKey().PubKey().Address()) @@ -173,7 +172,7 @@ func TestCrossChainValidator(t *testing.T) { // TestPendingSlashRequests tests the getter, setter, appending method, and deletion method for pending slash requests func TestPendingSlashRequests(t *testing.T) { - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() // prepare test setup by storing 10 pending slash requests @@ -205,3 +204,86 @@ func TestPendingSlashRequests(t *testing.T) { require.Len(t, requests, tc.expLen) } } + +// TestVerifyProviderChain tests the VerifyProviderChain method for the consumer keeper +func TestVerifyProviderChain(t *testing.T) { + + testCases := []struct { + name string + // State-mutating setup specific to this test case + mockSetup func(sdk.Context, testkeeper.MockedKeepers) + connectionHops []string + expError bool + }{ + { + name: "success", + mockSetup: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) { + gomock.InOrder( + mocks.MockConnectionKeeper.EXPECT().GetConnection( + ctx, "connectionID", + ).Return(conntypes.ConnectionEnd{ClientId: "clientID"}, true).Times(1), + ) + }, + connectionHops: []string{"connectionID"}, + expError: false, + }, + { + name: "connection hops is not length 1", + mockSetup: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) { + // Expect no calls to GetConnection(), VerifyProviderChain will return from first step. + gomock.InAnyOrder( + mocks.MockConnectionKeeper.EXPECT().GetConnection(gomock.Any(), gomock.Any()).Times(0), + ) + }, + connectionHops: []string{"connectionID", "otherConnID"}, + expError: true, + }, + { + name: "connection does not exist", + mockSetup: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) { + gomock.InOrder( + mocks.MockConnectionKeeper.EXPECT().GetConnection( + ctx, "connectionID").Return(conntypes.ConnectionEnd{}, + false, // Found is returned as false + ).Times(1), + ) + }, + connectionHops: []string{"connectionID"}, + expError: true, + }, + { + name: "found clientID does not match expectation", + mockSetup: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) { + gomock.InOrder( + mocks.MockConnectionKeeper.EXPECT().GetConnection( + ctx, "connectionID").Return( + conntypes.ConnectionEnd{ClientId: "unexpectedClientID"}, true, + ).Times(1), + ) + }, + connectionHops: []string{"connectionID"}, + expError: true, + }, + } + + for _, tc := range testCases { + + keeperParams := testkeeper.NewInMemKeeperParams(t) + consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, keeperParams) + + // Common setup + consumerKeeper.SetProviderClientID(ctx, "clientID") // Set expected provider clientID + + // Specific mock setup + tc.mockSetup(ctx, mocks) + + err := consumerKeeper.VerifyProviderChain(ctx, tc.connectionHops) + + if tc.expError { + require.Error(t, err, "invalid case did not return error") + } else { + require.NoError(t, err, "valid case returned error") + } + ctrl.Finish() + } +} diff --git a/x/ccv/consumer/keeper/params_test.go b/x/ccv/consumer/keeper/params_test.go index a30866c121..0bc043d984 100644 --- a/x/ccv/consumer/keeper/params_test.go +++ b/x/ccv/consumer/keeper/params_test.go @@ -10,7 +10,7 @@ import ( // TestParams tests the default params set for a consumer chain, and related getters/setters func TestParams(t *testing.T) { - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() consumerKeeper.SetParams(ctx, types.DefaultParams()) diff --git a/x/ccv/consumer/keeper/relay_test.go b/x/ccv/consumer/keeper/relay_test.go index 88af8983da..028675793c 100644 --- a/x/ccv/consumer/keeper/relay_test.go +++ b/x/ccv/consumer/keeper/relay_test.go @@ -109,7 +109,7 @@ func TestOnRecvVSCPacket(t *testing.T) { }, } - consumerKeeper, ctx, ctrl := testkeeper.GetConsumerKeeperAndCtx(t) + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() // Set channel to provider, still in context of consumer chain diff --git a/x/ccv/consumer/keeper/validators_test.go b/x/ccv/consumer/keeper/validators_test.go index b5ef2003fb..2b4f1484fe 100644 --- a/x/ccv/consumer/keeper/validators_test.go +++ b/x/ccv/consumer/keeper/validators_test.go @@ -9,7 +9,6 @@ import ( testkeeper "github.com/cosmos/interchain-security/testutil/keeper" "github.com/cosmos/interchain-security/x/ccv/consumer/keeper" "github.com/cosmos/interchain-security/x/ccv/consumer/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" tmrand "github.com/tendermint/tendermint/libs/rand" @@ -18,16 +17,12 @@ import ( // TestApplyCCValidatorChanges tests the ApplyCCValidatorChanges method for a consumer keeper func TestApplyCCValidatorChanges(t *testing.T) { - // Construct a keeper with a custom codec - keeperParams := testkeeper.NewInMemKeeperParams(t) - - // Explicitly register public key interface - testkeeper.RegisterSdkCryptoCodecInterfaces(&keeperParams) - ctrl := gomock.NewController(t) + keeperParams := testkeeper.NewInMemKeeperParams(t) + // Explicitly register cdc with public key interface + keeperParams.RegisterSdkCryptoCodecInterfaces() + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, keeperParams) defer ctrl.Finish() - consumerKeeper := testkeeper.NewInMemConsumerKeeper(keeperParams, testkeeper.NewMockedKeepers(ctrl)) - ctx := keeperParams.Ctx // utility functions getCCVals := func() (vals []types.CrossChainValidator) { @@ -112,15 +107,12 @@ func TestApplyCCValidatorChanges(t *testing.T) { // Tests the getter and setter behavior for historical info func TestHistoricalInfo(t *testing.T) { - // Construct a keeper with a custom codec keeperParams := testkeeper.NewInMemKeeperParams(t) - // Explicitly register public key interface - testkeeper.RegisterSdkCryptoCodecInterfaces(&keeperParams) - ctrl := gomock.NewController(t) + // Explicitly register cdc with public key interface + keeperParams.RegisterSdkCryptoCodecInterfaces() + consumerKeeper, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, keeperParams) defer ctrl.Finish() - consumerKeeper := testkeeper.NewInMemConsumerKeeper(keeperParams, testkeeper.NewMockedKeepers(ctrl)) - - ctx := keeperParams.Ctx.WithBlockHeight(15) + ctx = ctx.WithBlockHeight(15) // Generate test validators, save them to store, and retrieve stored records validators := GenerateValidators(t) diff --git a/x/ccv/provider/ibc_module.go b/x/ccv/provider/ibc_module.go index e573478197..8792236d14 100644 --- a/x/ccv/provider/ibc_module.go +++ b/x/ccv/provider/ibc_module.go @@ -16,6 +16,9 @@ import ( ) // OnChanOpenInit implements the IBCModule interface +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-coinit1 +// Spec Tag: [CCV-PCF-COINIT.1] func (am AppModule) OnChanOpenInit( ctx sdk.Context, order channeltypes.Order, @@ -30,6 +33,10 @@ func (am AppModule) OnChanOpenInit( } // OnChanOpenTry implements the IBCModule interface +// +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-cotry1 +// Spec tag: [CCV-PCF-COTRY.1] func (am AppModule) OnChanOpenTry( ctx sdk.Context, order channeltypes.Order, @@ -110,6 +117,9 @@ func validateCCVChannelParams( } // OnChanOpenAck implements the IBCModule interface +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-coack1 +// Spec tag: [CCV-PCF-COACK.1] func (am AppModule) OnChanOpenAck( ctx sdk.Context, portID, @@ -121,6 +131,9 @@ func (am AppModule) OnChanOpenAck( } // OnChanOpenConfirm implements the IBCModule interface +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-coconfirm1 +// Spec tag: [CCV-PCF-COCONFIRM.1] func (am AppModule) OnChanOpenConfirm( ctx sdk.Context, portID, @@ -153,7 +166,7 @@ func (am AppModule) OnChanCloseConfirm( } // OnRecvPacket implements the IBCModule interface. A successful acknowledgement -// is returned if the packet data is succesfully decoded and the receive application +// is returned if the packet data is successfully decoded and the receive application // logic returns without error. func (am AppModule) OnRecvPacket( ctx sdk.Context, diff --git a/x/ccv/provider/ibc_module_test.go b/x/ccv/provider/ibc_module_test.go new file mode 100644 index 0000000000..c2ebbe636f --- /dev/null +++ b/x/ccv/provider/ibc_module_test.go @@ -0,0 +1,336 @@ +package provider_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + conntypes "github.com/cosmos/ibc-go/v3/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v3/modules/core/24-host" + ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + testkeeper "github.com/cosmos/interchain-security/testutil/keeper" + "github.com/cosmos/interchain-security/x/ccv/provider" + providerkeeper "github.com/cosmos/interchain-security/x/ccv/provider/keeper" + providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" + ccv "github.com/cosmos/interchain-security/x/ccv/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +// TestOnChanOpenInit tests the provider's OnChanOpenInit method against spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-coinit1 +// Spec Tag: [CCV-PCF-COINIT.1] +func TestOnChanOpenInit(t *testing.T) { + + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + providerModule := provider.NewAppModule(&providerKeeper) + + // OnChanOpenInit must error for provider even with correct arguments + err := providerModule.OnChanOpenInit( + ctx, + channeltypes.ORDERED, + []string{"connection-1"}, + ccv.ProviderPortID, + "channel-1", + nil, + channeltypes.NewCounterparty(ccv.ConsumerPortID, "channel-1"), + ccv.Version, + ) + require.Error(t, err, "OnChanOpenInit must error on provider chain") +} + +// TestOnChanOpenTry validates the provider's OnChanOpenTry implementation against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-cotry1 +// Spec tag: [CCV-PCF-COTRY.1] +func TestOnChanOpenTry(t *testing.T) { + + // Params for the ChanOpenTry method + type params struct { + ctx sdk.Context + order channeltypes.Order + connectionHops []string + portID string + channelID string + chanCap *capabilitytypes.Capability + counterparty channeltypes.Counterparty + counterpartyVersion string + } + + testCases := []struct { + name string + mutateParams func(*params, *providerkeeper.Keeper) + expPass bool + }{ + { + "success", func(*params, *providerkeeper.Keeper) {}, true, + }, + { + "invalid order", func(params *params, keeper *providerkeeper.Keeper) { + params.order = channeltypes.UNORDERED + }, false, + }, + { + "invalid port ID", func(params *params, keeper *providerkeeper.Keeper) { + params.portID = "bad port" + }, false, + }, + { + "invalid counter party port ID", func(params *params, keeper *providerkeeper.Keeper) { + params.counterparty.PortId = "bad port" + }, false, + }, + { + "invalid counter party version", func(params *params, keeper *providerkeeper.Keeper) { + params.counterpartyVersion = "invalidVersion" + }, false, + }, + { + "unexpected client ID mapped to chain ID", func(params *params, keeper *providerkeeper.Keeper) { + keeper.SetConsumerClientId( + params.ctx, + "consumerChainID", + "invalidClientID", + ) + }, false, + }, + { + "other CCV channel exists for this consumer chain", + func(params *params, keeper *providerkeeper.Keeper) { + keeper.SetChainToChannel( + params.ctx, + "consumerChainID", + "some existing channel ID", + ) + }, false, + }, + } + + for _, tc := range testCases { + + // Setup + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + providerModule := provider.NewAppModule(&providerKeeper) + + providerKeeper.SetPort(ctx, ccv.ProviderPortID) + providerKeeper.SetConsumerClientId(ctx, "consumerChainID", "clientIDToConsumer") + + // Instantiate valid params as default. Individual test cases mutate these as needed. + params := params{ + ctx: ctx, + order: channeltypes.ORDERED, + connectionHops: []string{"connectionIDToConsumer"}, + portID: ccv.ProviderPortID, + channelID: "providerChannelID", + chanCap: &capabilitytypes.Capability{}, + counterparty: channeltypes.NewCounterparty(ccv.ConsumerPortID, "consumerChannelID"), + counterpartyVersion: ccv.Version, + } + + // Expected mock calls + moduleAcct := authtypes.ModuleAccount{BaseAccount: &authtypes.BaseAccount{}} + moduleAcct.BaseAccount.Address = authtypes.NewModuleAddress(authtypes.FeeCollectorName).String() + + // Number of calls is not asserted, since not all code paths are hit for failures + gomock.InOrder( + mocks.MockScopedKeeper.EXPECT().ClaimCapability( + params.ctx, params.chanCap, host.ChannelCapabilityPath(params.portID, params.channelID)).AnyTimes(), + mocks.MockConnectionKeeper.EXPECT().GetConnection(ctx, "connectionIDToConsumer").Return( + conntypes.ConnectionEnd{ClientId: "clientIDToConsumer"}, true, + ).AnyTimes(), + mocks.MockClientKeeper.EXPECT().GetClientState(ctx, "clientIDToConsumer").Return( + &ibctmtypes.ClientState{ChainId: "consumerChainID"}, true, + ).AnyTimes(), + mocks.MockAccountKeeper.EXPECT().GetModuleAccount(ctx, "").Return(&moduleAcct).AnyTimes(), + ) + + tc.mutateParams(¶ms, &providerKeeper) + + metadata, err := providerModule.OnChanOpenTry( + params.ctx, + params.order, + params.connectionHops, + params.portID, + params.channelID, + params.chanCap, + params.counterparty, + params.counterpartyVersion, + ) + + if tc.expPass { + require.NoError(t, err) + md := &providertypes.HandshakeMetadata{} + err = md.Unmarshal([]byte(metadata)) + require.NoError(t, err) + require.Equal(t, moduleAcct.BaseAccount.Address, md.ProviderFeePoolAddr, + "returned dist account metadata must match expected") + require.Equal(t, ccv.Version, md.Version, "returned ccv version metadata must match expected") + ctrl.Finish() + } else { + require.Error(t, err) + } + } +} + +// TestOnChanOpenAck tests the provider's OnChanOpenAck method against spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-coack1 +// Spec tag: [CCV-PCF-COACK.1] +func TestOnChanOpenAck(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + providerModule := provider.NewAppModule(&providerKeeper) + + // OnChanOpenAck must error for provider even with correct arguments + err := providerModule.OnChanOpenAck( + ctx, + ccv.ProviderPortID, + "providerChannelID", + "consumerChannelID", + ccv.Version, + ) + require.Error(t, err, "OnChanOpenAck must error on provider chain") +} + +// TestOnChanOpenConfirm tests the provider's OnChanOpenConfirm method against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-coconfirm1 +// Spec tag: [CCV-PCF-COCONFIRM.1] +// +// TODO: Validate spec requirement that duplicate channels attempting to become canonical CCV channel are closed. +// See: https://github.com/cosmos/interchain-security/issues/327 +func TestOnChanOpenConfirm(t *testing.T) { + + testCases := []struct { + name string + mockExpectations func(sdk.Context, testkeeper.MockedKeepers) []*gomock.Call + setDuplicateChannel bool + expPass bool + }{ + { + name: "channel not found", + mockExpectations: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) []*gomock.Call { + return []*gomock.Call{ + mocks.MockChannelKeeper.EXPECT().GetChannel( + ctx, ccv.ProviderPortID, gomock.Any()).Return(channeltypes.Channel{}, + false, // Found is false + ).Times(1), + } + }, + expPass: false, + }, + { + name: "too many connection hops", + mockExpectations: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) []*gomock.Call { + return []*gomock.Call{ + mocks.MockChannelKeeper.EXPECT().GetChannel( + ctx, ccv.ProviderPortID, gomock.Any()).Return(channeltypes.Channel{ + State: channeltypes.OPEN, + ConnectionHops: []string{"connectionID", "another"}, // Two hops is two many + }, false, + ).Times(1), + } + }, + expPass: false, + }, + { + name: "connection not found", + mockExpectations: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) []*gomock.Call { + return []*gomock.Call{ + mocks.MockChannelKeeper.EXPECT().GetChannel( + ctx, ccv.ProviderPortID, gomock.Any()).Return(channeltypes.Channel{ + State: channeltypes.OPEN, + ConnectionHops: []string{"connectionID"}, + }, true, + ).Times(1), + mocks.MockConnectionKeeper.EXPECT().GetConnection(ctx, "connectionID").Return( + conntypes.ConnectionEnd{}, false, // Found is false + ).Times(1), + } + }, + expPass: false, + }, + { + name: "client state not found", + mockExpectations: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) []*gomock.Call { + return []*gomock.Call{ + mocks.MockChannelKeeper.EXPECT().GetChannel(ctx, ccv.ProviderPortID, gomock.Any()).Return( + channeltypes.Channel{ + State: channeltypes.OPEN, + ConnectionHops: []string{"connectionID"}, + }, + true, + ).Times(1), + mocks.MockConnectionKeeper.EXPECT().GetConnection(ctx, "connectionID").Return( + conntypes.ConnectionEnd{ClientId: "clientID"}, true, + ).Times(1), + mocks.MockClientKeeper.EXPECT().GetClientState(ctx, "clientID").Return( + nil, false, // Found is false + ).Times(1), + } + }, + expPass: false, + }, + { + name: "CCV channel already exists, error returned, but dup channel is not closed", + mockExpectations: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) []*gomock.Call { + // Error is returned after all expected mock calls are hit for SetConsumerChain + return testkeeper.GetMocksForSetConsumerChain(ctx, &mocks, "consumerChainID") + }, + setDuplicateChannel: true, // Only case where duplicate channel is setup + expPass: false, + }, + { + name: "success", + mockExpectations: func(ctx sdk.Context, mocks testkeeper.MockedKeepers) []*gomock.Call { + // Full SetConsumerChain method should run without error, hitting all expected mocks + return testkeeper.GetMocksForSetConsumerChain(ctx, &mocks, "consumerChainID") + }, + expPass: true, + }, + } + + for _, tc := range testCases { + + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx( + t, testkeeper.NewInMemKeeperParams(t)) + + gomock.InOrder(tc.mockExpectations(ctx, mocks)...) + + if tc.setDuplicateChannel { + providerKeeper.SetChainToChannel(ctx, "consumerChainID", "existingChannelID") + } + + providerModule := provider.NewAppModule(&providerKeeper) + + err := providerModule.OnChanOpenConfirm(ctx, "providerPortID", "channelID") + + if tc.expPass { + + require.NoError(t, err) + // Validate channel mappings + channelID, found := providerKeeper.GetChainToChannel(ctx, "consumerChainID") + require.True(t, found) + require.Equal(t, "channelID", channelID) + + chainID, found := providerKeeper.GetChannelToChain(ctx, "channelID") + require.True(t, found) + require.Equal(t, "consumerChainID", chainID) + + height, found := providerKeeper.GetInitChainHeight(ctx, "consumerChainID") + require.True(t, found) + require.Equal(t, ctx.BlockHeight(), int64(height)) + + } else { + require.Error(t, err) + } + ctrl.Finish() + } +} diff --git a/x/ccv/provider/keeper/genesis.go b/x/ccv/provider/keeper/genesis.go index adba5471a8..c8b3846a0e 100644 --- a/x/ccv/provider/keeper/genesis.go +++ b/x/ccv/provider/keeper/genesis.go @@ -14,7 +14,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) { // Only try to bind to port if it is not already bound, since we may already own // port capability from capability InitGenesis if !k.IsBound(ctx, ccv.ProviderPortID) { - // transfer module binds to the transfer port on InitChain + // CCV module binds to the provider port on InitChain // and claims the returned capability err := k.BindPort(ctx, ccv.ProviderPortID) if err != nil { diff --git a/x/ccv/provider/keeper/keeper.go b/x/ccv/provider/keeper/keeper.go index 9342884690..dd01fe24ee 100644 --- a/x/ccv/provider/keeper/keeper.go +++ b/x/ccv/provider/keeper/keeper.go @@ -77,26 +77,26 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+host.ModuleName+"-"+types.ModuleName) } -// IsBound checks if the transfer module is already bound to the desired port +// IsBound checks if the CCV module is already bound to the desired port func (k Keeper) IsBound(ctx sdk.Context, portID string) bool { _, ok := k.scopedKeeper.GetCapability(ctx, host.PortPath(portID)) return ok } -// BindPort defines a wrapper function for the ort Keeper's function in +// BindPort defines a wrapper function for the port Keeper's function in // order to expose it to module's InitGenesis function func (k Keeper) BindPort(ctx sdk.Context, portID string) error { cap := k.portKeeper.BindPort(ctx, portID) return k.ClaimCapability(ctx, cap, host.PortPath(portID)) } -// GetPort returns the portID for the transfer module. Used in ExportGenesis +// GetPort returns the portID for the CCV module. Used in ExportGenesis func (k Keeper) GetPort(ctx sdk.Context) string { store := ctx.KVStore(k.storeKey) return string(store.Get(types.PortKey())) } -// SetPort sets the portID for the transfer module. Used in InitGenesis +// SetPort sets the portID for the CCV module. Used in InitGenesis func (k Keeper) SetPort(ctx sdk.Context, portID string) { store := ctx.KVStore(k.storeKey) store.Set(types.PortKey(), []byte(portID)) @@ -271,14 +271,14 @@ func (k Keeper) SetConsumerChain(ctx sdk.Context, channelID string) error { return sdkerrors.Wrap(channeltypes.ErrTooManyConnectionHops, "must have direct connection to consumer chain") } connectionID := channel.ConnectionHops[0] - chainID, tmClient, err := k.getUnderlyingClient(ctx, connectionID) + _, tmClient, err := k.getUnderlyingClient(ctx, connectionID) if err != nil { return err } // Verify that there isn't already a CCV channel for the consumer chain - // If there is, then close the channel. - if prevChannel, ok := k.GetChannelToChain(ctx, chainID); ok { - return sdkerrors.Wrapf(ccv.ErrDuplicateChannel, "CCV channel with ID: %s already created for consumer chain %s", prevChannel, chainID) + chainID := tmClient.ChainId + if prevChannelID, ok := k.GetChainToChannel(ctx, chainID); ok { + return sdkerrors.Wrapf(ccv.ErrDuplicateChannel, "CCV channel with ID: %s already created for consumer chain %s", prevChannelID, chainID) } // the CCV channel is established: @@ -449,21 +449,27 @@ func (k Keeper) EmptyMaturedUnbondingOps(ctx sdk.Context) ([]uint64, error) { return ids, nil } -func (k Keeper) getUnderlyingClient(ctx sdk.Context, connectionID string) (string, *ibctmtypes.ClientState, error) { - // Retrieve the underlying client state. +// Retrieves the underlying client state corresponding to a connection ID. +func (k Keeper) getUnderlyingClient(ctx sdk.Context, connectionID string) ( + clientID string, tmClient *ibctmtypes.ClientState, err error) { + conn, ok := k.connectionKeeper.GetConnection(ctx, connectionID) if !ok { - return "", nil, sdkerrors.Wrapf(conntypes.ErrConnectionNotFound, "connection not found for connection ID: %s", connectionID) + return "", nil, sdkerrors.Wrapf(conntypes.ErrConnectionNotFound, + "connection not found for connection ID: %s", connectionID) } - client, ok := k.clientKeeper.GetClientState(ctx, conn.ClientId) + clientID = conn.ClientId + clientState, ok := k.clientKeeper.GetClientState(ctx, clientID) if !ok { - return "", nil, sdkerrors.Wrapf(clienttypes.ErrClientNotFound, "client not found for client ID: %s", conn.ClientId) + return "", nil, sdkerrors.Wrapf(clienttypes.ErrClientNotFound, + "client not found for client ID: %s", conn.ClientId) } - tmClient, ok := client.(*ibctmtypes.ClientState) + tmClient, ok = clientState.(*ibctmtypes.ClientState) if !ok { - return "", nil, sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, "invalid client type. expected %s, got %s", ibcexported.Tendermint, client.ClientType()) + return "", nil, sdkerrors.Wrapf(clienttypes.ErrInvalidClientType, + "invalid client type. expected %s, got %s", ibcexported.Tendermint, clientState.ClientType()) } - return conn.ClientId, tmClient, nil + return clientID, tmClient, nil } // chanCloseInit defines a wrapper function for the channel Keeper's function diff --git a/x/ccv/provider/keeper/keeper_test.go b/x/ccv/provider/keeper/keeper_test.go index 8f4c6f5f66..c201f53e33 100644 --- a/x/ccv/provider/keeper/keeper_test.go +++ b/x/ccv/provider/keeper/keeper_test.go @@ -23,7 +23,7 @@ import ( // TestValsetUpdateBlockHeight tests the getter, setter, and deletion methods for valset updates mapped to block height func TestValsetUpdateBlockHeight(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() blockHeight, found := providerKeeper.GetValsetUpdateBlockHeight(ctx, uint64(0)) @@ -49,7 +49,7 @@ func TestValsetUpdateBlockHeight(t *testing.T) { // TestSlashAcks tests the getter, setter, iteration, and deletion methods for stored slash acknowledgements func TestSlashAcks(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() var chainsAcks [][]string @@ -92,7 +92,7 @@ func TestSlashAcks(t *testing.T) { // TestAppendSlashAck tests the append method for stored slash acknowledgements func TestAppendSlashAck(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() p := []string{"alice", "bob", "charlie"} @@ -112,7 +112,7 @@ func TestAppendSlashAck(t *testing.T) { // TestPendingVSCs tests the getter, appending, and deletion methods for stored pending VSCs func TestPendingVSCs(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() chainID := "consumer" @@ -167,7 +167,7 @@ func TestPendingVSCs(t *testing.T) { // TestInitHeight tests the getter and setter methods for the stored block heights (on provider) when a given consumer chain was started func TestInitHeight(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() tc := []struct { @@ -252,7 +252,7 @@ func TestHandleSlashPacketDoubleSigning(t *testing.T) { func TestIterateOverUnbondingOpIndex(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() chainID := "6" @@ -278,7 +278,7 @@ func TestIterateOverUnbondingOpIndex(t *testing.T) { func TestMaturedUnbondingOps(t *testing.T) { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() ids, err := providerKeeper.GetMaturedUnbondingOps(ctx) diff --git a/x/ccv/provider/keeper/params_test.go b/x/ccv/provider/keeper/params_test.go index 685e3cb71f..6a8298ad6f 100644 --- a/x/ccv/provider/keeper/params_test.go +++ b/x/ccv/provider/keeper/params_test.go @@ -9,7 +9,6 @@ import ( ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" testkeeper "github.com/cosmos/interchain-security/testutil/keeper" "github.com/cosmos/interchain-security/x/ccv/provider/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) @@ -17,15 +16,11 @@ import ( func TestParams(t *testing.T) { defaultParams := types.DefaultParams() - // Construct an in-mem keeper with a default template client state set + // Construct an in-mem keeper with a populated template client state keeperParams := testkeeper.NewInMemKeeperParams(t) - ctrl := gomock.NewController(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) defer ctrl.Finish() - mocks := testkeeper.NewMockedKeepers(ctrl) - ctx := keeperParams.Ctx - // Populate template client state to test against - testkeeper.SetTemplateClientState(ctx, keeperParams.ParamsSubspace) - providerKeeper := testkeeper.NewInMemProviderKeeper(keeperParams, mocks) params := providerKeeper.GetParams(ctx) require.Equal(t, defaultParams, params) diff --git a/x/ccv/provider/keeper/proposal.go b/x/ccv/provider/keeper/proposal.go index 7e1ed37182..a0690ce247 100644 --- a/x/ccv/provider/keeper/proposal.go +++ b/x/ccv/provider/keeper/proposal.go @@ -23,6 +23,10 @@ import ( // HandleConsumerAdditionProposal will receive the consumer chain's client state from the proposal. // If the spawn time has already passed, then set the consumer chain. Otherwise store the client // as a pending client, and set once spawn time has passed. +// +// Note: This method implements SpawnConsumerChainProposalHandler in spec. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-spccprop1 +// Spec tag: [CCV-PCF-SPCCPROP.1] func (k Keeper) HandleConsumerAdditionProposal(ctx sdk.Context, p *types.ConsumerAdditionProposal) error { if !ctx.BlockTime().Before(p.SpawnTime) { // lockUbdOnTimeout is set to be false, regardless of what the proposal says, until we can specify and test issues around this use case more thoroughly @@ -37,8 +41,63 @@ func (k Keeper) HandleConsumerAdditionProposal(ctx sdk.Context, p *types.Consume return nil } -// HandleConsumerRemovalProposal stops a consumer chain and releases the outstanding unbonding operations. +// CreateConsumerClient will create the CCV client for the given consumer chain. The CCV channel must be built +// on top of the CCV client to ensure connection with the right consumer chain. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-crclient1 +// Spec tag: [CCV-PCF-CRCLIENT.1] +func (k Keeper) CreateConsumerClient(ctx sdk.Context, chainID string, initialHeight clienttypes.Height, lockUbdOnTimeout bool) error { + // check that a client for this chain does not exist + if _, found := k.GetConsumerClientId(ctx, chainID); found { + // drop the proposal + return nil + } + + // Use the unbonding period on the provider to compute the unbonding period on the consumer + unbondingPeriod := utils.ComputeConsumerUnbondingPeriod(k.stakingKeeper.UnbondingTime(ctx)) + + // Create client state by getting template client from parameters and filling in zeroed fields from proposal. + clientState := k.GetTemplateClient(ctx) + clientState.ChainId = chainID + clientState.LatestHeight = initialHeight + clientState.TrustingPeriod = unbondingPeriod / utils.TrustingPeriodFraction + clientState.UnbondingPeriod = unbondingPeriod + + // TODO: Allow for current validators to set different keys + consensusState := ibctmtypes.NewConsensusState( + ctx.BlockTime(), + commitmenttypes.NewMerkleRoot([]byte(ibctmtypes.SentinelRoot)), + ctx.BlockHeader().NextValidatorsHash, + ) + + clientID, err := k.clientKeeper.CreateClient(ctx, clientState, consensusState) + if err != nil { + return err + } + k.SetConsumerClientId(ctx, chainID, clientID) + + consumerGen, err := k.MakeConsumerGenesis(ctx) + if err != nil { + return err + } + err = k.SetConsumerGenesis(ctx, chainID, consumerGen) + if err != nil { + return err + } + + // store LockUnbondingOnTimeout flag + if lockUbdOnTimeout { + k.SetLockUnbondingOnTimeout(ctx, chainID) + } + return nil +} + +// HandleConsumerRemovalProposal stops a consumer chain and released the outstanding unbonding operations. // If the stop time hasn't already passed, it stores the proposal as a pending proposal. +// +// This method implements StopConsumerChainProposalHandler from spec. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-stccprop1 +// Spec tag: [CCV-PCF-STCCPROP.1] func (k Keeper) HandleConsumerRemovalProposal(ctx sdk.Context, p *types.ConsumerRemovalProposal) error { if !ctx.BlockTime().Before(p.StopTime) { @@ -51,6 +110,10 @@ func (k Keeper) HandleConsumerRemovalProposal(ctx sdk.Context, p *types.Consumer // StopConsumerChain cleans up the states for the given consumer chain ID and, if the given lockUbd is false, // it completes the outstanding unbonding operations lock by the consumer chain. +// +// This method implements StopConsumerChain from spec. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-stcc1 +// Spec tag: [CCV-PCF-STCC.1] func (k Keeper) StopConsumerChain(ctx sdk.Context, chainID string, lockUbd, closeChan bool) (err error) { // check that a client for chainID exists if _, found := k.GetConsumerClientId(ctx, chainID); !found { @@ -125,51 +188,7 @@ func (k Keeper) StopConsumerChain(ctx sdk.Context, chainID string, lockUbd, clos return nil } -// CreateConsumerClient will create the CCV client for the given consumer chain. The CCV channel must be built -// on top of the CCV client to ensure connection with the right consumer chain. -func (k Keeper) CreateConsumerClient(ctx sdk.Context, chainID string, initialHeight clienttypes.Height, lockUbdOnTimeout bool) error { - // check that a client for this chain does not exist - if _, found := k.GetConsumerClientId(ctx, chainID); found { - // drop the proposal - return nil - } - - // Use the unbonding period on the provider to - // compute the unbonding period on the consumer - unbondingTime := utils.ComputeConsumerUnbondingPeriod(k.stakingKeeper.UnbondingTime(ctx)) - - // create clientstate by getting template client from parameters and filling in zeroed fields from proposal. - clientState := k.GetTemplateClient(ctx) - clientState.ChainId = chainID - clientState.LatestHeight = initialHeight - clientState.TrustingPeriod = unbondingTime / utils.TrustingPeriodFraction - clientState.UnbondingPeriod = unbondingTime - - // TODO: Allow for current validators to set different keys - consensusState := ibctmtypes.NewConsensusState(ctx.BlockTime(), commitmenttypes.NewMerkleRoot([]byte(ibctmtypes.SentinelRoot)), ctx.BlockHeader().NextValidatorsHash) - clientID, err := k.clientKeeper.CreateClient(ctx, clientState, consensusState) - if err != nil { - return err - } - - k.SetConsumerClientId(ctx, chainID, clientID) - consumerGen, err := k.MakeConsumerGenesis(ctx) - if err != nil { - return err - } - - err = k.SetConsumerGenesis(ctx, chainID, consumerGen) - if err != nil { - return err - } - - // store LockUnbondingOnTimeout flag - if lockUbdOnTimeout { - k.SetLockUnbondingOnTimeout(ctx, chainID) - } - return nil -} - +// MakeConsumerGenesis constructs a consumer genesis state. func (k Keeper) MakeConsumerGenesis(ctx sdk.Context) (gen consumertypes.GenesisState, err error) { unbondingTime := k.stakingKeeper.UnbondingTime(ctx) height := clienttypes.GetSelfHeight(ctx) @@ -241,16 +260,16 @@ func (k Keeper) SetPendingConsumerAdditionProp(ctx sdk.Context, clientInfo *type } // GetPendingConsumerAdditionProp retrieves a pending proposal to create a consumer chain client (by spawn time and chain id) -func (k Keeper) GetPendingConsumerAdditionProp(ctx sdk.Context, spawnTime time.Time, chainID string) types.ConsumerAdditionProposal { +func (k Keeper) GetPendingConsumerAdditionProp(ctx sdk.Context, spawnTime time.Time, + chainID string) (prop types.ConsumerAdditionProposal, found bool) { store := ctx.KVStore(k.storeKey) bz := store.Get(types.PendingCAPKey(spawnTime, chainID)) if len(bz) == 0 { - return types.ConsumerAdditionProposal{} + return prop, false } - var clientInfo types.ConsumerAdditionProposal - k.cdc.MustUnmarshal(bz, &clientInfo) + k.cdc.MustUnmarshal(bz, &prop) - return clientInfo + return prop, true } func (k Keeper) PendingConsumerAdditionPropIterator(ctx sdk.Context) sdk.Iterator { @@ -258,9 +277,12 @@ func (k Keeper) PendingConsumerAdditionPropIterator(ctx sdk.Context) sdk.Iterato return sdk.KVStorePrefixIterator(store, []byte{types.PendingCAPBytePrefix}) } -// IteratePendingConsumerAdditionProps iterates over the pending consumer addition proposals to create -// clients in order and creates the consumer client if the spawn time has passed. -func (k Keeper) IteratePendingConsumerAdditionProps(ctx sdk.Context) { +// BeginBlockInit iterates over the pending consumer addition proposals in order, and creates +// clients for props in which the spawn time has passed. Executed proposals are deleted. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-bblock-init1 +// Spec tag:[CCV-PCF-BBLOCK-INIT.1] +func (k Keeper) BeginBlockInit(ctx sdk.Context) { propsToExecute := k.ConsumerAdditionPropsToExecute(ctx) for _, prop := range propsToExecute { @@ -349,9 +371,13 @@ func (k Keeper) PendingConsumerRemovalPropIterator(ctx sdk.Context) sdk.Iterator return sdk.KVStorePrefixIterator(store, []byte{types.PendingCRPBytePrefix}) } -// IteratePendingConsumerRemovalProps iterates over the pending consumer removal proposals -// in order and stop/removes the chain if the stop time has passed, otherwise it will break out of loop and return. -func (k Keeper) IteratePendingConsumerRemovalProps(ctx sdk.Context) { +// BeginBlockCCR iterates over the pending consumer removal proposals +// in order and stop/removes the chain if the stop time has passed, +// otherwise it will break out of loop and return. Executed proposals are deleted. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-bblock-ccr1 +// Spec tag: [CCV-PCF-BBLOCK-CCR.1] +func (k Keeper) BeginBlockCCR(ctx sdk.Context) { propsToExecute := k.ConsumerRemovalPropsToExecute(ctx) for _, prop := range propsToExecute { diff --git a/x/ccv/provider/keeper/proposal_test.go b/x/ccv/provider/keeper/proposal_test.go index 1932b042d3..840f4e5831 100644 --- a/x/ccv/provider/keeper/proposal_test.go +++ b/x/ccv/provider/keeper/proposal_test.go @@ -1,212 +1,774 @@ package keeper_test import ( + "encoding/json" "testing" "time" + _go "github.com/confio/ics23/go" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + ibctmtypes "github.com/cosmos/ibc-go/v3/modules/light-clients/07-tendermint/types" + "github.com/golang/mock/gomock" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/stretchr/testify/require" testkeeper "github.com/cosmos/interchain-security/testutil/keeper" + consumertypes "github.com/cosmos/interchain-security/x/ccv/consumer/types" + providerkeeper "github.com/cosmos/interchain-security/x/ccv/provider/keeper" "github.com/cosmos/interchain-security/x/ccv/provider/types" + providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" ) -func TestPendingConsumerRemovalPropDeletion(t *testing.T) { +// +// Initialization sub-protocol related tests of proposal.go +// + +// Tests the HandleConsumerAdditionProposal method against the SpawnConsumerChainProposalHandler spec. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-spccprop1 +// Spec tag: [CCV-PCF-SPCCPROP.1] +func TestHandleConsumerAdditionProposal(t *testing.T) { + + type testCase struct { + description string + prop *providertypes.ConsumerAdditionProposal + // Time when prop is handled + blockTime time.Time + // Whether it's expected that the spawn time has passed and client should be created + expCreatedClient bool + } + + // Snapshot times asserted in tests + now := time.Now().UTC() + hourFromNow := now.Add(time.Hour).UTC() + + tests := []testCase{ + { + description: "ctx block time is after proposal's spawn time, expected that client is created", + prop: providertypes.NewConsumerAdditionProposal( + "title", + "description", + "chainID", + clienttypes.NewHeight(2, 3), + []byte("gen_hash"), + []byte("bin_hash"), + now, // Spawn time + ).(*providertypes.ConsumerAdditionProposal), + blockTime: hourFromNow, + expCreatedClient: true, + }, + { + description: `ctx block time is before proposal's spawn time, + expected that no client is created and the proposal is persisted as pending`, + prop: providertypes.NewConsumerAdditionProposal( + "title", + "description", + "chainID", + clienttypes.NewHeight(2, 3), + []byte("gen_hash"), + []byte("bin_hash"), + hourFromNow, // Spawn time + ).(*types.ConsumerAdditionProposal), + blockTime: now, + expCreatedClient: false, + }, + } + + for _, tc := range tests { + // Common setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + ctx = ctx.WithBlockTime(tc.blockTime) + + if tc.expCreatedClient { + // Mock calls are only asserted if we expect a client to be created. + gomock.InOrder( + testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chainID", clienttypes.NewHeight(2, 3))..., + ) + } + + tc.prop.LockUnbondingOnTimeout = false // Full functionality not implemented yet. + + err := providerKeeper.HandleConsumerAdditionProposal(ctx, tc.prop) + require.NoError(t, err) + + if tc.expCreatedClient { + testCreatedConsumerClient(t, ctx, providerKeeper, tc.prop.ChainId, "clientID") + } else { + // check that stored pending prop is exactly the same as the initially instantiated prop + gotProposal, found := providerKeeper.GetPendingConsumerAdditionProp(ctx, tc.prop.SpawnTime, tc.prop.ChainId) + require.True(t, found) + require.Equal(t, *tc.prop, gotProposal) + // double check that a client for this chain does not exist + _, found = providerKeeper.GetConsumerClientId(ctx, tc.prop.ChainId) + require.False(t, found) + } + ctrl.Finish() + } +} + +// Tests the CreateConsumerClient method against the spec, +// with more granularity than what's covered in TestHandleCreateConsumerChainProposal. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-crclient1 +// Spec tag: [CCV-PCF-CRCLIENT.1] +func TestCreateConsumerClient(t *testing.T) { + + type testCase struct { + description string + // Any state-mutating setup on keeper and expected mock calls, specific to this test case + setup func(*providerkeeper.Keeper, sdk.Context, *testkeeper.MockedKeepers) + // Whether a client should be created + expClientCreated bool + } + tests := []testCase{ + { + description: "No state mutation, new client should be created", + setup: func(providerKeeper *providerkeeper.Keeper, ctx sdk.Context, mocks *testkeeper.MockedKeepers) { + + // Valid client creation is asserted with mock expectations here + gomock.InOrder( + testkeeper.GetMocksForCreateConsumerClient(ctx, mocks, "chainID", clienttypes.NewHeight(4, 5))..., + ) + }, + expClientCreated: true, + }, + { + description: "client for this chain already exists, new one is not created", + setup: func(providerKeeper *providerkeeper.Keeper, ctx sdk.Context, mocks *testkeeper.MockedKeepers) { + + providerKeeper.SetConsumerClientId(ctx, "chainID", "clientID") + + // Expect none of the client creation related calls to happen + mocks.MockStakingKeeper.EXPECT().UnbondingTime(gomock.Any()).Times(0) + mocks.MockClientKeeper.EXPECT().CreateClient(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + mocks.MockClientKeeper.EXPECT().GetSelfConsensusState(gomock.Any(), gomock.Any()).Times(0) + mocks.MockStakingKeeper.EXPECT().IterateLastValidatorPowers(gomock.Any(), gomock.Any()).Times(0) + + }, + expClientCreated: false, + }, + } + + for _, tc := range tests { + // Common setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + + // Test specific setup + tc.setup(&providerKeeper, ctx, &mocks) + + // Call method with same arbitrary values as defined above in mock expectations. + err := providerKeeper.CreateConsumerClient( + ctx, "chainID", clienttypes.NewHeight(4, 5), false) // LockUbdOnTimeout always false for now + + require.NoError(t, err) + + if tc.expClientCreated { + testCreatedConsumerClient(t, ctx, providerKeeper, "chainID", "clientID") + } + + // Assert mock calls from setup functions + ctrl.Finish() + } +} + +// Executes test assertions for a created consumer client. +// +// Note: Separated from TestCreateConsumerClient to also be called from TestCreateConsumerChainProposal. +func testCreatedConsumerClient(t *testing.T, + ctx sdk.Context, providerKeeper providerkeeper.Keeper, expectedChainID string, expectedClientID string) { + + // ClientID should be stored. + clientId, found := providerKeeper.GetConsumerClientId(ctx, expectedChainID) + require.True(t, found, "consumer client not found") + require.Equal(t, expectedClientID, clientId) + + // Lock unbonding on timeout flag always false for now. + lockUbdOnTimeout := providerKeeper.GetLockUnbondingOnTimeout(ctx, expectedChainID) + require.False(t, lockUbdOnTimeout) + + // Only assert that consumer genesis was set, + // more granular tests on consumer genesis should be defined in TestMakeConsumerGenesis + _, ok := providerKeeper.GetConsumerGenesis(ctx, expectedChainID) + require.True(t, ok) +} + +// TestPendingConsumerAdditionPropDeletion tests the getting/setting +// and deletion keeper methods for pending consumer addition props +func TestPendingConsumerAdditionPropDeletion(t *testing.T) { testCases := []struct { - types.ConsumerRemovalProposal + types.ConsumerAdditionProposal ExpDeleted bool }{ { - ConsumerRemovalProposal: types.ConsumerRemovalProposal{ChainId: "8", StopTime: time.Now().UTC()}, - ExpDeleted: true, + ConsumerAdditionProposal: types.ConsumerAdditionProposal{ChainId: "0", SpawnTime: time.Now().UTC()}, + ExpDeleted: true, }, { - ConsumerRemovalProposal: types.ConsumerRemovalProposal{ChainId: "9", StopTime: time.Now().UTC().Add(time.Hour)}, - ExpDeleted: false, + ConsumerAdditionProposal: types.ConsumerAdditionProposal{ChainId: "1", SpawnTime: time.Now().UTC().Add(time.Hour)}, + ExpDeleted: false, }, } - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() for _, tc := range testCases { - providerKeeper.SetPendingConsumerRemovalProp(ctx, tc.ChainId, tc.StopTime) + err := providerKeeper.SetPendingConsumerAdditionProp(ctx, &tc.ConsumerAdditionProposal) + require.NoError(t, err) } ctx = ctx.WithBlockTime(time.Now().UTC()) - propsToExecute := providerKeeper.ConsumerRemovalPropsToExecute(ctx) - // Delete consumer removal proposals, same as what would be done by IteratePendingConsumerRemovalProps - providerKeeper.DeletePendingConsumerRemovalProps(ctx, propsToExecute...) + propsToExecute := providerKeeper.ConsumerAdditionPropsToExecute(ctx) + // Delete consumer addition proposals, same as what would be done by IteratePendingConsumerAdditionProps + providerKeeper.DeletePendingConsumerAdditionProps(ctx, propsToExecute...) numDeleted := 0 for _, tc := range testCases { - res := providerKeeper.GetPendingConsumerRemovalProp(ctx, tc.ChainId, tc.StopTime) + res, found := providerKeeper.GetPendingConsumerAdditionProp(ctx, tc.SpawnTime, tc.ChainId) if !tc.ExpDeleted { - require.NotEmpty(t, res, "consumer removal prop was deleted: %s %s", tc.ChainId, tc.StopTime.String()) + require.True(t, found) + require.NotEmpty(t, res, "consumer addition proposal was deleted: %s %s", tc.ChainId, tc.SpawnTime.String()) continue } - require.Empty(t, res, "consumer removal prop was not deleted %s %s", tc.ChainId, tc.StopTime.String()) + require.Empty(t, res, "consumer addition proposal was not deleted %s %s", tc.ChainId, tc.SpawnTime.String()) require.Equal(t, propsToExecute[numDeleted].ChainId, tc.ChainId) numDeleted += 1 } } -// Tests that pending consumer removal proposals are accessed in order by timestamp via the iterator -func TestPendingConsumerRemovalPropOrder(t *testing.T) { +// TestPendingConsumerAdditionPropOrder tests that pending consumer addition proposals +// are accessed in order by timestamp via the iterator +func TestPendingConsumerAdditionPropOrder(t *testing.T) { now := time.Now().UTC() // props with unique chain ids and spawn times - sampleProp1 := types.ConsumerRemovalProposal{ChainId: "1", StopTime: now} - sampleProp2 := types.ConsumerRemovalProposal{ChainId: "2", StopTime: now.Add(1 * time.Hour)} - sampleProp3 := types.ConsumerRemovalProposal{ChainId: "3", StopTime: now.Add(2 * time.Hour)} - sampleProp4 := types.ConsumerRemovalProposal{ChainId: "4", StopTime: now.Add(3 * time.Hour)} - sampleProp5 := types.ConsumerRemovalProposal{ChainId: "5", StopTime: now.Add(4 * time.Hour)} + sampleProp1 := types.ConsumerAdditionProposal{ChainId: "1", SpawnTime: now} + sampleProp2 := types.ConsumerAdditionProposal{ChainId: "2", SpawnTime: now.Add(1 * time.Hour)} + sampleProp3 := types.ConsumerAdditionProposal{ChainId: "3", SpawnTime: now.Add(2 * time.Hour)} + sampleProp4 := types.ConsumerAdditionProposal{ChainId: "4", SpawnTime: now.Add(3 * time.Hour)} + sampleProp5 := types.ConsumerAdditionProposal{ChainId: "5", SpawnTime: now.Add(4 * time.Hour)} testCases := []struct { - propSubmitOrder []types.ConsumerRemovalProposal + propSubmitOrder []types.ConsumerAdditionProposal accessTime time.Time - expectedOrderedProps []types.ConsumerRemovalProposal + expectedOrderedProps []types.ConsumerAdditionProposal }{ { - propSubmitOrder: []types.ConsumerRemovalProposal{ + propSubmitOrder: []types.ConsumerAdditionProposal{ sampleProp1, sampleProp2, sampleProp3, sampleProp4, sampleProp5, }, accessTime: now.Add(30 * time.Minute), - expectedOrderedProps: []types.ConsumerRemovalProposal{ + expectedOrderedProps: []types.ConsumerAdditionProposal{ sampleProp1, }, }, { - propSubmitOrder: []types.ConsumerRemovalProposal{ + propSubmitOrder: []types.ConsumerAdditionProposal{ sampleProp3, sampleProp2, sampleProp1, sampleProp5, sampleProp4, }, accessTime: now.Add(3 * time.Hour).Add(30 * time.Minute), - expectedOrderedProps: []types.ConsumerRemovalProposal{ + expectedOrderedProps: []types.ConsumerAdditionProposal{ sampleProp1, sampleProp2, sampleProp3, sampleProp4, }, }, { - propSubmitOrder: []types.ConsumerRemovalProposal{ + propSubmitOrder: []types.ConsumerAdditionProposal{ sampleProp5, sampleProp4, sampleProp3, sampleProp2, sampleProp1, }, accessTime: now.Add(5 * time.Hour), - expectedOrderedProps: []types.ConsumerRemovalProposal{ + expectedOrderedProps: []types.ConsumerAdditionProposal{ sampleProp1, sampleProp2, sampleProp3, sampleProp4, sampleProp5, }, }, } for _, tc := range testCases { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() + ctx = ctx.WithBlockTime(tc.accessTime) for _, prop := range tc.propSubmitOrder { - providerKeeper.SetPendingConsumerRemovalProp(ctx, prop.ChainId, prop.StopTime) + err := providerKeeper.SetPendingConsumerAdditionProp(ctx, &prop) + require.NoError(t, err) } - propsToExecute := providerKeeper.ConsumerRemovalPropsToExecute(ctx) + propsToExecute := providerKeeper.ConsumerAdditionPropsToExecute(ctx) require.Equal(t, tc.expectedOrderedProps, propsToExecute) } } -func TestPendingConsumerAdditionPropDeletion(t *testing.T) { +// +// Consumer Chain Removal sub-protocol related tests of proposal.go +// + +// TestHandleConsumerRemovalProposal tests HandleConsumerRemovalProposal against its corresponding spec method. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-stccprop1 +// Spec tag: [CCV-PCF-STCCPROP.1] +func TestHandleConsumerRemovalProposal(t *testing.T) { + + type testCase struct { + description string + // Consumer removal proposal to handle + prop *types.ConsumerRemovalProposal + // Time when prop is handled + blockTime time.Time + // Whether consumer chain should have been stopped + expStop bool + } + + // Snapshot times asserted in tests + now := time.Now().UTC() + hourFromNow := now.Add(time.Hour).UTC() + + tests := []testCase{ + { + description: "valid proposal: stop time reached", + prop: providertypes.NewConsumerRemovalProposal( + "title", + "description", + "chainID", + now, + ).(*providertypes.ConsumerRemovalProposal), + blockTime: hourFromNow, // After stop time. + expStop: true, + }, + { + description: "valid proposal: stop time has not yet been reached", + prop: providertypes.NewConsumerRemovalProposal( + "title", + "description", + "chainID", + hourFromNow, + ).(*providertypes.ConsumerRemovalProposal), + blockTime: now, // Before proposal's stop time + expStop: false, + }, + } + + for _, tc := range tests { + + // Common setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + ctx = ctx.WithBlockTime(tc.blockTime) + + // Mock expectations and setup for stopping the consumer chain, if applicable + if tc.expStop { + testkeeper.SetupForStoppingConsumerChain(t, ctx, &providerKeeper, mocks) + } + // Note: when expStop is false, no mocks are setup, + // meaning no external keeper methods are allowed to be called. + + err := providerKeeper.HandleConsumerRemovalProposal(ctx, tc.prop) + require.NoError(t, err) + + if tc.expStop { + // Expect no pending proposal to exist + found := providerKeeper.GetPendingConsumerRemovalProp(ctx, tc.prop.ChainId, tc.prop.StopTime) + require.False(t, found) + + testConsumerStateIsCleaned(t, ctx, providerKeeper, tc.prop.ChainId, "channelID") + } else { + // Proposal should be stored as pending + found := providerKeeper.GetPendingConsumerRemovalProp(ctx, tc.prop.ChainId, tc.prop.StopTime) + require.True(t, found) + } + + // Assert mock calls from setup function + ctrl.Finish() + } +} + +// Tests the StopConsumerChain method against the spec, +// with more granularity than what's covered in TestHandleConsumerRemovalProposal, or e2e tests. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-stcc1 +// Spec tag: [CCV-PCF-STCC.1] +func TestStopConsumerChain(t *testing.T) { + type testCase struct { + description string + // State-mutating setup specific to this test case + setup func(sdk.Context, *providerkeeper.Keeper, testkeeper.MockedKeepers) + // Whether we should expect the method to return an error + expErr bool + } + + tests := []testCase{ + { + description: "fail due to an invalid unbonding index", + setup: func(ctx sdk.Context, providerKeeper *providerkeeper.Keeper, mocks testkeeper.MockedKeepers) { + // set invalid unbonding op index + providerKeeper.SetUnbondingOpIndex(ctx, "chainID", 0, []uint64{0}) + + // StopConsumerChain should return error, but state is still cleaned (asserted with mocks). + testkeeper.SetupForStoppingConsumerChain(t, ctx, providerKeeper, mocks) + }, + expErr: true, + }, + { + description: "proposal dropped, client doesn't exist", + setup: func(ctx sdk.Context, providerKeeper *providerkeeper.Keeper, mocks testkeeper.MockedKeepers) { + // No mocks, meaning no external keeper methods are allowed to be called. + }, + expErr: false, + }, + { + description: "valid stop of consumer chain, all mock calls hit", + setup: func(ctx sdk.Context, providerKeeper *providerkeeper.Keeper, mocks testkeeper.MockedKeepers) { + testkeeper.SetupForStoppingConsumerChain(t, ctx, providerKeeper, mocks) + }, + expErr: false, + }, + } + + for _, tc := range tests { + + // Common setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + + // Setup specific to test case + tc.setup(ctx, &providerKeeper, mocks) + + err := providerKeeper.StopConsumerChain(ctx, "chainID", false, true) + + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + testConsumerStateIsCleaned(t, ctx, providerKeeper, "chainID", "channelID") + + ctrl.Finish() + } +} + +// testConsumerStateIsCleaned executes test assertions for a stopped consumer chain's state being cleaned. +func testConsumerStateIsCleaned(t *testing.T, ctx sdk.Context, providerKeeper providerkeeper.Keeper, + expectedChainID string, expectedChannelID string) { + + _, found := providerKeeper.GetConsumerClientId(ctx, expectedChainID) + require.False(t, found) + found = providerKeeper.GetLockUnbondingOnTimeout(ctx, expectedChainID) + require.False(t, found) + _, found = providerKeeper.GetChainToChannel(ctx, expectedChainID) + require.False(t, found) + _, found = providerKeeper.GetChannelToChain(ctx, expectedChannelID) + require.False(t, found) + _, found = providerKeeper.GetInitChainHeight(ctx, expectedChainID) + require.False(t, found) + acks := providerKeeper.GetSlashAcks(ctx, expectedChainID) + require.Empty(t, acks) +} + +// TestPendingConsumerRemovalPropDeletion tests the getting/setting +// and deletion methods for pending consumer removal props +func TestPendingConsumerRemovalPropDeletion(t *testing.T) { testCases := []struct { - types.ConsumerAdditionProposal + types.ConsumerRemovalProposal ExpDeleted bool }{ { - ConsumerAdditionProposal: types.ConsumerAdditionProposal{ChainId: "0", SpawnTime: time.Now().UTC()}, - ExpDeleted: true, + ConsumerRemovalProposal: types.ConsumerRemovalProposal{ChainId: "8", StopTime: time.Now().UTC()}, + ExpDeleted: true, }, { - ConsumerAdditionProposal: types.ConsumerAdditionProposal{ChainId: "1", SpawnTime: time.Now().UTC().Add(time.Hour)}, - ExpDeleted: false, + ConsumerRemovalProposal: types.ConsumerRemovalProposal{ChainId: "9", StopTime: time.Now().UTC().Add(time.Hour)}, + ExpDeleted: false, }, } - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() for _, tc := range testCases { - err := providerKeeper.SetPendingConsumerAdditionProp(ctx, &tc.ConsumerAdditionProposal) - require.NoError(t, err) + providerKeeper.SetPendingConsumerRemovalProp(ctx, tc.ChainId, tc.StopTime) } ctx = ctx.WithBlockTime(time.Now().UTC()) - propsToExecute := providerKeeper.ConsumerAdditionPropsToExecute(ctx) - // Delete consumer addition proposals, same as what would be done by IteratePendingConsumerAdditionProps - providerKeeper.DeletePendingConsumerAdditionProps(ctx, propsToExecute...) + propsToExecute := providerKeeper.ConsumerRemovalPropsToExecute(ctx) + // Delete consumer removal proposals, same as what would be done by IteratePendingConsumerRemovalProps + providerKeeper.DeletePendingConsumerRemovalProps(ctx, propsToExecute...) numDeleted := 0 for _, tc := range testCases { - res := providerKeeper.GetPendingConsumerAdditionProp(ctx, tc.SpawnTime, tc.ChainId) + res := providerKeeper.GetPendingConsumerRemovalProp(ctx, tc.ChainId, tc.StopTime) if !tc.ExpDeleted { - require.NotEmpty(t, res, "consumer addition proposal was deleted: %s %s", tc.ChainId, tc.SpawnTime.String()) + require.NotEmpty(t, res, "consumer removal prop was deleted: %s %s", tc.ChainId, tc.StopTime.String()) continue } - require.Empty(t, res, "consumer addition proposal was not deleted %s %s", tc.ChainId, tc.SpawnTime.String()) + require.Empty(t, res, "consumer removal prop was not deleted %s %s", tc.ChainId, tc.StopTime.String()) require.Equal(t, propsToExecute[numDeleted].ChainId, tc.ChainId) numDeleted += 1 } } -// Tests that pending consumer addition proposals are accessed in order by timestamp via the iterator -func TestPendingConsumerAdditionPropOrder(t *testing.T) { +// Tests that pending consumer removal proposals are accessed in order by timestamp via the iterator +func TestPendingConsumerRemovalPropOrder(t *testing.T) { now := time.Now().UTC() // props with unique chain ids and spawn times - sampleProp1 := types.ConsumerAdditionProposal{ChainId: "1", SpawnTime: now} - sampleProp2 := types.ConsumerAdditionProposal{ChainId: "2", SpawnTime: now.Add(1 * time.Hour)} - sampleProp3 := types.ConsumerAdditionProposal{ChainId: "3", SpawnTime: now.Add(2 * time.Hour)} - sampleProp4 := types.ConsumerAdditionProposal{ChainId: "4", SpawnTime: now.Add(3 * time.Hour)} - sampleProp5 := types.ConsumerAdditionProposal{ChainId: "5", SpawnTime: now.Add(4 * time.Hour)} + sampleProp1 := types.ConsumerRemovalProposal{ChainId: "1", StopTime: now} + sampleProp2 := types.ConsumerRemovalProposal{ChainId: "2", StopTime: now.Add(1 * time.Hour)} + sampleProp3 := types.ConsumerRemovalProposal{ChainId: "3", StopTime: now.Add(2 * time.Hour)} + sampleProp4 := types.ConsumerRemovalProposal{ChainId: "4", StopTime: now.Add(3 * time.Hour)} + sampleProp5 := types.ConsumerRemovalProposal{ChainId: "5", StopTime: now.Add(4 * time.Hour)} testCases := []struct { - propSubmitOrder []types.ConsumerAdditionProposal + propSubmitOrder []types.ConsumerRemovalProposal accessTime time.Time - expectedOrderedProps []types.ConsumerAdditionProposal + expectedOrderedProps []types.ConsumerRemovalProposal }{ { - propSubmitOrder: []types.ConsumerAdditionProposal{ + propSubmitOrder: []types.ConsumerRemovalProposal{ sampleProp1, sampleProp2, sampleProp3, sampleProp4, sampleProp5, }, accessTime: now.Add(30 * time.Minute), - expectedOrderedProps: []types.ConsumerAdditionProposal{ + expectedOrderedProps: []types.ConsumerRemovalProposal{ sampleProp1, }, }, { - propSubmitOrder: []types.ConsumerAdditionProposal{ + propSubmitOrder: []types.ConsumerRemovalProposal{ sampleProp3, sampleProp2, sampleProp1, sampleProp5, sampleProp4, }, accessTime: now.Add(3 * time.Hour).Add(30 * time.Minute), - expectedOrderedProps: []types.ConsumerAdditionProposal{ + expectedOrderedProps: []types.ConsumerRemovalProposal{ sampleProp1, sampleProp2, sampleProp3, sampleProp4, }, }, { - propSubmitOrder: []types.ConsumerAdditionProposal{ + propSubmitOrder: []types.ConsumerRemovalProposal{ sampleProp5, sampleProp4, sampleProp3, sampleProp2, sampleProp1, }, accessTime: now.Add(5 * time.Hour), - expectedOrderedProps: []types.ConsumerAdditionProposal{ + expectedOrderedProps: []types.ConsumerRemovalProposal{ sampleProp1, sampleProp2, sampleProp3, sampleProp4, sampleProp5, }, }, } for _, tc := range testCases { - providerKeeper, ctx, ctrl := testkeeper.GetProviderKeeperAndCtx(t) + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() - ctx = ctx.WithBlockTime(tc.accessTime) for _, prop := range tc.propSubmitOrder { - err := providerKeeper.SetPendingConsumerAdditionProp(ctx, &prop) - require.NoError(t, err) + providerKeeper.SetPendingConsumerRemovalProp(ctx, prop.ChainId, prop.StopTime) } - propsToExecute := providerKeeper.ConsumerAdditionPropsToExecute(ctx) + propsToExecute := providerKeeper.ConsumerRemovalPropsToExecute(ctx) require.Equal(t, tc.expectedOrderedProps, propsToExecute) } } + +// TestMakeConsumerGenesis tests the MakeConsumerGenesis keeper method +// +// Note: the initial intention of this test wasn't very clear, it was migrated with best effort +func TestMakeConsumerGenesis(t *testing.T) { + + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState( + &ibctmtypes.ClientState{ + TrustLevel: ibctmtypes.DefaultTrustLevel, + MaxClockDrift: 10000000000, + ProofSpecs: []*_go.ProofSpec{ + { + LeafSpec: &_go.LeafOp{ + Hash: _go.HashOp_SHA256, + PrehashKey: _go.HashOp_NO_HASH, + PrehashValue: _go.HashOp_SHA256, + Length: _go.LengthOp_VAR_PROTO, + Prefix: []byte{0x00}, + }, + InnerSpec: &_go.InnerSpec{ + ChildOrder: []int32{0, 1}, + ChildSize: 33, + MinPrefixLength: 4, + MaxPrefixLength: 12, + Hash: _go.HashOp_SHA256, + }, + MaxDepth: 0, + MinDepth: 0, + }, + { + LeafSpec: &_go.LeafOp{ + Hash: _go.HashOp_SHA256, + PrehashKey: _go.HashOp_NO_HASH, + PrehashValue: _go.HashOp_SHA256, + Length: _go.LengthOp_VAR_PROTO, + Prefix: []byte{0x00}, + }, + InnerSpec: &_go.InnerSpec{ + ChildOrder: []int32{0, 1}, + ChildSize: 32, + MinPrefixLength: 1, + MaxPrefixLength: 1, + Hash: _go.HashOp_SHA256, + }, + MaxDepth: 0, + }, + }, + UpgradePath: []string{"upgrade", "upgradedIBCState"}, + AllowUpdateAfterExpiry: true, + AllowUpdateAfterMisbehaviour: true, + }, + ) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + defer ctrl.Finish() + + // + // Other setup not covered by custom template client state + // + ctx = ctx.WithChainID("testchain1") // chainID is obtained from ctx + ctx = ctx.WithBlockHeight(5) // RevisionHeight obtained from ctx + gomock.InOrder(testkeeper.GetMocksForMakeConsumerGenesis(ctx, &mocks, 1814400000000000)...) + + actualGenesis, err := providerKeeper.MakeConsumerGenesis(ctx) + require.NoError(t, err) + + jsonString := `{"params":{"enabled":true, "blocks_per_distribution_transmission":1000, "lock_unbonding_on_timeout": false},"new_chain":true,"provider_client_state":{"chain_id":"testchain1","trust_level":{"numerator":1,"denominator":3},"trusting_period":907200000000000,"unbonding_period":1814400000000000,"max_clock_drift":10000000000,"frozen_height":{},"latest_height":{"revision_height":5},"proof_specs":[{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":33,"min_prefix_length":4,"max_prefix_length":12,"hash":1}},{"leaf_spec":{"hash":1,"prehash_value":1,"length":1,"prefix":"AA=="},"inner_spec":{"child_order":[0,1],"child_size":32,"min_prefix_length":1,"max_prefix_length":1,"hash":1}}],"upgrade_path":["upgrade","upgradedIBCState"],"allow_update_after_expiry":true,"allow_update_after_misbehaviour":true},"provider_consensus_state":{"timestamp":"2020-01-02T00:00:10Z","root":{"hash":"LpGpeyQVLUo9HpdsgJr12NP2eCICspcULiWa5u9udOA="},"next_validators_hash":"E30CE736441FB9101FADDAF7E578ABBE6DFDB67207112350A9A904D554E1F5BE"},"unbonding_sequences":null,"initial_val_set":[{"pub_key":{"type":"tendermint/PubKeyEd25519","value":"dcASx5/LIKZqagJWN0frOlFtcvz91frYmj/zmoZRWro="},"power":1}]}` + + var expectedGenesis consumertypes.GenesisState + err = json.Unmarshal([]byte(jsonString), &expectedGenesis) + require.NoError(t, err) + + // Zeroing out different fields that are challenging to mock + actualGenesis.InitialValSet = []abci.ValidatorUpdate{} + expectedGenesis.InitialValSet = []abci.ValidatorUpdate{} + actualGenesis.ProviderConsensusState = &ibctmtypes.ConsensusState{} + expectedGenesis.ProviderConsensusState = &ibctmtypes.ConsensusState{} + + require.Equal(t, actualGenesis, expectedGenesis, "consumer chain genesis created incorrectly") +} + +// TestBeginBlockInit directly tests BeginBlockInit against the spec using helpers defined above. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-bblock-init1 +// Spec tag:[CCV-PCF-BBLOCK-INIT.1] +func TestBeginBlockInit(t *testing.T) { + + now := time.Now().UTC() + + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + defer ctrl.Finish() + ctx = ctx.WithBlockTime(now) + + pendingProps := []*providertypes.ConsumerAdditionProposal{ + providertypes.NewConsumerAdditionProposal( + "title", "description", "chain1", clienttypes.NewHeight(3, 4), []byte{}, []byte{}, + now.Add(-time.Hour).UTC()).(*providertypes.ConsumerAdditionProposal), + providertypes.NewConsumerAdditionProposal( + "title", "description", "chain2", clienttypes.NewHeight(3, 4), []byte{}, []byte{}, + now.UTC()).(*providertypes.ConsumerAdditionProposal), + providertypes.NewConsumerAdditionProposal( + "title", "description", "chain3", clienttypes.NewHeight(3, 4), []byte{}, []byte{}, + now.Add(time.Hour).UTC()).(*providertypes.ConsumerAdditionProposal), + } + + gomock.InOrder( + // Expect client creation for the 1st and second proposals (spawn time already passed) + append(testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain1", clienttypes.NewHeight(3, 4)), + testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chain2", clienttypes.NewHeight(3, 4))...)..., + ) + + for _, prop := range pendingProps { + err := providerKeeper.SetPendingConsumerAdditionProp(ctx, prop) + require.NoError(t, err) + } + + providerKeeper.BeginBlockInit(ctx) + + // Only the 3rd (final) proposal is still stored as pending + _, found := providerKeeper.GetPendingConsumerAdditionProp( + ctx, pendingProps[0].SpawnTime, pendingProps[0].ChainId) + require.False(t, found) + _, found = providerKeeper.GetPendingConsumerAdditionProp( + ctx, pendingProps[1].SpawnTime, pendingProps[1].ChainId) + require.False(t, found) + _, found = providerKeeper.GetPendingConsumerAdditionProp( + ctx, pendingProps[2].SpawnTime, pendingProps[2].ChainId) + require.True(t, found) +} + +// TestBeginBlockCCR tests BeginBlockCCR against the spec. +// +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-bblock-ccr1 +// Spec tag: [CCV-PCF-BBLOCK-CCR.1] +func TestBeginBlockCCR(t *testing.T) { + now := time.Now().UTC() + + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + defer ctrl.Finish() + ctx = ctx.WithBlockTime(now) + + pendingProps := []*providertypes.ConsumerRemovalProposal{ + providertypes.NewConsumerRemovalProposal( + "title", "description", "chain1", now.Add(-time.Hour).UTC(), + ).(*providertypes.ConsumerRemovalProposal), + providertypes.NewConsumerRemovalProposal( + "title", "description", "chain2", now, + ).(*providertypes.ConsumerRemovalProposal), + providertypes.NewConsumerRemovalProposal( + "title", "description", "chain3", now.Add(time.Hour).UTC(), + ).(*providertypes.ConsumerRemovalProposal), + } + + // + // Mock expectations + // + expectations := []*gomock.Call{} + for _, prop := range pendingProps { + // A consumer chain is setup corresponding to each prop, making these mocks necessary + expectations = append(expectations, testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, + prop.ChainId, clienttypes.NewHeight(2, 3))...) + expectations = append(expectations, testkeeper.GetMocksForSetConsumerChain(ctx, &mocks, prop.ChainId)...) + } + // Only first two consumer chains should be stopped + expectations = append(expectations, testkeeper.GetMocksForStopConsumerChain(ctx, &mocks)...) + expectations = append(expectations, testkeeper.GetMocksForStopConsumerChain(ctx, &mocks)...) + + gomock.InOrder(expectations...) + + // + // Remaining setup + // + for _, prop := range pendingProps { + // Setup a valid consumer chain for each prop + err := providerKeeper.CreateConsumerClient(ctx, prop.ChainId, clienttypes.NewHeight(2, 3), false) + require.NoError(t, err) + err = providerKeeper.SetConsumerChain(ctx, "channelID") + require.NoError(t, err) + + // Set removal props for all consumer chains + providerKeeper.SetPendingConsumerRemovalProp(ctx, prop.ChainId, prop.StopTime) + } + + // + // Test execution + // + providerKeeper.BeginBlockCCR(ctx) + + // Only the 3rd (final) proposal is still stored as pending + found := providerKeeper.GetPendingConsumerRemovalProp( + ctx, pendingProps[0].ChainId, pendingProps[0].StopTime) + require.False(t, found) + found = providerKeeper.GetPendingConsumerRemovalProp( + ctx, pendingProps[1].ChainId, pendingProps[1].StopTime) + require.False(t, found) + found = providerKeeper.GetPendingConsumerRemovalProp( + ctx, pendingProps[2].ChainId, pendingProps[2].StopTime) + require.True(t, found) +} diff --git a/x/ccv/provider/module.go b/x/ccv/provider/module.go index e5be2fcaf3..8b6721f3d4 100644 --- a/x/ccv/provider/module.go +++ b/x/ccv/provider/module.go @@ -128,12 +128,15 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { providertypes.RegisterQueryServer(cfg.QueryServer(), am.keeper) } -// InitGenesis performs genesis initialization for the provider module. It returns -// no validator updates. +// InitGenesis performs genesis initialization for the provider module. It returns no validator updates. +// Note: This method along with ValidateGenesis satisfies the CCV spec: +// https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-initg1 func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { var genesisState providertypes.GenesisState cdc.MustUnmarshalJSON(data, &genesisState) + am.keeper.InitGenesis(ctx, &genesisState) + // initialize validator update id // TODO: Include in genesis and initialize from genesis value am.keeper.SetValidatorSetUpdateId(ctx, 1) @@ -152,10 +155,10 @@ func (AppModule) ConsensusVersion() uint64 { return 1 } // BeginBlock implements the AppModule interface func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - // Check if there are any consumer chains that are due to be spawned - am.keeper.IteratePendingConsumerAdditionProps(ctx) - // Check if there are any consumer chains that are due to be stopped - am.keeper.IteratePendingConsumerRemovalProps(ctx) + // Create clients to consumer chains that are due to be spawned via pending consumer addition proposals + am.keeper.BeginBlockInit(ctx) + // Stop and remove state for any consumer chains that are due to be stopped via pending consumer removal proposals + am.keeper.BeginBlockCCR(ctx) } // EndBlock implements the AppModule interface diff --git a/x/ccv/provider/module_test.go b/x/ccv/provider/module_test.go new file mode 100644 index 0000000000..c7f504a125 --- /dev/null +++ b/x/ccv/provider/module_test.go @@ -0,0 +1,157 @@ +package provider_test + +import ( + "testing" + + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + host "github.com/cosmos/ibc-go/v3/modules/core/24-host" + testkeeper "github.com/cosmos/interchain-security/testutil/keeper" + "github.com/cosmos/interchain-security/x/ccv/provider" + "github.com/cosmos/interchain-security/x/ccv/provider/types" + ccv "github.com/cosmos/interchain-security/x/ccv/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/golang/mock/gomock" +) + +// Tests the provider's InitGenesis implementation against the spec. +// See: https://github.com/cosmos/ibc/blob/main/spec/app/ics-028-cross-chain-validation/methods.md#ccv-pcf-initg1 +// Spec tag: [CCV-PCF-INITG.1] +// +// Note: Genesis validation for the provider is tested in TestValidateGenesisState +func TestInitGenesis(t *testing.T) { + + type testCase struct { + name string + // Whether port capability is already bound to the CCV provider module + isBound bool + // Provider's storage of consumer state to test against + consumerStates []types.ConsumerState + // Error returned from ClaimCapability during port binding, default: nil + errFromClaimCap error + // Whether method call should panic, default: false + expPanic bool + } + + tests := []testCase{ + { + name: "already bound port, no consumer states", + isBound: true, + consumerStates: []types.ConsumerState{}, + }, + { + name: "no bound port, no consumer states", + isBound: false, + consumerStates: []types.ConsumerState{}, + }, + { + name: "no bound port, multiple consumer states", + isBound: false, + consumerStates: []types.ConsumerState{ + { + ChainId: "chainId1", + ChannelId: "channelIdToChain1", + }, + { + ChainId: "chainId2", + ChannelId: "channelIdToChain2", + }, + { + ChainId: "chainId3", + ChannelId: "channelIdToChain3", + }, + }, + }, + { + name: "already bound port, one consumer state", + isBound: true, + consumerStates: []types.ConsumerState{ + { + ChainId: "chainId77", + ChannelId: "channelIdToChain77", + }, + }, + }, + { + name: "capability not owned, method should panic", + isBound: false, + consumerStates: []types.ConsumerState{ + { + ChainId: "chainId77", + ChannelId: "channelIdToChain77", + }, + }, + errFromClaimCap: capabilitytypes.ErrCapabilityNotOwned, + expPanic: true, + }, + } + + for _, tc := range tests { + // + // Setup + // + keeperParams := testkeeper.NewInMemKeeperParams(t) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + + appModule := provider.NewAppModule(&providerKeeper) + genState := types.NewGenesisState(tc.consumerStates, types.DefaultParams()) + + cdc := keeperParams.Cdc + jsonBytes := cdc.MustMarshalJSON(genState) + + // + // Assert mocked logic before method executes + // + orderedCalls := []*gomock.Call{ + mocks.MockScopedKeeper.EXPECT().GetCapability( + ctx, host.PortPath(ccv.ProviderPortID), + ).Return( + &capabilitytypes.Capability{}, + tc.isBound, // Capability is returned successfully if port capability is already bound to this module. + ), + } + + // If port capability is not already bound, port will be bound and capability claimed. + if !tc.isBound { + dummyCap := &capabilitytypes.Capability{} + + orderedCalls = append(orderedCalls, + mocks.MockPortKeeper.EXPECT().BindPort(ctx, ccv.ProviderPortID).Return(dummyCap), + mocks.MockScopedKeeper.EXPECT().ClaimCapability( + ctx, dummyCap, host.PortPath(ccv.ProviderPortID)).Return(tc.errFromClaimCap), + ) + } + + gomock.InOrder(orderedCalls...) + + // + // Execute method, then assert expected results + // + if tc.expPanic { + require.Panics(t, assert.PanicTestFunc(func() { + appModule.InitGenesis(ctx, cdc, jsonBytes) + }), tc.name) + continue // Nothing else to verify + } + + valUpdates := appModule.InitGenesis(ctx, cdc, jsonBytes) + + numStatesCounted := 0 + for _, state := range tc.consumerStates { + numStatesCounted += 1 + channelID, found := providerKeeper.GetChainToChannel(ctx, state.ChainId) + require.True(t, found) + require.Equal(t, state.ChannelId, channelID) + + chainID, found := providerKeeper.GetChannelToChain(ctx, state.ChannelId) + require.True(t, found) + require.Equal(t, state.ChainId, chainID) + } + require.Equal(t, len(tc.consumerStates), numStatesCounted) + + require.Empty(t, valUpdates, "InitGenesis should return no validator updates") + + ctrl.Finish() + } +} diff --git a/x/ccv/provider/proposal_handler_test.go b/x/ccv/provider/proposal_handler_test.go new file mode 100644 index 0000000000..92c806a1ed --- /dev/null +++ b/x/ccv/provider/proposal_handler_test.go @@ -0,0 +1,90 @@ +package provider_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + + "testing" + "time" + + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + testkeeper "github.com/cosmos/interchain-security/testutil/keeper" + "github.com/cosmos/interchain-security/x/ccv/provider" + "github.com/cosmos/interchain-security/x/ccv/provider/types" +) + +// TestConsumerChainProposalHandler tests the highest level handler for proposals concerning both +// creating and stopping consumer chains. +func TestConsumerChainProposalHandler(t *testing.T) { + + // Snapshot times asserted in tests + now := time.Now().UTC() + hourFromNow := now.Add(time.Hour).UTC() + + testCases := []struct { + name string + content govtypes.Content + blockTime time.Time + expValidConsumerAddition bool + expValidConsumerRemoval bool + }{ + { + name: "valid consumer addition proposal", + content: types.NewConsumerAdditionProposal( + "title", "description", "chainID", + clienttypes.NewHeight(2, 3), []byte("gen_hash"), []byte("bin_hash"), now), + blockTime: hourFromNow, // ctx blocktime is after proposal's spawn time + expValidConsumerAddition: true, + }, + { + name: "valid consumer removal proposal", + content: types.NewConsumerRemovalProposal( + "title", "description", "chainID", now), + blockTime: hourFromNow, + expValidConsumerRemoval: true, + }, + { + name: "nil proposal", + content: nil, + blockTime: hourFromNow, + }, + { + name: "unsupported proposal type", + content: distributiontypes.NewCommunityPoolSpendProposal( + "title", "desc", []byte{}, + sdk.NewCoins(sdk.NewCoin("communityfunds", sdk.NewInt(10)))), + }, + } + + for _, tc := range testCases { + + // Setup + keeperParams := testkeeper.NewInMemKeeperParams(t) + keeperParams.SetTemplateClientState(nil) + providerKeeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) + ctx = ctx.WithBlockTime(tc.blockTime) + + // Mock expectations depending on expected outcome + if tc.expValidConsumerAddition { + gomock.InOrder(testkeeper.GetMocksForCreateConsumerClient(ctx, &mocks, "chainID", clienttypes.NewHeight(2, 3))...) + } + if tc.expValidConsumerRemoval { + testkeeper.SetupForStoppingConsumerChain(t, ctx, &providerKeeper, mocks) + } + + // Execution + proposalHandler := provider.NewConsumerChainProposalHandler(providerKeeper) + err := proposalHandler(ctx, tc.content) + + if tc.expValidConsumerAddition || tc.expValidConsumerRemoval { + require.NoError(t, err) + } else { + require.Error(t, err) + } + ctrl.Finish() + } +} diff --git a/x/ccv/provider/types/genesis.pb.go b/x/ccv/provider/types/genesis.pb.go index 180e5788d9..59ba6ebd23 100644 --- a/x/ccv/provider/types/genesis.pb.go +++ b/x/ccv/provider/types/genesis.pb.go @@ -79,7 +79,9 @@ func (m *GenesisState) GetParams() Params { // ConsumerState defines the state that the provider chain stores for each consumer chain type ConsumerState struct { + // The provider's identifier for this consumer chain. ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + // The provider's channel identifier to this consumer chain. ChannelId string `protobuf:"bytes,2,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` } diff --git a/x/ccv/provider/types/genesis_test.go b/x/ccv/provider/types/genesis_test.go index d6cf64926d..8b0276dcbd 100644 --- a/x/ccv/provider/types/genesis_test.go +++ b/x/ccv/provider/types/genesis_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" ) +// Tests validation of consumer states and params within a provider genesis state func TestValidateGenesisState(t *testing.T) { testCases := []struct { name string