-
Notifications
You must be signed in to change notification settings - Fork 679
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
Changes from all commits
63aba09
e715162
99a5077
7131853
b86dd59
10b053d
8257cf2
ffac372
d7b0b7f
32b99fb
0785a21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package keeper | ||
|
||
import ( | ||
"bytes" | ||
"strconv" | ||
|
||
errorsmod "cosmossdk.io/errors" | ||
|
@@ -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 | ||
|
@@ -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 | ||
// 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
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?