diff --git a/modules/core/04-channel/keeper/events.go b/modules/core/04-channel/keeper/events.go index b93f3d0c3d2..136c382bb12 100644 --- a/modules/core/04-channel/keeper/events.go +++ b/modules/core/04-channel/keeper/events.go @@ -121,7 +121,7 @@ func emitChannelCloseConfirmEvent(ctx sdk.Context, portID string, channelID stri // emitSendPacketEvent emits an event with packet data along with other packet information for relayer // to pick up and relay to other chain -func emitSendPacketEvent(ctx sdk.Context, packet types.Packet, channel types.Channel, timeoutHeight exported.Height) { +func EmitSendPacketEvent(ctx sdk.Context, packet types.Packet, channel types.Channel, timeoutHeight exported.Height) { ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeSendPacket, diff --git a/modules/core/04-channel/keeper/packet.go b/modules/core/04-channel/keeper/packet.go index 1ab81953cb9..0711722cc55 100644 --- a/modules/core/04-channel/keeper/packet.go +++ b/modules/core/04-channel/keeper/packet.go @@ -89,7 +89,7 @@ func (k *Keeper) SendPacket( k.SetNextSequenceSend(ctx, sourcePort, sourceChannel, sequence+1) k.SetPacketCommitment(ctx, sourcePort, sourceChannel, packet.GetSequence(), commitment) - emitSendPacketEvent(ctx, packet, channel, timeoutHeight) + EmitSendPacketEvent(ctx, packet, channel, timeoutHeight) k.Logger(ctx).Info( "packet sent", diff --git a/modules/core/packet-server/keeper/keeper.go b/modules/core/packet-server/keeper/keeper.go index bc3223cae9d..49d6e9ed410 100644 --- a/modules/core/packet-server/keeper/keeper.go +++ b/modules/core/packet-server/keeper/keeper.go @@ -1,8 +1,16 @@ package keeper import ( + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channelkeeper "github.com/cosmos/ibc-go/v9/modules/core/04-channel/keeper" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + "github.com/cosmos/ibc-go/v9/modules/core/exported" "github.com/cosmos/ibc-go/v9/modules/core/packet-server/types" ) @@ -19,3 +27,75 @@ func NewKeeper(cdc codec.BinaryCodec, channelKeeper types.ChannelKeeper, clientK ClientKeeper: clientKeeper, } } + +func (k Keeper) SendPacket( + ctx sdk.Context, + _ *capabilitytypes.Capability, + sourceChannel string, + sourcePort string, + destPort string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + version string, + data []byte, +) (uint64, error) { + // Lookup counterparty associated with our source channel to retrieve the destination channel + counterparty, ok := k.ClientKeeper.GetCounterparty(ctx, sourceChannel) + if !ok { + return 0, channeltypes.ErrChannelNotFound + } + destChannel := counterparty.ClientId + + // retrieve the sequence send for this channel + // if no packets have been sent yet, initialize the sequence to 1. + sequence, found := k.ChannelKeeper.GetNextSequenceSend(ctx, sourcePort, sourceChannel) + if !found { + sequence = 1 + } + + // construct packet from given fields and channel state + packet := channeltypes.NewPacketWithVersion(data, sequence, sourcePort, sourceChannel, + destPort, destChannel, timeoutHeight, timeoutTimestamp, version) + + if err := packet.ValidateBasic(); err != nil { + return 0, errorsmod.Wrap(err, "constructed packet failed basic validation") + } + + // check that the client of receiver chain is still active + if status := k.ClientKeeper.GetClientStatus(ctx, sourceChannel); status != exported.Active { + return 0, errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client state is not active: %s", status) + } + + // retrieve latest height and timestamp of the client of receiver chain + latestHeight := k.ClientKeeper.GetClientLatestHeight(ctx, sourceChannel) + if latestHeight.IsZero() { + return 0, errorsmod.Wrapf(clienttypes.ErrInvalidHeight, "cannot send packet using client (%s) with zero height", sourceChannel) + } + + latestTimestamp, err := k.ClientKeeper.GetClientTimestampAtHeight(ctx, sourceChannel, latestHeight) + if err != nil { + return 0, err + } + + // check if packet is timed out on the receiving chain + timeout := channeltypes.NewTimeout(packet.GetTimeoutHeight().(clienttypes.Height), packet.GetTimeoutTimestamp()) + if timeout.Elapsed(latestHeight, latestTimestamp) { + return 0, errorsmod.Wrap(timeout.ErrTimeoutElapsed(latestHeight, latestTimestamp), "invalid packet timeout") + } + + commitment := channeltypes.CommitPacket(packet) + + // bump the sequence and set the packet commitment so it is provable by the counterparty + k.ChannelKeeper.SetNextSequenceSend(ctx, sourcePort, sourceChannel, sequence+1) + k.ChannelKeeper.SetPacketCommitment(ctx, sourcePort, sourceChannel, packet.GetSequence(), commitment) + + // create sentinel channel for events + channel := channeltypes.Channel{ + Ordering: channeltypes.ORDERED, + ConnectionHops: []string{sourceChannel}, + } + channelkeeper.EmitSendPacketEvent(ctx, packet, channel, timeoutHeight) + + // return the sequence + return sequence, nil +} diff --git a/modules/core/packet-server/keeper/keeper_test.go b/modules/core/packet-server/keeper/keeper_test.go index c73615069ae..aae084891a8 100644 --- a/modules/core/packet-server/keeper/keeper_test.go +++ b/modules/core/packet-server/keeper/keeper_test.go @@ -1,11 +1,16 @@ package keeper_test import ( + "fmt" "testing" testifysuite "github.com/stretchr/testify/suite" + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + tmtypes "github.com/cosmos/ibc-go/v9/modules/light-clients/07-tendermint" ibctesting "github.com/cosmos/ibc-go/v9/testing" + "github.com/cosmos/ibc-go/v9/testing/mock" ) // KeeperTestSuite is a testing suite to test keeper functions. @@ -35,16 +40,72 @@ func (suite *KeeperTestSuite) SetupTest() { suite.coordinator.CommitNBlocks(suite.chainB, 2) } -// TODO: Remove, just testing the testing setup. -func (suite *KeeperTestSuite) TestCreateEurekaClients() { - path := ibctesting.NewPath(suite.chainA, suite.chainB) - path.SetupV2() +func (suite *KeeperTestSuite) TestSendPacket() { + var ( + path *ibctesting.Path + packet channeltypes.Packet + ) - // Assert counterparty set and creator deleted - _, found := suite.chainA.App.GetPacketServer().ClientKeeper.GetCounterparty(suite.chainA.GetContext(), path.EndpointA.ClientID) - suite.Require().True(found) + testCases := []struct { + name string + malleate func() + expError error + }{ + {"success", func() {}, nil}, + {"counterparty not found", func() { + packet.SourceChannel = ibctesting.FirstChannelID + }, channeltypes.ErrChannelNotFound}, + {"packet failed basic validation", func() { + // invalid data + packet.Data = nil + }, channeltypes.ErrInvalidPacket}, + {"client status invalid", func() { + // make underlying client Frozen to get invalid client status + clientState, ok := suite.chainA.App.GetIBCKeeper().ClientKeeper.GetClientState(suite.chainA.GetContext(), path.EndpointA.ClientID) + suite.Require().True(ok, "could not retrieve client state") + tmClientState, ok := clientState.(*tmtypes.ClientState) + suite.Require().True(ok, "client is not tendermint client") + tmClientState.FrozenHeight = clienttypes.NewHeight(0, 1) + suite.chainA.App.GetIBCKeeper().ClientKeeper.SetClientState(suite.chainA.GetContext(), path.EndpointA.ClientID, tmClientState) + }, clienttypes.ErrClientNotActive}, + {"timeout elapsed", func() { + packet.TimeoutTimestamp = 1 + }, channeltypes.ErrTimeoutElapsed}, + } - // Assert counterparty set and creator deleted - _, found = suite.chainB.App.GetPacketServer().ClientKeeper.GetCounterparty(suite.chainB.GetContext(), path.EndpointB.ClientID) - suite.Require().True(found) + for i, tc := range testCases { + tc := tc + suite.Run(fmt.Sprintf("Case %s, %d/%d tests", tc.name, i, len(testCases)), func() { + suite.SetupTest() // reset + + // create clients and set counterparties on both chains + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetupV2() + + // create standard packet that can be malleated + packet = channeltypes.NewPacketWithVersion(mock.MockPacketData, 1, mock.PortID, + path.EndpointA.ClientID, mock.PortID, path.EndpointB.ClientID, clienttypes.NewHeight(1, 100), 0, mock.Version) + + // malleate the test case + tc.malleate() + + // send packet + seq, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, + packet.DestinationPort, packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + suite.Require().Equal(uint64(1), seq) + expCommitment := channeltypes.CommitPacket(packet) + suite.Require().Equal(expCommitment, suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetPacketCommitment(suite.chainA.GetContext(), packet.SourcePort, packet.SourceChannel, seq)) + } else { + suite.Require().Error(err) + suite.Require().ErrorIs(err, tc.expError) + suite.Require().Equal(uint64(0), seq) + suite.Require().Nil(suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetPacketCommitment(suite.chainA.GetContext(), packet.SourcePort, packet.SourceChannel, seq)) + + } + }) + } } diff --git a/modules/core/packet-server/types/expected_keepers.go b/modules/core/packet-server/types/expected_keepers.go index 7b4c6a03b1f..2e9a701e2c8 100644 --- a/modules/core/packet-server/types/expected_keepers.go +++ b/modules/core/packet-server/types/expected_keepers.go @@ -43,4 +43,11 @@ type ClientKeeper interface { // the executing chain // This is a private path that is only used by the IBC lite module GetCounterparty(ctx sdk.Context, clientID string) (clienttypes.Counterparty, bool) + // GetClientStatus returns the status of a client given the client ID + GetClientStatus(ctx sdk.Context, clientID string) exported.Status + // GetClientLatestHeight returns the latest height of a client given the client ID + GetClientLatestHeight(ctx sdk.Context, clientID string) clienttypes.Height + // GetClientTimestampAtHeight returns the timestamp for a given height on the client + // given its client ID and height + GetClientTimestampAtHeight(ctx sdk.Context, clientID string, height exported.Height) (uint64, error) }