diff --git a/modules/apps/27-interchain-accounts/ibc_module.go b/modules/apps/27-interchain-accounts/ibc_module.go index abc488af67a..ce5af58f4c8 100644 --- a/modules/apps/27-interchain-accounts/ibc_module.go +++ b/modules/apps/27-interchain-accounts/ibc_module.go @@ -1,20 +1,17 @@ package interchain_accounts import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/keeper" - "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/types" channeltypes "github.com/cosmos/ibc-go/v2/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v2/modules/core/05-port/types" ibcexported "github.com/cosmos/ibc-go/v2/modules/core/exported" ) -// IBCModule implements the ICS26 callbacks for interchain accounts given the +// IBCModule implements the ICS26 interface for interchain accounts given the // interchain account keeper and underlying application. type IBCModule struct { keeper keeper.Keeper @@ -29,7 +26,13 @@ func NewIBCModule(k keeper.Keeper, app porttypes.IBCModule) IBCModule { } } -// OnChanOpenInit implements the IBCModule interface +// OnChanOpenInit implements the IBCModule interface. Interchain Accounts is +// implemented to act as middleware for connected authentication modules on +// the controller side. The connected modules may not change the controller side portID or +// version. They will be allowed to perform custom logic without changing +// the parameters stored within a channel struct. +// +// Controller Chain func (im IBCModule) OnChanOpenInit( ctx sdk.Context, order channeltypes.Order, @@ -40,10 +43,18 @@ func (im IBCModule) OnChanOpenInit( counterparty channeltypes.Counterparty, version string, ) error { - return im.keeper.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version) + if err := im.keeper.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version); err != nil { + return err + } + + // call underlying app's OnChanOpenInit callback with the appVersion + return im.app.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, + chanCap, counterparty, version) } // OnChanOpenTry implements the IBCModule interface +// +// Host Chain func (im IBCModule) OnChanOpenTry( ctx sdk.Context, order channeltypes.Order, @@ -58,17 +69,30 @@ func (im IBCModule) OnChanOpenTry( return im.keeper.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version, counterpartyVersion) } -// OnChanOpenAck implements the IBCModule interface +// OnChanOpenAck implements the IBCModule interface. Interchain Accounts is +// implemented to act as middleware for connected authentication modules on +// the controller side. The connected modules may not change the portID or +// version. They will be allowed to perform custom logic without changing +// the parameters stored within a channel struct. +// +// Controller Chain func (im IBCModule) OnChanOpenAck( ctx sdk.Context, portID, channelID string, counterpartyVersion string, ) error { - return im.keeper.OnChanOpenAck(ctx, portID, channelID, counterpartyVersion) + if err := im.keeper.OnChanOpenAck(ctx, portID, channelID, counterpartyVersion); err != nil { + return err + } + + // call underlying app's OnChanOpenAck callback with the counterparty app version. + return im.app.OnChanOpenAck(ctx, portID, channelID, counterpartyVersion) } -// OnChanOpenConfirm implements the IBCModule interface +// OnChanOpenAck implements the IBCModule interface +// +// Host Chain func (im IBCModule) OnChanOpenConfirm( ctx sdk.Context, portID, @@ -97,6 +121,8 @@ func (im IBCModule) OnChanCloseConfirm( } // OnRecvPacket implements the IBCModule interface +// +// Host Chain func (im IBCModule) OnRecvPacket( ctx sdk.Context, packet channeltypes.Packet, @@ -104,11 +130,6 @@ func (im IBCModule) OnRecvPacket( ) ibcexported.Acknowledgement { ack := channeltypes.NewResultAcknowledgement([]byte{byte(1)}) - var data types.InterchainAccountPacketData - if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { - ack = channeltypes.NewErrorAcknowledgement(fmt.Sprintf("cannot unmarshal ICS-27 interchain account packet data: %s", err.Error())) - } - // only attempt the application logic if the packet data // was successfully decoded if ack.Success() { @@ -123,36 +144,31 @@ func (im IBCModule) OnRecvPacket( } // OnAcknowledgementPacket implements the IBCModule interface +// +// Controller Chain func (im IBCModule) OnAcknowledgementPacket( ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, - _ sdk.AccAddress, + relayer sdk.AccAddress, ) error { - var ack channeltypes.Acknowledgement - - if err := types.ModuleCdc.UnmarshalJSON(acknowledgement, &ack); err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal ICS-27 interchain account packet acknowledgment: %v", err) - } - var data types.InterchainAccountPacketData - if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal ICS-27 interchain account packet data: %s", err.Error()) - } - - if err := im.keeper.OnAcknowledgementPacket(ctx, packet, data, ack); err != nil { - return err - } - - return nil + // call underlying app's OnAcknowledgementPacket callback. + return im.app.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) } // OnTimeoutPacket implements the IBCModule interface +// +// Controller Chain func (im IBCModule) OnTimeoutPacket( ctx sdk.Context, packet channeltypes.Packet, - _ sdk.AccAddress, + relayer sdk.AccAddress, ) error { - return im.keeper.OnTimeoutPacket(ctx, packet) + if err := im.keeper.OnTimeoutPacket(ctx, packet); err != nil { + return err + } + + return im.app.OnTimeoutPacket(ctx, packet, relayer) } // NegotiateAppVersion implements the IBCModule interface diff --git a/modules/apps/27-interchain-accounts/ibc_module_test.go b/modules/apps/27-interchain-accounts/ibc_module_test.go new file mode 100644 index 00000000000..ca010b6ede1 --- /dev/null +++ b/modules/apps/27-interchain-accounts/ibc_module_test.go @@ -0,0 +1,539 @@ +package interchain_accounts_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + "github.com/stretchr/testify/suite" + "github.com/tendermint/tendermint/crypto" + + "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/types" + clienttypes "github.com/cosmos/ibc-go/v2/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v2/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v2/modules/core/24-host" + "github.com/cosmos/ibc-go/v2/modules/core/exported" + ibctesting "github.com/cosmos/ibc-go/v2/testing" +) + +var ( + // TestAccAddress defines a resuable bech32 address for testing purposes + // TODO: update crypto.AddressHash() when sdk uses address.Module() + TestAccAddress = types.GenerateAddress(sdk.AccAddress(crypto.AddressHash([]byte(types.ModuleName))), TestPortID) + // TestOwnerAddress defines a reusable bech32 address for testing purposes + TestOwnerAddress = "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs" + // TestPortID defines a resuable port identifier for testing purposes + TestPortID, _ = types.GeneratePortID(TestOwnerAddress, "connection-0", "connection-0") + // TestVersion defines a resuable interchainaccounts version string for testing purposes + TestVersion = types.NewAppVersion(types.VersionPrefix, TestAccAddress.String()) +) + +type InterchainAccountsTestSuite struct { + suite.Suite + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain +} + +func TestICATestSuite(t *testing.T) { + suite.Run(t, new(InterchainAccountsTestSuite)) +} + +func (suite *InterchainAccountsTestSuite) SetupTest() { + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(0)) + suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func NewICAPath(chainA, chainB *ibctesting.TestChain) *ibctesting.Path { + path := ibctesting.NewPath(chainA, chainB) + path.EndpointA.ChannelConfig.PortID = types.PortID + path.EndpointB.ChannelConfig.PortID = types.PortID + path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED + path.EndpointB.ChannelConfig.Order = channeltypes.ORDERED + path.EndpointA.ChannelConfig.Version = types.VersionPrefix + path.EndpointB.ChannelConfig.Version = TestVersion + + return path +} + +func InitInterchainAccount(endpoint *ibctesting.Endpoint, owner string) error { + portID, err := types.GeneratePortID(owner, endpoint.ConnectionID, endpoint.Counterparty.ConnectionID) + if err != nil { + return err + } + channelSequence := endpoint.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(endpoint.Chain.GetContext()) + + if err := endpoint.Chain.GetSimApp().ICAKeeper.InitInterchainAccount(endpoint.Chain.GetContext(), endpoint.ConnectionID, endpoint.Counterparty.ConnectionID, owner); err != nil { + return err + } + + // commit state changes for proof verification + endpoint.Chain.App.Commit() + endpoint.Chain.NextBlock() + + // update port/channel ids + endpoint.ChannelID = channeltypes.FormatChannelIdentifier(channelSequence) + endpoint.ChannelConfig.PortID = portID + return nil +} + +// SetupICAPath invokes the InterchainAccounts entrypoint and subsequent channel handshake handlers +func SetupICAPath(path *ibctesting.Path, owner string) error { + if err := InitInterchainAccount(path.EndpointA, owner); err != nil { + return err + } + + if err := path.EndpointB.ChanOpenTry(); err != nil { + return err + } + + if err := path.EndpointA.ChanOpenAck(); err != nil { + return err + } + + if err := path.EndpointB.ChanOpenConfirm(); err != nil { + return err + } + + return nil +} + +func (suite *InterchainAccountsTestSuite) TestOnChanOpenInit() { + var ( + channel *channeltypes.Channel + ) + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", func() {}, true, + }, + { + "ICA OnChanOpenInit fails - UNORDERED channel", func() { + channel.Ordering = channeltypes.UNORDERED + }, false, + }, + { + "ICA auth module callback fails", func() { + suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenInit = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string, + portID, channelID string, chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, version string, + ) error { + return fmt.Errorf("mock ica auth fails") + } + }, false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() // reset + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + // mock init interchain account + portID, err := types.GeneratePortID(TestOwnerAddress, path.EndpointA.ConnectionID, path.EndpointB.ConnectionID) + suite.Require().NoError(err) + portCap := suite.chainA.GetSimApp().IBCKeeper.PortKeeper.BindPort(suite.chainA.GetContext(), portID) + suite.chainA.GetSimApp().ICAKeeper.ClaimCapability(suite.chainA.GetContext(), portCap, host.PortPath(portID)) + + path.EndpointA.ChannelConfig.PortID = portID + path.EndpointA.ChannelID = ibctesting.FirstChannelID + + // default values + counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID) + channel = &channeltypes.Channel{ + State: channeltypes.INIT, + Ordering: channeltypes.ORDERED, + Counterparty: counterparty, + ConnectionHops: []string{path.EndpointA.ConnectionID}, + Version: types.VersionPrefix, + } + + tc.malleate() + + // ensure channel on chainA is set in state + suite.chainA.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, *channel) + + module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID) + suite.Require().NoError(err) + + chanCap, err := suite.chainA.App.GetScopedIBCKeeper().NewCapability(suite.chainA.GetContext(), host.ChannelCapabilityPath(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)) + suite.Require().NoError(err) + + cbs, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + err = cbs.OnChanOpenInit(suite.chainA.GetContext(), channel.Ordering, channel.GetConnectionHops(), + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, chanCap, channel.Counterparty, channel.GetVersion(), + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + }) + } +} + +func (suite *InterchainAccountsTestSuite) TestOnChanOpenTry() { + var ( + channel *channeltypes.Channel + ) + testCases := []struct { + name string + malleate func() + expPass bool + }{ + + { + "success", func() {}, true, + }, + { + "success: ICA auth module callback returns error", func() { + // mock module callback should not be called on host side + suite.chainB.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenTry = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string, + portID, channelID string, chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, version, counterpartyVersion string, + ) error { + return fmt.Errorf("mock ica auth fails") + } + + }, true, + }, + { + "ICA callback fails - invalid version", func() { + channel.Version = types.VersionPrefix + }, false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() // reset + path := NewICAPath(suite.chainA, suite.chainB) + counterpartyVersion := types.VersionPrefix + suite.coordinator.SetupConnections(path) + + err := InitInterchainAccount(path.EndpointA, TestOwnerAddress) + suite.Require().NoError(err) + path.EndpointB.ChannelID = ibctesting.FirstChannelID + + // default values + counterparty := channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + channel = &channeltypes.Channel{ + State: channeltypes.TRYOPEN, + Ordering: channeltypes.ORDERED, + Counterparty: counterparty, + ConnectionHops: []string{path.EndpointB.ConnectionID}, + Version: TestVersion, + } + + tc.malleate() + + // ensure channel on chainB is set in state + suite.chainB.GetSimApp().IBCKeeper.ChannelKeeper.SetChannel(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, *channel) + + module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID) + suite.Require().NoError(err) + + chanCap, err := suite.chainB.App.GetScopedIBCKeeper().NewCapability(suite.chainB.GetContext(), host.ChannelCapabilityPath(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID)) + suite.Require().NoError(err) + + cbs, ok := suite.chainB.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + err = cbs.OnChanOpenTry(suite.chainB.GetContext(), channel.Ordering, channel.GetConnectionHops(), + path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, chanCap, channel.Counterparty, channel.GetVersion(), counterpartyVersion, + ) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + + }) + } + +} + +func (suite *InterchainAccountsTestSuite) TestOnChanOpenAck() { + var ( + counterpartyVersion string + ) + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", func() {}, true, + }, + { + "ICA OnChanOpenACK fails - invalid version", func() { + counterpartyVersion = "invalid|version" + }, false, + }, + { + "ICA auth module callback fails", func() { + suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenAck = func( + ctx sdk.Context, portID, channelID string, counterpartyVersion string, + ) error { + return fmt.Errorf("mock ica auth fails") + } + }, false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() // reset + path := NewICAPath(suite.chainA, suite.chainB) + counterpartyVersion = TestVersion + suite.coordinator.SetupConnections(path) + + err := InitInterchainAccount(path.EndpointA, TestOwnerAddress) + suite.Require().NoError(err) + + err = path.EndpointB.ChanOpenTry() + suite.Require().NoError(err) + + tc.malleate() + + module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), types.PortID) + suite.Require().NoError(err) + + cbs, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + err = cbs.OnChanOpenAck(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, counterpartyVersion) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + + }) + } + +} + +func (suite *InterchainAccountsTestSuite) TestOnChanOpenConfirm() { + testCases := []struct { + name string + malleate func() + expPass bool + }{ + + { + "success", func() {}, true, + }, + { + "success: ICA auth module callback returns error", func() { + // mock module callback should not be called on host side + suite.chainB.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenConfirm = func( + ctx sdk.Context, portID, channelID string, + ) error { + return fmt.Errorf("mock ica auth fails") + } + + }, true, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + err := InitInterchainAccount(path.EndpointA, TestOwnerAddress) + suite.Require().NoError(err) + + err = path.EndpointB.ChanOpenTry() + suite.Require().NoError(err) + + err = path.EndpointA.ChanOpenAck() + suite.Require().NoError(err) + + tc.malleate() + + module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID) + suite.Require().NoError(err) + + cbs, ok := suite.chainB.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + err = cbs.OnChanOpenConfirm(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID) + + if tc.expPass { + suite.Require().NoError(err) + } else { + suite.Require().Error(err) + } + + }) + } + +} + +func (suite *InterchainAccountsTestSuite) TestOnRecvPacket() { + var ( + packetData []byte + ) + testCases := []struct { + name string + malleate func() + expAckSuccess bool + }{ + { + "success", func() {}, true, + }, + { + "success with ICA auth module callback failure", func() { + suite.chainB.GetSimApp().ICAAuthModule.IBCApp.OnRecvPacket = func( + ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress, + ) exported.Acknowledgement { + return channeltypes.NewErrorAcknowledgement("failed OnRecvPacket mock callback") + } + }, true, + }, + { + "ICA OnRecvPacket fails - cannot unmarshal packet data", func() { + packetData = []byte("invalid data") + }, false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() // reset + + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + // send 100stake to interchain account wallet + amount, _ := sdk.ParseCoinsNormalized("100stake") + interchainAccountAddr, _ := suite.chainB.GetSimApp().ICAKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), path.EndpointA.ChannelConfig.PortID) + bankMsg := &banktypes.MsgSend{FromAddress: suite.chainB.SenderAccount.GetAddress().String(), ToAddress: interchainAccountAddr, Amount: amount} + + _, err = suite.chainB.SendMsgs(bankMsg) + suite.Require().NoError(err) + + // build packet data + msg := &banktypes.MsgSend{ + FromAddress: interchainAccountAddr, + ToAddress: suite.chainB.SenderAccount.GetAddress().String(), + Amount: amount, + } + data, err := types.SerializeCosmosTx(suite.chainA.Codec, []sdk.Msg{msg}) + suite.Require().NoError(err) + + icaPacketData := types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: data, + } + packetData = icaPacketData.GetBytes() + + // malleate packetData for test cases + tc.malleate() + + seq := uint64(1) + packet := channeltypes.NewPacket(packetData, seq, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, clienttypes.NewHeight(0, 100), 0) + + tc.malleate() + + module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), path.EndpointB.ChannelConfig.PortID) + suite.Require().NoError(err) + + cbs, ok := suite.chainB.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + ack := cbs.OnRecvPacket(suite.chainB.GetContext(), packet, nil) + suite.Require().Equal(tc.expAckSuccess, ack.Success()) + + }) + } + +} + +func (suite *InterchainAccountsTestSuite) TestNegotiateAppVersion() { + var ( + proposedVersion string + ) + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", func() {}, true, + }, + { + "invalid proposed version", func() { + proposedVersion = "invalid version" + }, false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + + module, _, err := suite.chainA.GetSimApp().GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), types.PortID) + suite.Require().NoError(err) + + cbs, ok := suite.chainA.GetSimApp().GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + counterpartyPortID, err := types.GeneratePortID(TestOwnerAddress, path.EndpointA.ConnectionID, path.EndpointB.ConnectionID) + suite.Require().NoError(err) + + counterparty := &channeltypes.Counterparty{ + PortId: counterpartyPortID, + ChannelId: path.EndpointB.ChannelID, + } + + proposedVersion = types.VersionPrefix + + tc.malleate() + + version, err := cbs.NegotiateAppVersion(suite.chainA.GetContext(), channeltypes.ORDERED, path.EndpointA.ConnectionID, types.PortID, *counterparty, proposedVersion) + if tc.expPass { + suite.Require().NoError(err) + suite.Require().NoError(types.ValidateVersion(version)) + suite.Require().Equal(TestVersion, version) + } else { + suite.Require().Error(err) + suite.Require().Empty(version) + } + }) + } +} diff --git a/modules/apps/27-interchain-accounts/keeper/handshake.go b/modules/apps/27-interchain-accounts/keeper/handshake.go index 7481898fe4b..a6c69509013 100644 --- a/modules/apps/27-interchain-accounts/keeper/handshake.go +++ b/modules/apps/27-interchain-accounts/keeper/handshake.go @@ -62,11 +62,6 @@ func (k Keeper) OnChanOpenInit( return sdkerrors.Wrapf(porttypes.ErrInvalidPort, "existing active channel %s for portID %s", activeChannelID, portID) } - // Claim channel capability passed back by IBC module - if err := k.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil { - return sdkerrors.Wrapf(err, "failed to claim capability for channel %s on port %s", channelID, portID) - } - return nil } diff --git a/modules/apps/27-interchain-accounts/keeper/handshake_test.go b/modules/apps/27-interchain-accounts/keeper/handshake_test.go index e81a5781cd5..cf27023831c 100644 --- a/modules/apps/27-interchain-accounts/keeper/handshake_test.go +++ b/modules/apps/27-interchain-accounts/keeper/handshake_test.go @@ -103,15 +103,6 @@ func (suite *KeeperTestSuite) TestOnChanOpenInit() { }, false, }, - { - "capability already claimed", - func() { - path.EndpointA.SetChannel(*channel) - err := suite.chainA.GetSimApp().ScopedICAKeeper.ClaimCapability(suite.chainA.GetContext(), chanCap, host.ChannelCapabilityPath(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)) - suite.Require().NoError(err) - }, - false, - }, } for _, tc := range testCases { diff --git a/modules/apps/27-interchain-accounts/keeper/keeper.go b/modules/apps/27-interchain-accounts/keeper/keeper.go index 467fbc519dc..c730882207d 100644 --- a/modules/apps/27-interchain-accounts/keeper/keeper.go +++ b/modules/apps/27-interchain-accounts/keeper/keeper.go @@ -22,6 +22,7 @@ type Keeper struct { storeKey sdk.StoreKey cdc codec.BinaryCodec + ics4Wrapper types.ICS4Wrapper channelKeeper types.ChannelKeeper portKeeper types.PortKeeper accountKeeper types.AccountKeeper @@ -34,7 +35,7 @@ type Keeper struct { // NewKeeper creates a new interchain account Keeper instance func NewKeeper( cdc codec.BinaryCodec, key sdk.StoreKey, - channelKeeper types.ChannelKeeper, portKeeper types.PortKeeper, + ics4Wrapper types.ICS4Wrapper, channelKeeper types.ChannelKeeper, portKeeper types.PortKeeper, accountKeeper types.AccountKeeper, scopedKeeper capabilitykeeper.ScopedKeeper, msgRouter *baseapp.MsgServiceRouter, ) Keeper { @@ -46,6 +47,7 @@ func NewKeeper( return Keeper{ storeKey: key, cdc: cdc, + ics4Wrapper: ics4Wrapper, channelKeeper: channelKeeper, portKeeper: portKeeper, accountKeeper: accountKeeper, diff --git a/modules/apps/27-interchain-accounts/keeper/relay.go b/modules/apps/27-interchain-accounts/keeper/relay.go index bbd15f32f46..681433e03ee 100644 --- a/modules/apps/27-interchain-accounts/keeper/relay.go +++ b/modules/apps/27-interchain-accounts/keeper/relay.go @@ -3,16 +3,16 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/types" clienttypes "github.com/cosmos/ibc-go/v2/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v2/modules/core/04-channel/types" - host "github.com/cosmos/ibc-go/v2/modules/core/24-host" ) -// TODO: implement middleware functionality, this will allow us to use capabilities to -// manage helper module access to owner addresses they do not have capabilities for -func (k Keeper) TrySendTx(ctx sdk.Context, portID string, icaPacketData types.InterchainAccountPacketData) (uint64, error) { +// TrySendTx takes in a transaction from an authentication module and attempts to send the packet +// if the base application has the capability to send on the provided portID +func (k Keeper) TrySendTx(ctx sdk.Context, chanCap *capabilitytypes.Capability, portID string, icaPacketData types.InterchainAccountPacketData) (uint64, error) { // Check for the active channel activeChannelID, found := k.GetActiveChannelID(ctx, portID) if !found { @@ -27,7 +27,7 @@ func (k Keeper) TrySendTx(ctx sdk.Context, portID string, icaPacketData types.In destinationPort := sourceChannelEnd.GetCounterparty().GetPortID() destinationChannel := sourceChannelEnd.GetCounterparty().GetChannelID() - return k.createOutgoingPacket(ctx, portID, activeChannelID, destinationPort, destinationChannel, icaPacketData) + return k.createOutgoingPacket(ctx, portID, activeChannelID, destinationPort, destinationChannel, chanCap, icaPacketData) } func (k Keeper) createOutgoingPacket( @@ -36,17 +36,13 @@ func (k Keeper) createOutgoingPacket( sourceChannel, destinationPort, destinationChannel string, + chanCap *capabilitytypes.Capability, icaPacketData types.InterchainAccountPacketData, ) (uint64, error) { if err := icaPacketData.ValidateBasic(); err != nil { return 0, sdkerrors.Wrap(err, "invalid interchain account packet data") } - channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel)) - if !ok { - return 0, sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") - } - // get the next sequence sequence, found := k.channelKeeper.GetNextSequenceSend(ctx, sourcePort, sourceChannel) if !found { @@ -68,7 +64,7 @@ func (k Keeper) createOutgoingPacket( timeoutTimestamp, ) - if err := k.channelKeeper.SendPacket(ctx, channelCap, packet); err != nil { + if err := k.ics4Wrapper.SendPacket(ctx, chanCap, packet); err != nil { return 0, err } @@ -154,10 +150,6 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet) error } } -func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, data types.InterchainAccountPacketData, ack channeltypes.Acknowledgement) error { - return nil -} - // OnTimeoutPacket removes the active channel associated with the provided packet, the underlying channel end is closed // due to the semantics of ORDERED channels func (k Keeper) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet) error { diff --git a/modules/apps/27-interchain-accounts/keeper/relay_test.go b/modules/apps/27-interchain-accounts/keeper/relay_test.go index 106af7ccd96..bcc1ebccf27 100644 --- a/modules/apps/27-interchain-accounts/keeper/relay_test.go +++ b/modules/apps/27-interchain-accounts/keeper/relay_test.go @@ -5,10 +5,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/types" clienttypes "github.com/cosmos/ibc-go/v2/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v2/modules/core/04-channel/types" + host "github.com/cosmos/ibc-go/v2/modules/core/24-host" ibctesting "github.com/cosmos/ibc-go/v2/testing" ) @@ -17,6 +19,7 @@ func (suite *KeeperTestSuite) TestTrySendTx() { path *ibctesting.Path icaPacketData types.InterchainAccountPacketData portID string + chanCap *capabilitytypes.Capability ) testCases := []struct { @@ -60,6 +63,11 @@ func (suite *KeeperTestSuite) TestTrySendTx() { suite.Require().NoError(err) }, false, }, + { + "invalid channel capability provided", func() { + chanCap = nil + }, false, + }, } for _, tc := range testCases { @@ -73,10 +81,12 @@ func (suite *KeeperTestSuite) TestTrySendTx() { err := suite.SetupICAPath(path, TestOwnerAddress) suite.Require().NoError(err) + // default setup portID = path.EndpointA.ChannelConfig.PortID amount, _ := sdk.ParseCoinsNormalized("100stake") interchainAccountAddr, _ := suite.chainB.GetSimApp().ICAKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), path.EndpointA.ChannelConfig.PortID) + msg := &banktypes.MsgSend{FromAddress: interchainAccountAddr, ToAddress: suite.chainB.SenderAccount.GetAddress().String(), Amount: amount} data, err := types.SerializeCosmosTx(suite.chainB.GetSimApp().AppCodec(), []sdk.Msg{msg}) suite.Require().NoError(err) @@ -88,9 +98,13 @@ func (suite *KeeperTestSuite) TestTrySendTx() { Memo: "memo", } + var ok bool + chanCap, ok = suite.chainA.GetSimApp().ScopedICAMockKeeper.GetCapability(path.EndpointA.Chain.GetContext(), host.ChannelCapabilityPath(portID, path.EndpointA.ChannelID)) + suite.Require().True(ok) + tc.malleate() - _, err = suite.chainA.GetSimApp().ICAKeeper.TrySendTx(suite.chainA.GetContext(), portID, icaPacketData) + _, err = suite.chainA.GetSimApp().ICAKeeper.TrySendTx(suite.chainA.GetContext(), chanCap, portID, icaPacketData) if tc.expPass { suite.Require().NoError(err) diff --git a/modules/apps/27-interchain-accounts/module.go b/modules/apps/27-interchain-accounts/module.go index ab085b886dc..8e83463aef9 100644 --- a/modules/apps/27-interchain-accounts/module.go +++ b/modules/apps/27-interchain-accounts/module.go @@ -72,6 +72,7 @@ type AppModule struct { keeper keeper.Keeper } +// NewAppModule creates an interchain accounts app module. func NewAppModule(k keeper.Keeper) AppModule { return AppModule{ keeper: k, diff --git a/modules/apps/27-interchain-accounts/module_test.go b/modules/apps/27-interchain-accounts/module_test.go deleted file mode 100644 index a2ff103d3eb..00000000000 --- a/modules/apps/27-interchain-accounts/module_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package interchain_accounts_test - -import ( - "testing" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/stretchr/testify/suite" - "github.com/tendermint/tendermint/crypto" - - "github.com/cosmos/ibc-go/v2/modules/apps/27-interchain-accounts/types" - channeltypes "github.com/cosmos/ibc-go/v2/modules/core/04-channel/types" - host "github.com/cosmos/ibc-go/v2/modules/core/24-host" - ibctesting "github.com/cosmos/ibc-go/v2/testing" -) - -var ( - // TestAccAddress defines a resuable bech32 address for testing purposes - // TODO: update crypto.AddressHash() when sdk uses address.Module() - TestAccAddress = types.GenerateAddress(sdk.AccAddress(crypto.AddressHash([]byte(types.ModuleName))), TestPortID) - // TestOwnerAddress defines a reusable bech32 address for testing purposes - TestOwnerAddress = "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs" - // TestPortID defines a resuable port identifier for testing purposes - TestPortID, _ = types.GeneratePortID(TestOwnerAddress, "connection-0", "connection-0") - // TestVersion defines a resuable interchainaccounts version string for testing purposes - TestVersion = types.NewAppVersion(types.VersionPrefix, TestAccAddress.String()) -) - -type InterchainAccountsTestSuite struct { - suite.Suite - - coordinator *ibctesting.Coordinator - - // testing chains used for convenience and readability - chainA *ibctesting.TestChain - chainB *ibctesting.TestChain - chainC *ibctesting.TestChain -} - -func TestICATestSuite(t *testing.T) { - suite.Run(t, new(InterchainAccountsTestSuite)) -} - -func (suite *InterchainAccountsTestSuite) SetupTest() { - suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3) - suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(0)) - suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(1)) - suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(2)) -} - -func NewICAPath(chainA, chainB *ibctesting.TestChain) *ibctesting.Path { - path := ibctesting.NewPath(chainA, chainB) - path.EndpointA.ChannelConfig.PortID = types.PortID - path.EndpointB.ChannelConfig.PortID = types.PortID - path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED - path.EndpointB.ChannelConfig.Order = channeltypes.ORDERED - path.EndpointA.ChannelConfig.Version = types.VersionPrefix - path.EndpointB.ChannelConfig.Version = TestVersion - - return path -} - -func InitInterchainAccount(endpoint *ibctesting.Endpoint, owner string) error { - portID, err := types.GeneratePortID(owner, endpoint.ConnectionID, endpoint.Counterparty.ConnectionID) - if err != nil { - return err - } - channelSequence := endpoint.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(endpoint.Chain.GetContext()) - - if err := endpoint.Chain.GetSimApp().ICAKeeper.InitInterchainAccount(endpoint.Chain.GetContext(), endpoint.ConnectionID, endpoint.Counterparty.ConnectionID, owner); err != nil { - return err - } - - // commit state changes for proof verification - endpoint.Chain.App.Commit() - endpoint.Chain.NextBlock() - - // update port/channel ids - endpoint.ChannelID = channeltypes.FormatChannelIdentifier(channelSequence) - endpoint.ChannelConfig.PortID = portID - return nil -} - -// SetupICAPath invokes the InterchainAccounts entrypoint and subsequent channel handshake handlers -func SetupICAPath(path *ibctesting.Path, owner string) error { - if err := InitInterchainAccount(path.EndpointA, owner); err != nil { - return err - } - - if err := path.EndpointB.ChanOpenTry(); err != nil { - return err - } - - if err := path.EndpointA.ChanOpenAck(); err != nil { - return err - } - - if err := path.EndpointB.ChanOpenConfirm(); err != nil { - return err - } - - return nil -} - -func (suite *InterchainAccountsTestSuite) TestOnChanOpenInit() { - suite.SetupTest() // reset - path := NewICAPath(suite.chainA, suite.chainB) - suite.coordinator.SetupConnections(path) - - // mock init interchain account - portID, err := types.GeneratePortID(TestOwnerAddress, path.EndpointA.ConnectionID, path.EndpointB.ConnectionID) - suite.Require().NoError(err) - portCap := suite.chainA.GetSimApp().IBCKeeper.PortKeeper.BindPort(suite.chainA.GetContext(), portID) - suite.chainA.GetSimApp().ICAKeeper.ClaimCapability(suite.chainA.GetContext(), portCap, host.PortPath(portID)) - path.EndpointA.ChannelConfig.PortID = portID - - // default values - counterparty := channeltypes.NewCounterparty(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID) - channel := &channeltypes.Channel{ - State: channeltypes.INIT, - Ordering: channeltypes.ORDERED, - Counterparty: counterparty, - ConnectionHops: []string{path.EndpointA.ConnectionID}, - Version: types.VersionPrefix, - } - - // set channel - path.EndpointA.SetChannel(*channel) - - module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), types.PortID) - suite.Require().NoError(err) - - chanCap, err := suite.chainA.App.GetScopedIBCKeeper().NewCapability(suite.chainA.GetContext(), host.ChannelCapabilityPath(ibctesting.TransferPort, path.EndpointA.ChannelID)) - suite.Require().NoError(err) - - cbs, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) - suite.Require().True(ok) - - err = cbs.OnChanOpenInit(suite.chainA.GetContext(), channel.Ordering, channel.GetConnectionHops(), - path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, chanCap, channel.Counterparty, channel.GetVersion(), - ) - suite.Require().NoError(err) -} - -func (suite *InterchainAccountsTestSuite) TestOnChanOpenTry() { - suite.SetupTest() // reset - path := NewICAPath(suite.chainA, suite.chainB) - suite.coordinator.SetupConnections(path) - - err := InitInterchainAccount(path.EndpointA, TestOwnerAddress) - suite.Require().NoError(err) - - // default values - counterparty := channeltypes.NewCounterparty(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - channel := &channeltypes.Channel{ - State: channeltypes.TRYOPEN, - Ordering: channeltypes.ORDERED, - Counterparty: counterparty, - ConnectionHops: []string{path.EndpointB.ConnectionID}, - Version: types.VersionPrefix, - } - - // set channel - channelSequence := path.EndpointB.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(path.EndpointB.Chain.GetContext()) - path.EndpointB.ChannelID = channeltypes.FormatChannelIdentifier(channelSequence) - path.EndpointB.SetChannel(*channel) - - module, _, err := suite.chainB.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainB.GetContext(), types.PortID) - suite.Require().NoError(err) - - chanCap, err := suite.chainB.App.GetScopedIBCKeeper().NewCapability(suite.chainB.GetContext(), host.ChannelCapabilityPath(ibctesting.TransferPort, path.EndpointB.ChannelID)) - suite.Require().NoError(err) - - cbs, ok := suite.chainB.App.GetIBCKeeper().Router.GetRoute(module) - suite.Require().True(ok) - - err = cbs.OnChanOpenTry(suite.chainB.GetContext(), channel.Ordering, channel.GetConnectionHops(), - path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, chanCap, channel.Counterparty, TestVersion, types.VersionPrefix, - ) - suite.Require().NoError(err) -} - -func (suite *InterchainAccountsTestSuite) TestOnChanOpenAck() { - suite.SetupTest() // reset - path := NewICAPath(suite.chainA, suite.chainB) - suite.coordinator.SetupConnections(path) - - err := InitInterchainAccount(path.EndpointA, TestOwnerAddress) - suite.Require().NoError(err) - - err = path.EndpointB.ChanOpenTry() - suite.Require().NoError(err) - - module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), types.PortID) - suite.Require().NoError(err) - - cbs, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) - suite.Require().True(ok) - - err = cbs.OnChanOpenAck(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, TestVersion) - suite.Require().NoError(err) -} - -func (suite *InterchainAccountsTestSuite) TestOnChanOpenConfirm() { - suite.SetupTest() // reset - path := NewICAPath(suite.chainA, suite.chainB) - suite.coordinator.SetupConnections(path) - - err := InitInterchainAccount(path.EndpointA, TestOwnerAddress) - suite.Require().NoError(err) - - err = path.EndpointB.ChanOpenTry() - suite.Require().NoError(err) - - err = path.EndpointA.ChanOpenAck() - suite.Require().NoError(err) - - module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), types.PortID) - suite.Require().NoError(err) - - cbs, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) - suite.Require().True(ok) - - err = cbs.OnChanOpenConfirm(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) - suite.Require().NoError(err) -} - -func (suite *InterchainAccountsTestSuite) TestNegotiateAppVersion() { - suite.SetupTest() - path := NewICAPath(suite.chainA, suite.chainB) - suite.coordinator.SetupConnections(path) - - module, _, err := suite.chainA.GetSimApp().GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), types.PortID) - suite.Require().NoError(err) - - cbs, ok := suite.chainA.GetSimApp().GetIBCKeeper().Router.GetRoute(module) - suite.Require().True(ok) - - counterpartyPortID, err := types.GeneratePortID(TestOwnerAddress, path.EndpointA.ConnectionID, path.EndpointB.ConnectionID) - suite.Require().NoError(err) - - counterparty := &channeltypes.Counterparty{ - PortId: counterpartyPortID, - ChannelId: path.EndpointB.ChannelID, - } - - version, err := cbs.NegotiateAppVersion(suite.chainA.GetContext(), channeltypes.ORDERED, path.EndpointA.ConnectionID, types.PortID, *counterparty, types.VersionPrefix) - suite.Require().NoError(err) - suite.Require().NoError(types.ValidateVersion(version)) - suite.Require().Equal(TestVersion, version) -} diff --git a/modules/apps/27-interchain-accounts/types/expected_keepers.go b/modules/apps/27-interchain-accounts/types/expected_keepers.go index 2b08a9bcfe9..558a38c4033 100644 --- a/modules/apps/27-interchain-accounts/types/expected_keepers.go +++ b/modules/apps/27-interchain-accounts/types/expected_keepers.go @@ -18,11 +18,15 @@ type AccountKeeper interface { GetModuleAddress(name string) sdk.AccAddress } +// ICS4Wrapper defines the expected ICS4Wrapper for middleware +type ICS4Wrapper interface { + SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error +} + // ChannelKeeper defines the expected IBC channel keeper type ChannelKeeper interface { GetChannel(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) GetNextSequenceSend(ctx sdk.Context, portID, channelID string) (uint64, bool) - SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error CounterpartyHops(ctx sdk.Context, channel channeltypes.Channel) ([]string, bool) } diff --git a/testing/README.md b/testing/README.md index f7a71bb1348..189f8e8c6cc 100644 --- a/testing/README.md +++ b/testing/README.md @@ -294,7 +294,7 @@ When writing IBC applications acting as middleware, it might be desirable to tes This can be done by wiring a middleware stack in the app.go file using existing applications as middleware and IBC base applications. The mock module may also be leveraged to act as a base application in the instance that such an application is not available for testing or causes dependency concerns. -The mock module contains a `MockIBCApp`. This struct contains a function field for every IBC App Module callback. +The mock IBC module contains a `MockIBCApp`. This struct contains a function field for every IBC App Module callback. Each of these functions can be individually set to mock expected behaviour of a base application. For example, if one wanted to test that the base application cannot affect the outcome of the `OnChanOpenTry` callback, the mock module base application callback could be updated as such: @@ -303,3 +303,17 @@ For example, if one wanted to test that the base application cannot affect the o return fmt.Errorf("mock base app must not be called for OnChanOpenTry") } ``` + +Using a mock module as a base application in a middleware stack may require adding the module to your `SimApp`. +This is because IBC will route to the top level IBC module of a middleware stack, so a module which never +sits at the top of middleware stack will need to be accessed via a public field in `SimApp` + +This might look like: +```go + suite.chainA.GetSimApp().ICAAuthModule.IBCApp.OnChanOpenInit = func(ctx sdk.Context, order channeltypes.Order, connectionHops []string, + portID, channelID string, chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, version string, + ) error { + return fmt.Errorf("mock ica auth fails") + } +``` diff --git a/testing/simapp/app.go b/testing/simapp/app.go index e9f6cd33543..3050e6436d6 100644 --- a/testing/simapp/app.go +++ b/testing/simapp/app.go @@ -196,6 +196,11 @@ type SimApp struct { ScopedTransferKeeper capabilitykeeper.ScopedKeeper ScopedICAKeeper capabilitykeeper.ScopedKeeper ScopedIBCMockKeeper capabilitykeeper.ScopedKeeper + ScopedICAMockKeeper capabilitykeeper.ScopedKeeper + + // make IBC modules public for test purposes + // these modules are never directly routed to by the IBC Router + ICAAuthModule ibcmock.IBCModule // the module manager mm *module.Manager @@ -265,8 +270,9 @@ func NewSimApp( scopedICAKeeper := app.CapabilityKeeper.ScopeToModule(icatypes.ModuleName) // NOTE: the IBC mock keeper and application module is used only for testing core IBC. Do - // note replicate if you do not need to test core IBC or light clients. + // not replicate if you do not need to test core IBC or light clients. scopedIBCMockKeeper := app.CapabilityKeeper.ScopeToModule(ibcmock.ModuleName) + scopedICAMockKeeper := app.CapabilityKeeper.ScopeToModule(ibcmock.ModuleName + icatypes.ModuleName) // seal capability keeper after scoping modules app.CapabilityKeeper.Seal() @@ -333,24 +339,31 @@ func NewSimApp( transferModule := transfer.NewAppModule(app.TransferKeeper) transferIBCModule := transfer.NewIBCModule(app.TransferKeeper) + // NOTE: the IBC mock keeper and application module is used only for testing core IBC. Do + // not replicate if you do not need to test core IBC or light clients. + mockModule := ibcmock.NewAppModule(scopedIBCMockKeeper, &app.IBCKeeper.PortKeeper) + mockIBCModule := ibcmock.NewIBCModule(&ibcmock.MockIBCApp{}, scopedIBCMockKeeper) + app.ICAKeeper = icakeeper.NewKeeper( appCodec, keys[icatypes.StoreKey], + app.IBCKeeper.ChannelKeeper, // may be replaced with middleware such as ics29 fee app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.AccountKeeper, scopedICAKeeper, app.MsgServiceRouter(), ) icaModule := ica.NewAppModule(app.ICAKeeper) - icaIBCModule := ica.NewIBCModule(app.ICAKeeper, nil) - // NOTE: the IBC mock keeper and application module is used only for testing core IBC. Do - // note replicate if you do not need to test core IBC or light clients. - mockModule := ibcmock.NewAppModule(scopedIBCMockKeeper, &app.IBCKeeper.PortKeeper) - mockIBCModule := ibcmock.NewIBCModule(&ibcmock.MockIBCApp{}, scopedIBCMockKeeper) + // initialize ICA module with mock module as the authentication module on the controller side + icaAuthModule := ibcmock.NewIBCModule(&ibcmock.MockIBCApp{}, scopedICAMockKeeper) + app.ICAAuthModule = icaAuthModule + + icaIBCModule := ica.NewIBCModule(app.ICAKeeper, icaAuthModule) // Create static IBC router, add transfer route, then set and seal it ibcRouter := porttypes.NewRouter() - ibcRouter.AddRoute(icatypes.ModuleName, icaIBCModule) - ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferIBCModule) - ibcRouter.AddRoute(ibcmock.ModuleName, mockIBCModule) + ibcRouter.AddRoute(icatypes.ModuleName, icaIBCModule). + AddRoute(ibcmock.ModuleName+icatypes.ModuleName, icaIBCModule). // ica with mock auth module stack route to ica (top level of middleware stack) + AddRoute(ibctransfertypes.ModuleName, transferIBCModule). + AddRoute(ibcmock.ModuleName, mockIBCModule) app.IBCKeeper.SetRouter(ibcRouter) // create evidence keeper with router @@ -486,6 +499,7 @@ func NewSimApp( // NOTE: the IBC mock keeper and application module is used only for testing core IBC. Do // note replicate if you do not need to test core IBC or light clients. app.ScopedIBCMockKeeper = scopedIBCMockKeeper + app.ScopedICAMockKeeper = scopedICAMockKeeper return app } @@ -600,11 +614,6 @@ func (app *SimApp) GetScopedIBCKeeper() capabilitykeeper.ScopedKeeper { return app.ScopedIBCKeeper } -// GetMockModule returns the mock module in the testing application -func (app *SimApp) GetMockModule() ibcmock.AppModule { - return app.mm.Modules[ibcmock.ModuleName].(ibcmock.AppModule) -} - // GetTxConfig implements the TestingApp interface. func (app *SimApp) GetTxConfig() client.TxConfig { return MakeTestEncodingConfig().TxConfig