diff --git a/modules/core/04-channel/types/timeout.go b/modules/core/04-channel/types/timeout.go index 47265d5b3a9..ca1e70ca8e6 100644 --- a/modules/core/04-channel/types/timeout.go +++ b/modules/core/04-channel/types/timeout.go @@ -14,6 +14,14 @@ func NewTimeout(height clienttypes.Height, timestamp uint64) Timeout { } } +// NewTimeoutWithTimestamp creates a new Timeout with only the timestamp set. +func NewTimeoutWithTimestamp(timestamp uint64) Timeout { + return Timeout{ + Height: clienttypes.ZeroHeight(), + Timestamp: timestamp, + } +} + // IsValid returns true if either the height or timestamp is non-zero. func (t Timeout) IsValid() bool { return !t.Height.IsZero() || t.Timestamp != 0 @@ -25,6 +33,11 @@ func (t Timeout) Elapsed(height clienttypes.Height, timestamp uint64) bool { return t.heightElapsed(height) || t.timestampElapsed(timestamp) } +// TimestampElapsed returns true if the provided timestamp is past the timeout timestamp. +func (t Timeout) TimestampElapsed(timestamp uint64) bool { + return t.timestampElapsed(timestamp) +} + // ErrTimeoutElapsed returns a timeout elapsed error indicating which timeout value // has elapsed. func (t Timeout) ErrTimeoutElapsed(height clienttypes.Height, timestamp uint64) error { diff --git a/modules/core/04-channel/v2/keeper/events.go b/modules/core/04-channel/v2/keeper/events.go new file mode 100644 index 00000000000..ddaf8876080 --- /dev/null +++ b/modules/core/04-channel/v2/keeper/events.go @@ -0,0 +1,12 @@ +package keeper + +import ( + "context" + + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" +) + +// EmitSendPacketEvents emits events for the SendPacket handler. +func EmitSendPacketEvents(ctx context.Context, packet channeltypesv2.Packet) { + // TODO: https://github.com/cosmos/ibc-go/issues/7386 +} diff --git a/modules/core/04-channel/v2/keeper/keeper.go b/modules/core/04-channel/v2/keeper/keeper.go index ac50715c836..d367855475a 100644 --- a/modules/core/04-channel/v2/keeper/keeper.go +++ b/modules/core/04-channel/v2/keeper/keeper.go @@ -13,7 +13,11 @@ import ( "github.com/cosmos/cosmos-sdk/runtime" sdk "github.com/cosmos/cosmos-sdk/types" + connectionkeeper "github.com/cosmos/ibc-go/v9/modules/core/03-connection/keeper" + channelkeeperv1 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/keeper" + channeltypesv1 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + commitmentv2types "github.com/cosmos/ibc-go/v9/modules/core/23-commitment/types/v2" host "github.com/cosmos/ibc-go/v9/modules/core/24-host" hostv2 "github.com/cosmos/ibc-go/v9/modules/core/24-host/v2" "github.com/cosmos/ibc-go/v9/modules/core/exported" @@ -23,13 +27,20 @@ import ( type Keeper struct { cdc codec.BinaryCodec storeService corestore.KVStoreService + ClientKeeper types.ClientKeeper + // channelKeeperV1 is used for channel aliasing only. + channelKeeperV1 *channelkeeperv1.Keeper + connectionKeeper *connectionkeeper.Keeper } // NewKeeper creates a new channel v2 keeper -func NewKeeper(cdc codec.BinaryCodec, storeService corestore.KVStoreService) *Keeper { +func NewKeeper(cdc codec.BinaryCodec, storeService corestore.KVStoreService, clientKeeper types.ClientKeeper, channelKeeperV1 *channelkeeperv1.Keeper, connectionKeeper *connectionkeeper.Keeper) *Keeper { return &Keeper{ - cdc: cdc, - storeService: storeService, + cdc: cdc, + storeService: storeService, + channelKeeperV1: channelKeeperV1, + connectionKeeper: connectionKeeper, + ClientKeeper: clientKeeper, } } @@ -162,3 +173,29 @@ func (k *Keeper) SetNextSequenceSend(ctx context.Context, sourceID string, seque panic(err) } } + +// AliasV1Channel returns a version 2 channel for the given port and channel ID +// by converting the channel into a version 2 channel. +func (k *Keeper) AliasV1Channel(ctx context.Context, portID, channelID string) (types.Counterparty, bool) { + channel, ok := k.channelKeeperV1.GetChannel(ctx, portID, channelID) + if !ok { + return types.Counterparty{}, false + } + // Do not allow channel to be converted into a version 2 counterparty + // if the channel is not OPEN or if it is ORDERED + if channel.State != channeltypesv1.OPEN || channel.Ordering == channeltypesv1.ORDERED { + return types.Counterparty{}, false + } + connection, ok := k.connectionKeeper.GetConnection(ctx, channel.ConnectionHops[0]) + if !ok { + return types.Counterparty{}, false + } + merklePathPrefix := commitmentv2types.NewMerklePath(connection.Counterparty.Prefix.KeyPrefix, []byte("")) + + counterparty := types.Counterparty{ + CounterpartyChannelId: channel.Counterparty.ChannelId, + ClientId: connection.ClientId, + MerklePathPrefix: merklePathPrefix, + } + return counterparty, true +} diff --git a/modules/core/04-channel/v2/keeper/keeper_test.go b/modules/core/04-channel/v2/keeper/keeper_test.go new file mode 100644 index 00000000000..07269aeb645 --- /dev/null +++ b/modules/core/04-channel/v2/keeper/keeper_test.go @@ -0,0 +1,109 @@ +package keeper_test + +import ( + "testing" + + testifysuite "github.com/stretchr/testify/suite" + + "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channeltypes2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + commitmentv2types "github.com/cosmos/ibc-go/v9/modules/core/23-commitment/types/v2" + ibctesting "github.com/cosmos/ibc-go/v9/testing" +) + +func TestKeeperTestSuite(t *testing.T) { + testifysuite.Run(t, new(KeeperTestSuite)) +} + +type KeeperTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain + chainC *ibctesting.TestChain +} + +func (suite *KeeperTestSuite) SetupTest() { + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 3) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + suite.chainB = suite.coordinator.GetChain(ibctesting.GetChainID(2)) + suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(3)) +} + +func (suite *KeeperTestSuite) TestAliasV1Channel() { + var path *ibctesting.Path + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success", + func() {}, + true, + }, + { + "failure: channel not found", + func() { + path.EndpointA.ChannelID = "" + }, + false, + }, + { + "failure: channel not OPEN", + func() { + path.EndpointA.UpdateChannel(func(channel *types.Channel) { channel.State = types.TRYOPEN }) + }, + false, + }, + { + "failure: channel is ORDERED", + func() { + path.EndpointA.UpdateChannel(func(channel *types.Channel) { channel.Ordering = types.ORDERED }) + }, + false, + }, + { + "failure: connection not found", + func() { + path.EndpointA.UpdateChannel(func(channel *types.Channel) { channel.ConnectionHops = []string{ibctesting.InvalidID} }) + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + suite.SetupTest() // reset + + // create a previously existing path on chainA to change the identifiers + // between the path between chainA and chainB + path1 := ibctesting.NewPath(suite.chainA, suite.chainC) + path1.Setup() + + path = ibctesting.NewPath(suite.chainA, suite.chainB) + path.Setup() + + tc.malleate() + + counterparty, found := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeperV2.AliasV1Channel(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID) + + if tc.expPass { + suite.Require().True(found) + + merklePath := commitmentv2types.NewMerklePath([]byte("ibc"), []byte("")) + expCounterparty := channeltypes2.NewCounterparty(path.EndpointA.ClientID, path.EndpointB.ChannelID, merklePath) + suite.Require().Equal(counterparty, expCounterparty) + } else { + suite.Require().False(found) + suite.Require().Equal(counterparty, channeltypes2.Counterparty{}) + } + }) + } +} diff --git a/modules/core/04-channel/v2/keeper/msg_server.go b/modules/core/04-channel/v2/keeper/msg_server.go new file mode 100644 index 00000000000..90dca4d6b14 --- /dev/null +++ b/modules/core/04-channel/v2/keeper/msg_server.go @@ -0,0 +1,56 @@ +package keeper + +import ( + "context" + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" +) + +var _ channeltypesv2.PacketMsgServer = &Keeper{} + +// SendPacket implements the PacketMsgServer SendPacket method. +func (k *Keeper) SendPacket(ctx context.Context, msg *channeltypesv2.MsgSendPacket) (*channeltypesv2.MsgSendPacketResponse, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + sequence, err := k.sendPacket(ctx, msg.SourceId, msg.TimeoutTimestamp, msg.PacketData) + if err != nil { + sdkCtx.Logger().Error("send packet failed", "source-id", msg.SourceId, "error", errorsmod.Wrap(err, "send packet failed")) + return nil, errorsmod.Wrapf(err, "send packet failed for source id: %s", msg.SourceId) + } + + signer, err := sdk.AccAddressFromBech32(msg.Signer) + if err != nil { + sdkCtx.Logger().Error("send packet failed", "error", errorsmod.Wrap(err, "invalid address for msg Signer")) + 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 msg.PacketData { + // cbs := k.PortKeeper.AppRouter.Route(pd.SourcePort) + // err := cbs.OnSendPacket(ctx, msg.SourceId, sequence, msg.TimeoutTimestamp, pd, signer) + // if err != nil { + // return nil, err + // } + // } + + return &channeltypesv2.MsgSendPacketResponse{Sequence: sequence}, nil +} + +func (k Keeper) Acknowledgement(ctx context.Context, acknowledgement *channeltypesv2.MsgAcknowledgement) (*channeltypesv2.MsgAcknowledgementResponse, error) { + panic("implement me") +} + +// RecvPacket implements the PacketMsgServer RecvPacket method. +func (k *Keeper) RecvPacket(ctx context.Context, packet *channeltypesv2.MsgRecvPacket) (*channeltypesv2.MsgRecvPacketResponse, error) { + panic("implement me") +} + +// Timeout implements the PacketMsgServer Timeout method. +func (k *Keeper) Timeout(ctx context.Context, timeout *channeltypesv2.MsgTimeout) (*channeltypesv2.MsgTimeoutResponse, error) { + panic("implement me") +} diff --git a/modules/core/04-channel/v2/keeper/relay.go b/modules/core/04-channel/v2/keeper/relay.go new file mode 100644 index 00000000000..ce7af228895 --- /dev/null +++ b/modules/core/04-channel/v2/keeper/relay.go @@ -0,0 +1,89 @@ +package keeper + +import ( + "context" + "strconv" + + errorsmod "cosmossdk.io/errors" + + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc-go/v9/modules/core/exported" + "github.com/cosmos/ibc-go/v9/modules/core/packet-server/types" +) + +// sendPacket constructs a packet from the input arguments, writes a packet commitment to state +// in order for the packet to be sent to the counterparty. +func (k *Keeper) sendPacket( + ctx context.Context, + sourceID string, + timeoutTimestamp uint64, + data []channeltypesv2.PacketData, +) (uint64, error) { + // Lookup counterparty associated with our source channel to retrieve the destination channel + counterparty, ok := k.GetCounterparty(ctx, sourceID) + if !ok { + // If the counterparty is not found, attempt to retrieve a v1 channel from the channel keeper + // if it exists, then we will convert it to a v2 counterparty and store it in the packet server keeper + // for future use. + // TODO: figure out how aliasing will work when more than one packet data is sent. + if counterparty, ok = k.AliasV1Channel(ctx, data[0].SourcePort, sourceID); ok { + // we can key on just the source channel here since channel ids are globally unique + k.SetCounterparty(ctx, sourceID, counterparty) + } else { + // if neither a counterparty nor channel is found then simply return an error + return 0, errorsmod.Wrap(types.ErrCounterpartyNotFound, sourceID) + } + } + + destID := counterparty.CounterpartyChannelId + clientId := counterparty.ClientId + + // retrieve the sequence send for this channel + // if no packets have been sent yet, initialize the sequence to 1. + sequence, found := k.GetNextSequenceSend(ctx, sourceID) + if !found { + sequence = 1 + } + + // construct packet from given fields and channel state + packet := channeltypesv2.NewPacket(sequence, sourceID, destID, timeoutTimestamp, data...) + + if err := packet.ValidateBasic(); err != nil { + return 0, errorsmod.Wrapf(channeltypes.ErrInvalidPacket, "constructed packet failed basic validation: %v", err) + } + + // check that the client of counterparty chain is still active + if status := k.ClientKeeper.GetClientStatus(ctx, clientId); status != exported.Active { + return 0, errorsmod.Wrapf(clienttypes.ErrClientNotActive, "client (%s) status is %s", clientId, status) + } + + // retrieve latest height and timestamp of the client of counterparty chain + latestHeight := k.ClientKeeper.GetClientLatestHeight(ctx, clientId) + if latestHeight.IsZero() { + return 0, errorsmod.Wrapf(clienttypes.ErrInvalidHeight, "cannot send packet using client (%s) with zero height", clientId) + } + + latestTimestamp, err := k.ClientKeeper.GetClientTimestampAtHeight(ctx, clientId, latestHeight) + if err != nil { + return 0, err + } + // check if packet is timed out on the receiving chain + timeout := channeltypes.NewTimeoutWithTimestamp(timeoutTimestamp) + if timeout.TimestampElapsed(latestTimestamp) { + return 0, errorsmod.Wrap(timeout.ErrTimeoutElapsed(latestHeight, latestTimestamp), "invalid packet timeout") + } + + commitment := channeltypesv2.CommitPacket(packet) + + // bump the sequence and set the packet commitment, so it is provable by the counterparty + k.SetNextSequenceSend(ctx, sourceID, sequence+1) + k.SetPacketCommitment(ctx, sourceID, packet.GetSequence(), commitment) + + k.Logger(ctx).Info("packet sent", "sequence", strconv.FormatUint(packet.Sequence, 10), "dest_id", packet.DestinationId, "src_id", packet.SourceId) + + EmitSendPacketEvents(ctx, packet) + + return sequence, nil +} diff --git a/modules/core/04-channel/v2/types/counterparty.go b/modules/core/04-channel/v2/types/counterparty.go new file mode 100644 index 00000000000..d63a92da3e8 --- /dev/null +++ b/modules/core/04-channel/v2/types/counterparty.go @@ -0,0 +1,34 @@ +package types + +import ( + errorsmod "cosmossdk.io/errors" + + commitmenttypes "github.com/cosmos/ibc-go/v9/modules/core/23-commitment/types/v2" + host "github.com/cosmos/ibc-go/v9/modules/core/24-host" +) + +// NewCounterparty creates a new Counterparty instance +func NewCounterparty(clientID, counterpartyChannelID string, merklePathPrefix commitmenttypes.MerklePath) Counterparty { + return Counterparty{ + ClientId: clientID, + CounterpartyChannelId: counterpartyChannelID, + MerklePathPrefix: merklePathPrefix, + } +} + +// Validate validates the Counterparty +func (c Counterparty) Validate() error { + if err := host.ClientIdentifierValidator(c.ClientId); err != nil { + return err + } + + if err := host.ChannelIdentifierValidator(c.CounterpartyChannelId); err != nil { + return err + } + + if err := c.MerklePathPrefix.ValidateAsPrefix(); err != nil { + return errorsmod.Wrap(ErrInvalidCounterparty, err.Error()) + } + + return nil +} diff --git a/modules/core/04-channel/v2/types/errors.go b/modules/core/04-channel/v2/types/errors.go index bf32cfe0289..25acab9190d 100644 --- a/modules/core/04-channel/v2/types/errors.go +++ b/modules/core/04-channel/v2/types/errors.go @@ -4,8 +4,9 @@ import ( errorsmod "cosmossdk.io/errors" ) -// IBC channel sentinel errors var ( - ErrInvalidPacket = errorsmod.Register(SubModuleName, 1, "invalid packet") - ErrInvalidPayload = errorsmod.Register(SubModuleName, 2, "invalid payload") + ErrInvalidCounterparty = errorsmod.Register(SubModuleName, 1, "invalid counterparty") + ErrCounterpartyNotFound = errorsmod.Register(SubModuleName, 2, "counterparty not found") + ErrInvalidPacket = errorsmod.Register(SubModuleName, 3, "invalid packet") + ErrInvalidPayload = errorsmod.Register(SubModuleName, 4, "invalid payload") ) diff --git a/modules/core/04-channel/v2/types/expected_keepers.go b/modules/core/04-channel/v2/types/expected_keepers.go new file mode 100644 index 00000000000..31833373ed9 --- /dev/null +++ b/modules/core/04-channel/v2/types/expected_keepers.go @@ -0,0 +1,25 @@ +package types + +import ( + "context" + + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + "github.com/cosmos/ibc-go/v9/modules/core/exported" +) + +type ClientKeeper interface { + // VerifyMembership retrieves the light client module for the clientID and verifies the proof of the existence of a key-value pair at a specified height. + VerifyMembership(ctx context.Context, clientID string, height exported.Height, delayTimePeriod uint64, delayBlockPeriod uint64, proof []byte, path exported.Path, value []byte) error + // VerifyNonMembership retrieves the light client module for the clientID and verifies the absence of a given key at a specified height. + VerifyNonMembership(ctx context.Context, clientID string, height exported.Height, delayTimePeriod uint64, delayBlockPeriod uint64, proof []byte, path exported.Path) error + // GetClientStatus returns the status of a client given the client ID + GetClientStatus(ctx context.Context, clientID string) exported.Status + // GetClientLatestHeight returns the latest height of a client given the client ID + GetClientLatestHeight(ctx context.Context, clientID string) clienttypes.Height + // GetClientTimestampAtHeight returns the timestamp for a given height on the client + // given its client ID and height + GetClientTimestampAtHeight(ctx context.Context, clientID string, height exported.Height) (uint64, error) + + // GetCreator returns the creator of the client denoted by the clientID. + GetCreator(ctx context.Context, clientID string) (string, bool) +} diff --git a/modules/core/api/module.go b/modules/core/api/module.go new file mode 100644 index 00000000000..f54f12d86a1 --- /dev/null +++ b/modules/core/api/module.go @@ -0,0 +1,29 @@ +package api + +import ( + "context" + sdk "github.com/cosmos/cosmos-sdk/types" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" +) + +// IBCModule defines an interface that implements all the callbacks +// that modules must define as specified in IBC Protocol V2. +type IBCModule interface { + // OnSendPacket is executed when a packet is being sent from sending chain. + // this callback is provided with the source and destination IDs, the signer, the packet sequence and the packet data + // for this specific application. + OnSendPacket( + ctx context.Context, + sourceID string, + destinationID string, + sequence uint64, + data channeltypesv2.PacketData, + signer sdk.AccAddress, + ) error + + // OnRecvPacket + + // OnAcknowledgementPacket + + // OnTimeoutPacket +} diff --git a/modules/core/keeper/keeper.go b/modules/core/keeper/keeper.go index 665a43ee096..c58fcffbd70 100644 --- a/modules/core/keeper/keeper.go +++ b/modules/core/keeper/keeper.go @@ -13,6 +13,7 @@ import ( clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" connectionkeeper "github.com/cosmos/ibc-go/v9/modules/core/03-connection/keeper" channelkeeper "github.com/cosmos/ibc-go/v9/modules/core/04-channel/keeper" + channelkeeperv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/keeper" portkeeper "github.com/cosmos/ibc-go/v9/modules/core/05-port/keeper" porttypes "github.com/cosmos/ibc-go/v9/modules/core/05-port/types" packetserver "github.com/cosmos/ibc-go/v9/modules/core/packet-server/keeper" @@ -24,6 +25,7 @@ type Keeper struct { ClientKeeper *clientkeeper.Keeper ConnectionKeeper *connectionkeeper.Keeper ChannelKeeper *channelkeeper.Keeper + ChannelKeeperV2 *channelkeeperv2.Keeper PacketServerKeeper *packetserver.Keeper PortKeeper *portkeeper.Keeper @@ -51,12 +53,14 @@ func NewKeeper( portKeeper := portkeeper.NewKeeper() channelKeeper := channelkeeper.NewKeeper(cdc, storeService, clientKeeper, connectionKeeper) packetKeeper := packetserver.NewKeeper(cdc, storeService, channelKeeper, clientKeeper) + channelKeeperV2 := channelkeeperv2.NewKeeper(cdc, storeService, clientKeeper, channelKeeper, connectionKeeper) return &Keeper{ cdc: cdc, ClientKeeper: clientKeeper, ConnectionKeeper: connectionKeeper, ChannelKeeper: channelKeeper, + ChannelKeeperV2: channelKeeperV2, PacketServerKeeper: packetKeeper, PortKeeper: portKeeper, authority: authority,