diff --git a/modules/core/04-channel/v2/keeper/msg_server.go b/modules/core/04-channel/v2/keeper/msg_server.go index 123fd44310e..612ebdcee57 100644 --- a/modules/core/04-channel/v2/keeper/msg_server.go +++ b/modules/core/04-channel/v2/keeper/msg_server.go @@ -167,7 +167,7 @@ func (k *Keeper) Timeout(ctx context.Context, timeout *channeltypesv2.MsgTimeout sdkCtx := sdk.UnwrapSDKContext(ctx) if err := k.timeoutPacket(ctx, timeout.Packet, timeout.ProofUnreceived, timeout.ProofHeight); err != nil { sdkCtx.Logger().Error("Timeout packet failed", "source-channel", timeout.Packet.SourceChannel, "destination-channel", timeout.Packet.DestinationChannel, "error", errorsmod.Wrap(err, "timeout packet failed")) - return nil, errorsmod.Wrapf(err, "send packet failed for source id: %s and destination id: %s", timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel) + return nil, errorsmod.Wrapf(err, "timeout packet failed for source id: %s and destination id: %s", timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel) } signer, err := sdk.AccAddressFromBech32(timeout.Signer) @@ -176,17 +176,13 @@ func (k *Keeper) Timeout(ctx context.Context, timeout *channeltypesv2.MsgTimeout return nil, errorsmod.Wrap(err, "invalid address for msg Signer") } - _ = signer - - // TODO: implement once app router is wired up. - // https://github.com/cosmos/ibc-go/issues/7384 - // for _, pd := range timeout.Packet.Data { - // cbs := k.PortKeeper.AppRouter.Route(pd.SourcePort) - // err := cbs.OnTimeoutPacket(timeout.Packet.SourceChannel, timeout.Packet.TimeoutTimestamp, signer) - // if err != nil { - // return err, err - // } - // } + for _, pd := range timeout.Packet.Data { + cbs := k.Router.Route(pd.SourcePort) + err := cbs.OnTimeoutPacket(ctx, timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel, pd, signer) + if err != nil { + return nil, errorsmod.Wrapf(err, "failed OnTimeoutPacket for source port %s, source channel %s, destination channel %s", pd.SourcePort, timeout.Packet.SourceChannel, timeout.Packet.DestinationChannel) + } + } return &channeltypesv2.MsgTimeoutResponse{Result: channeltypesv1.SUCCESS}, nil } diff --git a/modules/core/04-channel/v2/keeper/msg_server_test.go b/modules/core/04-channel/v2/keeper/msg_server_test.go index 83eb1e8b9ff..4a39bfb64d1 100644 --- a/modules/core/04-channel/v2/keeper/msg_server_test.go +++ b/modules/core/04-channel/v2/keeper/msg_server_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -471,3 +472,112 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { }) } } + +func (suite *KeeperTestSuite) TestMsgTimeout() { + var ( + path *ibctesting.Path + msgTimeout *channeltypesv2.MsgTimeout + packet channeltypesv2.Packet + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + name: "success", + malleate: func() {}, + }, + { + name: "failure: no-op", + malleate: func() { + suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SetPacketCommitment(suite.chainA.GetContext(), packet.SourceChannel, packet.Sequence, []byte{}) + + // Modify the callback to return an error. + // This way, we can verify that the callback is not executed in a No-op case. + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(context.Context, string, string, channeltypesv2.PacketData, sdk.AccAddress) error { + return errors.New("OnAcknowledgementPacket callback failed") + } + }, + expError: channeltypesv1.ErrNoOpMsg, + }, + { + name: "failure: invalid signer", + malleate: func() { + msgTimeout.Signer = "" + }, + expError: errors.New("empty address string is not allowed"), + }, + { + name: "failure: callback fails", + malleate: func() { + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(context.Context, string, string, channeltypesv2.PacketData, sdk.AccAddress) error { + return errors.New("OnTimeoutPacket callback failed") + } + }, + expError: errors.New("OnTimeoutPacket callback failed"), + }, + { + name: "failure: counterparty not found", + malleate: func() { + // change the source id to a non-existent channel. + msgTimeout.Packet.SourceChannel = "not-existent-channel" + }, + expError: channeltypesv2.ErrChannelNotFound, + }, + { + name: "failure: invalid commitment", + malleate: func() { + suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SetPacketCommitment(suite.chainA.GetContext(), packet.SourceChannel, packet.Sequence, []byte("foo")) + }, + expError: channeltypesv2.ErrInvalidPacket, + }, + { + name: "failure: failed membership verification", + malleate: func() { + msgTimeout.ProofHeight = clienttypes.ZeroHeight() + }, + expError: clienttypes.ErrConsensusStateNotFound, + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetupV2() + + // Send packet from A to B + timeoutTimestamp := suite.chainA.GetTimeoutTimestamp() + + mockData := mockv2.NewMockPacketData(mockv2.ModuleNameA, mockv2.ModuleNameB) + msgSendPacket := channeltypesv2.NewMsgSendPacket(path.EndpointA.ChannelID, timeoutTimestamp, suite.chainA.SenderAccount.GetAddress().String(), mockData) + res, err := path.EndpointA.Chain.SendMsgs(msgSendPacket) + suite.Require().NoError(err) + suite.Require().NotNil(res) + suite.Require().NoError(path.EndpointB.UpdateClient()) + + suite.coordinator.IncrementTimeBy(time.Hour * 20) + suite.Require().NoError(path.EndpointA.UpdateClient()) + + packet = channeltypesv2.NewPacket(1, path.EndpointA.ChannelID, path.EndpointB.ChannelID, timeoutTimestamp, mockData) + + packetKey := hostv2.PacketReceiptKey(packet.DestinationChannel, packet.Sequence) + proof, proofHeight := path.EndpointB.QueryProof(packetKey) + msgTimeout = channeltypesv2.NewMsgTimeout(packet, proof, proofHeight, 1, suite.chainA.SenderAccount.GetAddress().String()) + + tc.malleate() + + res, err = suite.chainA.SendMsgs(msgTimeout) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + suite.NotNil(res) + } else { + ibctesting.RequireErrorIsOrContains(suite.T(), err, tc.expError, "expected error %q, got %q instead", tc.expError, err) + } + }) + } +} diff --git a/modules/core/04-channel/v2/types/msgs.go b/modules/core/04-channel/v2/types/msgs.go index cb06c5f90ef..5d138c8732c 100644 --- a/modules/core/04-channel/v2/types/msgs.go +++ b/modules/core/04-channel/v2/types/msgs.go @@ -101,3 +101,14 @@ func NewMsgAcknowledgement(packet Packet, acknowledgement Acknowledgement, proof Signer: signer, } } + +// NewMsgTimeout creates a new MsgTimeout instance +func NewMsgTimeout(packet Packet, proofUnreceived []byte, proofHeight clienttypes.Height, nextSequenceRecv uint64, signer string) *MsgTimeout { + return &MsgTimeout{ + Packet: packet, + ProofUnreceived: proofUnreceived, + ProofHeight: proofHeight, + NextSequenceRecv: nextSequenceRecv, + Signer: signer, + } +}