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

Timing out packets on ordered channels when in flushing state aborts upgrade and closes channel #4475

34 changes: 21 additions & 13 deletions modules/core/04-channel/keeper/timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper

import (
"bytes"
"fmt"
"strconv"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -150,15 +151,8 @@ func (k Keeper) TimeoutExecuted(

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

if channel.Ordering == types.ORDERED {
channel.State = types.CLOSED
k.SetChannel(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), channel)
emitChannelClosedEvent(ctx, packet, channel)
}

// TODO: handle situation outlined in https://github.com/cosmos/ibc-go/issues/4454
// if an upgrade is in progress, handling packet flushing and update channel state appropriately
if channel.State == types.FLUSHING {
if channel.State == types.FLUSHING && channel.Ordering == types.UNORDERED {
counterpartyUpgrade, found := k.GetCounterpartyUpgrade(ctx, packet.GetSourcePort(), packet.GetSourceChannel())
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's say the timeout on counterpartyUpgrade is not set yet (TRY has been called, we are in state FLUSHING but counterparty timeout is not yet set), but we are in the process of timing out a packet on an ordered channel.

We would end up skipping the code in timeout.IsValid(), timing out the packet and closing the channel, but the upgrade state would still remain in store.

Copy link
Contributor

Choose a reason for hiding this comment

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

guess we should be able to conditionally delete in the if channel.Ordering == types.ORDERED block yea?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

really nice catch, I guess then the situation we can be in is that

timeout is invalid, LastSequenceSend is 0, but upgradeFields are set. We could use this as the check for also triggering the abort.

if !found {
return errorsmod.Wrapf(types.ErrUpgradeNotFound, "counterparty upgrade not found for channel: %s", packet.GetSourceChannel())
Expand All @@ -171,17 +165,31 @@ func (k Keeper) TimeoutExecuted(
// packet flushing timeout has expired, abort the upgrade and return nil,
// committing an error receipt to state, restoring the channel and successfully timing out the packet.
k.MustAbortUpgrade(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), err)
return nil
}

// set the channel state to flush complete if all packets have been flushed.
if !k.HasInflightPackets(ctx, packet.GetSourcePort(), packet.GetSourceChannel()) {
} else if !k.HasInflightPackets(ctx, packet.GetSourcePort(), packet.GetSourceChannel()) {
// set the channel state to flush complete if all packets have been flushed.
channel.State = types.FLUSHCOMPLETE
k.SetChannel(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), channel)
}
// upgrade fields have been set but the timeout has not. This can happen when the counterparty
// upgrade is partially written in WriteUpgradeTryChannel.
} else if counterpartyUpgrade.Fields.Version != "" {
k.MustAbortUpgrade(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), fmt.Errorf("uh oh"))
}
}

if channel.Ordering == types.ORDERED {
// NOTE: if the channel is ORDERED and a packet is timed out in FLUSHING state then
// the upgrade is aborted and the channel is set to CLOSED.
if channel.State == types.FLUSHING {
// an error receipt is written to state and the channel is restored to OPEN
k.MustAbortUpgrade(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), types.ErrPacketTimeout)
}

channel.State = types.CLOSED
k.SetChannel(ctx, packet.GetSourcePort(), packet.GetSourceChannel(), channel)
emitChannelClosedEvent(ctx, packet, channel)
}

k.Logger(ctx).Info(
"packet timed-out",
"sequence", strconv.FormatUint(packet.GetSequence(), 10),
Expand Down
45 changes: 45 additions & 0 deletions modules/core/04-channel/keeper/timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,51 @@ func (suite *KeeperTestSuite) TestTimeoutExecuted() {
suite.Require().False(found, "error receipt should not be written")
},
},
{
"ordered channel is closed and upgrade is aborted when timeout is executed",
func() {
path.SetChannelOrdered()
suite.coordinator.Setup(path)

timeoutHeight := clienttypes.GetSelfHeight(suite.chainB.GetContext())
timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().UnixNano())

sequence, err := path.EndpointA.SendPacket(timeoutHeight, timeoutTimestamp, ibctesting.MockPacketData)
suite.Require().NoError(err)

packet = types.NewPacket(ibctesting.MockPacketData, sequence, path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, timeoutHeight, timeoutTimestamp)
chanCap = suite.chainA.GetChannelCapability(path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)

channel := path.EndpointA.GetChannel()
channel.State = types.FLUSHING
path.EndpointA.SetChannel(channel)
path.EndpointA.SetChannelUpgrade(types.Upgrade{
Fields: path.EndpointA.GetProposedUpgrade().Fields,
Timeout: types.NewTimeout(clienttypes.ZeroHeight(), 1),
})
path.EndpointA.SetChannelCounterpartyUpgrade(types.Upgrade{
Timeout: types.NewTimeout(clienttypes.ZeroHeight(), 1),
})
},
func(packetCommitment []byte, err error) {
suite.Require().NoError(err)

channel := path.EndpointA.GetChannel()
suite.Require().Equal(types.CLOSED, channel.State, "channel state should be CLOSED")

upgrade, found := suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetUpgrade(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
Copy link
Contributor

Choose a reason for hiding this comment

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

possible future refactor: checking post-state for cleared upgrade state is done commonly iirc, could be nice if possible to have a helper for this.

suite.Require().False(found, "upgrade should not be present")
suite.Require().Equal(types.Upgrade{}, upgrade, "upgrade should be zero value")

upgrade, found = suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetCounterpartyUpgrade(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
suite.Require().False(found, "counterparty upgrade should not be present")
suite.Require().Equal(types.Upgrade{}, upgrade, "counterparty upgrade should be zero value")

errorReceipt, found := suite.chainA.App.GetIBCKeeper().ChannelKeeper.GetUpgradeErrorReceipt(suite.chainA.GetContext(), path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID)
suite.Require().True(found, "error receipt should be present")
suite.Require().Equal(channel.UpgradeSequence, errorReceipt.Sequence, "error receipt sequence should be equal to channel upgrade sequence")
},
},
}

for i, tc := range testCases {
Expand Down