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: add acknowledgePacket handler to channel/v2 #7412

Merged
merged 9 commits into from
Oct 10, 2024
13 changes: 9 additions & 4 deletions modules/core/04-channel/v2/keeper/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ func EmitSendPacketEvents(ctx context.Context, packet channeltypesv2.Packet) {
// TODO: https://github.com/cosmos/ibc-go/issues/7386
}

// EmitTimeoutPacketEvents emits events for the TimeoutPacket handler.
func EmitTimeoutPacketEvents(ctx context.Context, packet channeltypesv2.Packet) {
// EmitRecvPacketEvents emits events for the RecvPacket handler.
func EmitRecvPacketEvents(ctx context.Context, packet channeltypesv2.Packet) {
// TODO: https://github.com/cosmos/ibc-go/issues/7386
}

// EmitRecvPacketEvents emits events for the RecvPacket handler.
func EmitRecvPacketEvents(ctx context.Context, packet channeltypesv2.Packet) {
// EmitAcknowledgePacketEvents emits events for the AcknowledgePacket handler.
func EmitAcknowledgePacketEvents(ctx context.Context, packet channeltypesv2.Packet) {
// TODO: https://github.com/cosmos/ibc-go/issues/7386
}

// EmitTimeoutPacketEvents emits events for the TimeoutPacket handler.
func EmitTimeoutPacketEvents(ctx context.Context, packet channeltypesv2.Packet) {
// TODO: https://github.com/cosmos/ibc-go/issues/7386
}
35 changes: 29 additions & 6 deletions modules/core/04-channel/v2/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,27 @@ func (k *Keeper) GetCounterparty(ctx context.Context, clientID string) (types.Co
}

// GetPacketReceipt returns the packet receipt from the packet receipt path based on the sourceID and sequence.
func (k *Keeper) GetPacketReceipt(ctx context.Context, sourceID string, sequence uint64) (string, bool) {
func (k *Keeper) GetPacketReceipt(ctx context.Context, sourceID string, sequence uint64) ([]byte, bool) {
store := k.storeService.OpenKVStore(ctx)
bz, err := store.Get(hostv2.PacketReceiptKey(sourceID, sequence))
if err != nil {
panic(err)
}
if len(bz) == 0 {
return "", false
return nil, false
}
return string(bz), true
return bz, true
}

// HasPacketRceipt returns true if the packet receipt exists, otherwise false.
func (k *Keeper) HasPacketReceipt(ctx context.Context, sourceID string, sequence uint64) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Broader question but I take this line to bring it up: given #7428, do we want to also use sourceChannel and destinationChannel in all function arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed we do, will update when others are merged

store := k.storeService.OpenKVStore(ctx)
has, err := store.Has(hostv2.PacketReceiptKey(sourceID, sequence))
if err != nil {
panic(err)
}

return has
}

// SetPacketReceipt writes the packet receipt under the receipt path
Expand Down Expand Up @@ -117,16 +128,16 @@ func (k *Keeper) HasPacketAcknowledgement(ctx context.Context, sourceID string,
}

// GetPacketCommitment returns the packet commitment hash under the commitment path.
func (k *Keeper) GetPacketCommitment(ctx context.Context, sourceID string, sequence uint64) (string, bool) {
func (k *Keeper) GetPacketCommitment(ctx context.Context, sourceID string, sequence uint64) []byte {
store := k.storeService.OpenKVStore(ctx)
bz, err := store.Get(hostv2.PacketCommitmentKey(sourceID, sequence))
if err != nil {
panic(err)
}
if len(bz) == 0 {
return "", false
return nil
}
return string(bz), true
return bz
}

// SetPacketCommitment writes the commitment hash under the commitment path.
Expand Down Expand Up @@ -192,3 +203,15 @@ func (k *Keeper) AliasV1Channel(ctx context.Context, portID, channelID string) (
}
return counterparty, true
}

// getV1Counterparty attempts to retrieve a v1 channel from the channel keeper if it exists, then converts it
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved from relay.go to keeper.go

// to a v2 counterparty and stores it in the v2 channel keeper for future use
func (k *Keeper) getV1Counterparty(ctx context.Context, port, id string) (types.Counterparty, bool) {
if counterparty, ok := k.AliasV1Channel(ctx, port, id); ok {
// we can key on just the channel here since channel ids are globally unique
k.SetCounterparty(ctx, id, counterparty)
return counterparty, true
}

return types.Counterparty{}, false
}
38 changes: 36 additions & 2 deletions modules/core/04-channel/v2/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,42 @@ func (k *Keeper) SendPacket(ctx context.Context, msg *channeltypesv2.MsgSendPack
return &channeltypesv2.MsgSendPacketResponse{Sequence: sequence}, nil
}

func (*Keeper) Acknowledgement(ctx context.Context, acknowledgement *channeltypesv2.MsgAcknowledgement) (*channeltypesv2.MsgAcknowledgementResponse, error) {
panic("implement me")
func (k *Keeper) Acknowledgement(ctx context.Context, msg *channeltypesv2.MsgAcknowledgement) (*channeltypesv2.MsgAcknowledgementResponse, error) {
sdkCtx := sdk.UnwrapSDKContext(ctx)
relayer, err := sdk.AccAddressFromBech32(msg.Signer)
if err != nil {
sdkCtx.Logger().Error("acknowledgement failed", "error", errorsmod.Wrap(err, "Invalid address for msg Signer"))
return nil, errorsmod.Wrap(err, "Invalid address for msg Signer")
}

cacheCtx, writeFn := sdkCtx.CacheContext()
err = k.acknowledgePacket(cacheCtx, msg.Packet, msg.Acknowledgement, msg.ProofAcked, msg.ProofHeight)

switch err {
case nil:
writeFn()
case channeltypesv1.ErrNoOpMsg:
// no-ops do not need event emission as they will be ignored
sdkCtx.Logger().Debug("no-op on redundant relay", "source-id", msg.Packet.SourceChannel)
return &channeltypesv2.MsgAcknowledgementResponse{Result: channeltypesv1.NOOP}, nil
default:
sdkCtx.Logger().Error("acknowledgement failed", "source-id", msg.Packet.SourceChannel, "error", errorsmod.Wrap(err, "acknowledge packet verification failed"))
return nil, errorsmod.Wrap(err, "acknowledge packet verification failed")
}

_ = relayer

// 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 nil, nil
}

// RecvPacket implements the PacketMsgServer RecvPacket method.
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed relay.go to packet.go

Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,6 @@ import (
"github.com/cosmos/ibc-go/v9/modules/core/packet-server/types"
)

// getV1Counterparty attempts to retrieve a v1 channel from the channel keeper if it exists, then converts it
// to a v2 counterparty and stores it in the v2 channel keeper for future use
func (k *Keeper) getV1Counterparty(ctx context.Context, port, id string) (channeltypesv2.Counterparty, bool) {
if counterparty, ok := k.AliasV1Channel(ctx, port, id); ok {
// we can key on just the channel here since channel ids are globally unique
k.SetCounterparty(ctx, id, counterparty)
return counterparty, true
}

return channeltypesv2.Counterparty{}, false
}

// 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(
Expand Down Expand Up @@ -106,7 +94,7 @@ func (k *Keeper) sendPacket(
// The packet handler will verify that the packet has not timed out and that the
// counterparty stored a packet commitment. If successful, a packet receipt is stored
// to indicate to the counterparty successful delivery.
func (k Keeper) recvPacket(
func (k *Keeper) recvPacket(
ctx context.Context,
packet channeltypesv2.Packet,
proof []byte,
Expand Down Expand Up @@ -137,8 +125,7 @@ func (k Keeper) recvPacket(
// REPLAY PROTECTION: Packet receipts will indicate that a packet has already been received
// on unordered channels. Packet receipts must not be pruned, unless it has been marked stale
// by the increase of the recvStartSequence.
_, found := k.GetPacketReceipt(ctx, packet.DestinationChannel, packet.Sequence)
if found {
if k.HasPacketReceipt(ctx, packet.DestinationChannel, packet.Sequence) {
EmitRecvPacketEvents(ctx, packet)
// This error indicates that the packet has already been relayed. Core IBC will
// treat this error as a no-op in order to prevent an entire relay transaction
Expand Down Expand Up @@ -173,14 +160,70 @@ func (k Keeper) recvPacket(
return nil
}

func (k *Keeper) acknowledgePacket(ctx context.Context, packet channeltypesv2.Packet, acknowledgement channeltypesv2.Acknowledgement, proof []byte, proofHeight exported.Height) error {
// Lookup counterparty associated with our channel and ensure
// that the packet was indeed sent by our counterparty.
counterparty, ok := k.GetCounterparty(ctx, packet.SourceChannel)
if !ok {
return errorsmod.Wrap(types.ErrChannelNotFound, packet.SourceChannel)
}

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

commitment := k.GetPacketCommitment(ctx, packet.SourceChannel, packet.Sequence)
if len(commitment) == 0 {
// TODO: signal noop in events?
EmitAcknowledgePacketEvents(ctx, packet)

// This error indicates that the acknowledgement has already been relayed
// or there is a misconfigured relayer attempting to prove an acknowledgement
// 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 := channeltypesv2.CommitPacket(packet)

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

path := hostv2.PacketAcknowledgementKey(packet.DestinationChannel, packet.Sequence)
merklePath := types.BuildMerklePath(counterparty.MerklePathPrefix, path)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

BuildMerklePath, used here and other funcs in this file is imported from the packet-server package.

We should move this to the channel/v2 types pkg afaik. We should not depend on anything in packet-server (which will ultimately be removed).

I'd also like some insight into the implementation of BuildMerklePath.
Some questions I have are:
Why are we not using the MerklePrefix type for the "ibc" prefix used by counterparties - instead we're using MerklePath.
Secondly, implementation looks complex and kinda funky, haven't stepped through to see exactly why that is, was hoping someone could explain it for me.

Copy link
Contributor

Choose a reason for hiding this comment

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

re dependencies: I think we can do a pass at the end and make sure there is no dependency on v1 anything within the channel/v2 package.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

cc. @AdityaSripal @DimitrisJim on this func


if err := k.ClientKeeper.VerifyMembership(
ctx,
clientID,
proofHeight,
0, 0,
proof,
merklePath,
channeltypesv2.CommitAcknowledgement(acknowledgement),
); err != nil {
return errorsmod.Wrapf(err, "failed packet acknowledgement verification for client (%s)", clientID)
}

k.DeletePacketCommitment(ctx, packet.SourceChannel, packet.Sequence)

k.Logger(ctx).Info("packet acknowledged", "sequence", strconv.FormatUint(packet.GetSequence(), 10), "source_channel_id", packet.GetSourceChannel(), "destination_channel_id", packet.GetDestinationChannel())

EmitAcknowledgePacketEvents(ctx, packet)

return nil
}

// timeoutPacket implements the timeout logic required by a packet handler.
// The packet is checked for correctness including asserting that the packet was
// sent and received on clients which are counterparties for one another.
// If no packet commitment exists, a no-op error is returned, otherwise
// an absence proof of the packet receipt is performed to ensure that the packet
// was never delivered to the counterparty. If successful, the packet commitment
// is deleted and the packet has completed its lifecycle.
func (k Keeper) timeoutPacket(
func (k *Keeper) timeoutPacket(
ctx context.Context,
packet channeltypesv2.Packet,
proof []byte,
Expand Down Expand Up @@ -209,9 +252,8 @@ func (k Keeper) timeoutPacket(
}

// check that the commitment has not been cleared and that it matches the packet sent by relayer
commitment, ok := k.GetPacketCommitment(ctx, packet.SourceChannel, packet.Sequence)

if !ok {
commitment := k.GetPacketCommitment(ctx, packet.SourceChannel, packet.Sequence)
if len(commitment) == 0 {
EmitTimeoutPacketEvents(ctx, packet)
// This error indicates that the timeout has already been relayed
// or there is a misconfigured relayer attempting to prove a timeout
Expand All @@ -222,7 +264,7 @@ func (k Keeper) timeoutPacket(

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

Expand Down
55 changes: 55 additions & 0 deletions modules/core/04-channel/v2/types/commitment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package types

import (
"crypto/sha256"

sdk "github.com/cosmos/cosmos-sdk/types"
)

// CommitPacket returns the V2 packet commitment bytes. The commitment consists of:
// sha256_hash(timeout) + sha256_hash(destinationChannel) + sha256_hash(packetData) from a given packet.
// This results in a fixed length preimage.
// NOTE: A fixed length preimage is ESSENTIAL to prevent relayers from being able
// to malleate the packet fields and create a commitment hash that matches the original packet.
func CommitPacket(packet Packet) []byte {
buf := sdk.Uint64ToBigEndian(packet.GetTimeoutTimestamp())

destIDHash := sha256.Sum256([]byte(packet.DestinationChannel))
buf = append(buf, destIDHash[:]...)

for _, data := range packet.Data {
buf = append(buf, hashPacketData(data)...)
}

hash := sha256.Sum256(buf)
return hash[:]
}

// hashPacketData returns the hash of the packet data.
func hashPacketData(data PacketData) []byte {
var buf []byte
sourceHash := sha256.Sum256([]byte(data.SourcePort))
buf = append(buf, sourceHash[:]...)
destHash := sha256.Sum256([]byte(data.DestinationPort))
buf = append(buf, destHash[:]...)
payloadValueHash := sha256.Sum256(data.Payload.Value)
buf = append(buf, payloadValueHash[:]...)
payloadEncodingHash := sha256.Sum256([]byte(data.Payload.Encoding))
buf = append(buf, payloadEncodingHash[:]...)
payloadVersionHash := sha256.Sum256([]byte(data.Payload.Version))
buf = append(buf, payloadVersionHash[:]...)
hash := sha256.Sum256(buf)
return hash[:]
}

// CommitAcknowledgement returns the hash of the acknowledgement data.
func CommitAcknowledgement(acknowledgement Acknowledgement) []byte {
var buf []byte
for _, ack := range acknowledgement.GetAcknowledgementResults() {
hash := sha256.Sum256(ack.RecvPacketResult.GetAcknowledgement())
buf = append(buf, hash[:]...)
}

hash := sha256.Sum256(buf)
return hash[:]
}
39 changes: 0 additions & 39 deletions modules/core/04-channel/v2/types/packet.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package types

import (
"crypto/sha256"
"strings"

errorsmod "cosmossdk.io/errors"

sdk "github.com/cosmos/cosmos-sdk/types"

host "github.com/cosmos/ibc-go/v9/modules/core/24-host"
)

Expand Down Expand Up @@ -87,39 +84,3 @@ func (p Payload) Validate() error {
}
return nil
}

// CommitPacket returns the V2 packet commitment bytes. The commitment consists of:
// sha256_hash(timeout) + sha256_hash(destinationChannel) + sha256_hash(packetData) from a given packet.
// This results in a fixed length preimage.
// NOTE: A fixed length preimage is ESSENTIAL to prevent relayers from being able
// to malleate the packet fields and create a commitment hash that matches the original packet.
func CommitPacket(packet Packet) []byte {
buf := sdk.Uint64ToBigEndian(packet.GetTimeoutTimestamp())

destIDHash := sha256.Sum256([]byte(packet.DestinationChannel))
buf = append(buf, destIDHash[:]...)

for _, data := range packet.Data {
buf = append(buf, hashPacketData(data)...)
}

hash := sha256.Sum256(buf)
return hash[:]
}

// hashPacketData returns the hash of the packet data.
func hashPacketData(data PacketData) []byte {
var buf []byte
sourceHash := sha256.Sum256([]byte(data.SourcePort))
buf = append(buf, sourceHash[:]...)
destHash := sha256.Sum256([]byte(data.DestinationPort))
buf = append(buf, destHash[:]...)
payloadValueHash := sha256.Sum256(data.Payload.Value)
buf = append(buf, payloadValueHash[:]...)
payloadEncodingHash := sha256.Sum256([]byte(data.Payload.Encoding))
buf = append(buf, payloadEncodingHash[:]...)
payloadVersionHash := sha256.Sum256([]byte(data.Payload.Version))
buf = append(buf, payloadVersionHash[:]...)
hash := sha256.Sum256(buf)
return hash[:]
}
Loading