Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core/eureka): add timeout handler #7060

Merged
merged 11 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/core/04-channel/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func emitAcknowledgePacketEvent(ctx sdk.Context, packet types.Packet, channel ty

// emitTimeoutPacketEvent emits a timeout packet event. It will be emitted both the first time a packet
// is timed out for a certain sequence and for all duplicate timeouts.
func emitTimeoutPacketEvent(ctx sdk.Context, packet types.Packet, channel types.Channel) {
func EmitTimeoutPacketEvent(ctx sdk.Context, packet types.Packet, channel types.Channel) {
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
types.EventTypeTimeoutPacket,
Expand Down
2 changes: 1 addition & 1 deletion modules/core/04-channel/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func (k *Keeper) SetPacketCommitment(ctx sdk.Context, portID, channelID string,
store.Set(host.PacketCommitmentKey(portID, channelID, sequence), commitmentHash)
}

func (k *Keeper) deletePacketCommitment(ctx sdk.Context, portID, channelID string, sequence uint64) {
func (k *Keeper) DeletePacketCommitment(ctx sdk.Context, portID, channelID string, sequence uint64) {
store := ctx.KVStore(k.storeKey)
store.Delete(host.PacketCommitmentKey(portID, channelID, sequence))
}
Expand Down
2 changes: 1 addition & 1 deletion modules/core/04-channel/keeper/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ func (k *Keeper) AcknowledgePacket(
}

// Delete packet commitment, since the packet has been acknowledged, the commitement is no longer necessary
k.deletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
k.DeletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())

// log that a packet has been acknowledged
k.Logger(ctx).Info(
Expand Down
8 changes: 4 additions & 4 deletions modules/core/04-channel/keeper/timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (k *Keeper) TimeoutPacket(
commitment := k.GetPacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())

if len(commitment) == 0 {
emitTimeoutPacketEvent(ctx, packet, channel)
EmitTimeoutPacketEvent(ctx, packet, channel)
// This error indicates that the timeout has already been relayed
// or there is a misconfigured relayer attempting to prove a timeout
// for a packet never sent. Core IBC will treat this error as a no-op in order to
Expand Down Expand Up @@ -148,7 +148,7 @@ func (k *Keeper) TimeoutExecuted(
)
}

k.deletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())
k.DeletePacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())

// if an upgrade is in progress, handling packet flushing and update channel state appropriately
if channel.State == types.FLUSHING && channel.Ordering == types.UNORDERED {
Expand Down Expand Up @@ -183,7 +183,7 @@ func (k *Keeper) TimeoutExecuted(
)

// emit an event marking that we have processed the timeout
emitTimeoutPacketEvent(ctx, packet, channel)
EmitTimeoutPacketEvent(ctx, packet, channel)

return nil
}
Expand Down Expand Up @@ -236,7 +236,7 @@ func (k *Keeper) TimeoutOnClose(
commitment := k.GetPacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())

if len(commitment) == 0 {
emitTimeoutPacketEvent(ctx, packet, channel)
EmitTimeoutPacketEvent(ctx, packet, channel)
// This error indicates that the timeout has already been relayed
// or there is a misconfigured relayer attempting to prove a timeout
// for a packet never sent. Core IBC will treat this error as a no-op in order to
Expand Down
88 changes: 82 additions & 6 deletions modules/core/packet-server/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"bytes"
"strconv"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -99,12 +100,10 @@ func (k Keeper) SendPacket(
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)
// log that a packet has been sent
k.Logger(ctx).Info("packet sent", "sequence", strconv.FormatUint(packet.Sequence, 10), "src_port", packet.SourcePort, "src_channel", packet.SourceChannel, "dst_port", packet.DestinationPort, "dst_channel", packet.DestinationChannel)

channelkeeper.EmitSendPacketEvent(ctx, packet, sentinelChannel(sourceChannel), timeoutHeight)

// return the sequence
return sequence, nil
Expand Down Expand Up @@ -182,6 +181,83 @@ func (k Keeper) RecvPacket(
return nil
}

func (k Keeper) TimeoutPacket(
ctx sdk.Context,
packet channeltypes.Packet,
proof []byte,
proofHeight exported.Height,
_ uint64,
) error {
if packet.ProtocolVersion != channeltypes.IBC_VERSION_2 {
return channeltypes.ErrInvalidPacket
}
// Lookup counterparty associated with our channel and ensure that destination channel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do protocol version check here too?

// is the expected counterparty
counterparty, ok := k.ClientKeeper.GetCounterparty(ctx, packet.SourceChannel)
if !ok {
return channeltypes.ErrChannelNotFound
}

if counterparty.ClientId != packet.DestinationChannel {
return channeltypes.ErrInvalidChannelIdentifier
}

// check that timeout height or timeout timestamp has passed on the other end
proofTimestamp, err := k.ClientKeeper.GetClientTimestampAtHeight(ctx, packet.SourceChannel, proofHeight)
if err != nil {
return err
}

timeout := channeltypes.NewTimeout(packet.GetTimeoutHeight().(clienttypes.Height), packet.GetTimeoutTimestamp())
if !timeout.Elapsed(proofHeight.(clienttypes.Height), proofTimestamp) {
return errorsmod.Wrap(timeout.ErrTimeoutNotReached(proofHeight.(clienttypes.Height), proofTimestamp), "packet timeout not reached")
}

// check that the commitment has not been cleared and that it matches the packet sent by relayer
commitment := k.ChannelKeeper.GetPacketCommitment(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence())

if len(commitment) == 0 {
channelkeeper.EmitTimeoutPacketEvent(ctx, packet, sentinelChannel(packet.SourceChannel))
// This error indicates that the timeout has already been relayed
// or there is a misconfigured relayer attempting to prove a timeout
// for a packet never sent. Core IBC will treat this error as a no-op in order to
// prevent an entire relay transaction from failing and consuming unnecessary fees.
return channeltypes.ErrNoOpMsg
}

packetCommitment := channeltypes.CommitPacket(packet)
// verify we sent the packet and haven't cleared it out yet
if !bytes.Equal(commitment, packetCommitment) {
return errorsmod.Wrapf(channeltypes.ErrInvalidPacket, "packet commitment bytes are not equal: got (%v), expected (%v)", commitment, packetCommitment)
}

// verify packet receipt absence
path := host.PacketReceiptKey(packet.DestinationPort, packet.DestinationChannel, packet.Sequence)
merklePath := types.BuildMerklePath(counterparty.MerklePathPrefix, path)

if err := k.ClientKeeper.VerifyNonMembership(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guess a client status check might also be nice here if working it in the tests isn't too much of a pain in the ass

ctx,
packet.SourceChannel,
proofHeight,
0, 0,
proof,
merklePath,
); err != nil {
return errorsmod.Wrapf(err, "failed packet receipt absence verification for client (%s)", packet.SourceChannel)
}

// delete packet commitment to prevent replay
k.ChannelKeeper.DeletePacketCommitment(ctx, packet.SourcePort, packet.SourceChannel, packet.Sequence)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can also do the log here (think we also missed it for Send)

// log that a packet has been timed out
k.Logger(ctx).Info("packet timed out", "sequence", strconv.FormatUint(packet.Sequence, 10), "src_port", packet.SourcePort, "src_channel", packet.SourceChannel, "dst_port", packet.DestinationPort, "dst_channel", packet.DestinationChannel)

// emit timeout events
channelkeeper.EmitTimeoutPacketEvent(ctx, packet, sentinelChannel(packet.SourceChannel))

return nil
}

// sentinelChannel creates a sentinel channel for use in events for Eureka protocol handlers.
func sentinelChannel(clientID string) channeltypes.Channel {
return channeltypes.Channel{Ordering: channeltypes.UNORDERED, ConnectionHops: []string{clientID}}
Expand Down
181 changes: 181 additions & 0 deletions modules/core/packet-server/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,184 @@ func (suite *KeeperTestSuite) TestRecvPacket() {
})
}
}

func (suite *KeeperTestSuite) TestTimeoutPacket() {
var (
path *ibctesting.Path
packet channeltypes.Packet
freezeClient bool
)

testCases := []struct {
name string
malleate func()
expError error
}{
{
"success with timeout height",
func() {
// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")
},
nil,
},
{
"success with timeout timestamp",
func() {
// disable timeout height and set timeout timestamp to a past timestamp
packet.TimeoutHeight = clienttypes.Height{}
packet.TimeoutTimestamp = uint64(suite.chainB.GetContext().BlockTime().UnixNano())

// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")
},
nil,
},
{
"failure: invalid protocol version",
func() {
// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")

packet.ProtocolVersion = channeltypes.IBC_VERSION_1
packet.AppVersion = ""
},
channeltypes.ErrInvalidPacket,
},
{
"failure: counterparty not found",
func() {
// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")

packet.SourceChannel = ibctesting.FirstChannelID
},
channeltypes.ErrChannelNotFound,
},
{
"failure: counterparty client identifier different than source channel",
func() {
// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")

packet.DestinationChannel = ibctesting.FirstChannelID
},
channeltypes.ErrInvalidChannelIdentifier,
},
{
"failure: packet has not timed out yet",
func() {
packet.TimeoutHeight = defaultTimeoutHeight

// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")
},
channeltypes.ErrTimeoutNotReached,
},
{
"failure: packet already timed out",
func() {}, // equivalent to not sending packet at all
channeltypes.ErrNoOpMsg,
},
{
"failure: packet does not match commitment",
func() {
// send a different packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, []byte("different data"))
suite.Require().NoError(err, "send packet failed")
},
channeltypes.ErrInvalidPacket,
},
{
"failure: client status invalid",
func() {
// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")

freezeClient = true
},
clienttypes.ErrClientNotActive,
},
{
"failure: verify non-membership failed",
func() {
// send packet
_, err := suite.chainA.App.GetPacketServer().SendPacket(suite.chainA.GetContext(), nil, packet.SourceChannel, packet.SourcePort, packet.DestinationPort,
packet.TimeoutHeight, packet.TimeoutTimestamp, packet.AppVersion, packet.Data)
suite.Require().NoError(err, "send packet failed")

// set packet receipt to mock a valid past receive
suite.chainB.App.GetIBCKeeper().ChannelKeeper.SetPacketReceipt(suite.chainB.GetContext(), packet.DestinationPort, packet.DestinationChannel, packet.Sequence)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to just mock the receive here. I could do an actual receive but then would have to make sure the packet isn't timeout out for when the receive occurs but is timed out for the timeout and it ended up looking more confusing setup than just setting directly

},
commitmenttypes.ErrInvalidProof,
},
}

for _, tc := range testCases {
tc := tc

suite.Run(tc.name, func() {
suite.SetupTest()
// initialize freezeClient to false
freezeClient = false

path = ibctesting.NewPath(suite.chainA, suite.chainB)
path.SetupV2()

// create default packet with a timed out height
// test cases may mutate timeout values
timeoutHeight := clienttypes.GetSelfHeight(suite.chainB.GetContext())
packet = channeltypes.NewPacketWithVersion(ibctesting.MockPacketData, 1, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ClientID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ClientID, timeoutHeight, disabledTimeoutTimestamp, "")

tc.malleate()

// need to update chainA's client representing chainB to prove missing ack
// commit the changes and update the clients
suite.coordinator.CommitBlock(path.EndpointA.Chain)
suite.Require().NoError(path.EndpointB.UpdateClient())
suite.Require().NoError(path.EndpointA.UpdateClient())

// get proof of packet receipt absence from chainB
receiptKey := host.PacketReceiptKey(packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence())
proof, proofHeight := path.EndpointB.QueryProof(receiptKey)

if freezeClient {
// 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)
}

err := suite.chainA.App.GetPacketServer().TimeoutPacket(suite.chainA.GetContext(), packet, proof, proofHeight, 0)

expPass := tc.expError == nil
if expPass {
suite.Require().NoError(err)

commitment := suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetPacketCommitment(suite.chainA.GetContext(), packet.DestinationPort, packet.DestinationChannel, packet.Sequence)
suite.Require().Nil(commitment, "packet commitment not deleted")
} else {
suite.Require().Error(err)
suite.Require().ErrorIs(err, tc.expError)
}
})
}
}
2 changes: 1 addition & 1 deletion modules/core/packet-server/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ChannelKeeper interface {
GetPacketCommitment(ctx sdk.Context, portID string, channelID string, sequence uint64) []byte

// DeletePacketCommitment deletes the packet commitment hash under the commitment path
// DeletePacketCommitment(ctx sdk.Context, portID string, channelID string, sequence uint64)
DeletePacketCommitment(ctx sdk.Context, portID string, channelID string, sequence uint64)

// SetNextSequenceSend writes the next send sequence under the sequence path
// This is a public path that is standardized by the IBC specification
Expand Down
Loading