diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bd211a61d..8458127aac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* [\#8545](https://github.com/cosmos/ibc-go/pull/8545) Support sending multiple payloads in the same packet for atomic payload execution. + ### Dependencies * [\#8369](https://github.com/cosmos/ibc-go/pull/8369) Bump **github.com/CosmWasm/wasmvm** to **2.2.4** diff --git a/modules/apps/transfer/v2/transfer_test.go b/modules/apps/transfer/v2/transfer_test.go new file mode 100644 index 00000000000..01a7014319f --- /dev/null +++ b/modules/apps/transfer/v2/transfer_test.go @@ -0,0 +1,310 @@ +package v2_test + +import ( + "time" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc-go/v10/testing/mock" + mockv2 "github.com/cosmos/ibc-go/v10/testing/mock/v2" +) + +func (suite *TransferTestSuite) TestTransferV2Flow() { + originalBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + + amount, ok := sdkmath.NewIntFromString("9223372036854775808") // 2^63 (one above int64) + suite.Require().True(ok) + originalCoin := sdk.NewCoin(sdk.DefaultBondDenom, amount) + + token := types.Token{ + Denom: types.Denom{Base: originalCoin.Denom}, + Amount: originalCoin.Amount.String(), + } + + transferData := types.NewFungibleTokenPacketData(token.Denom.Path(), token.Amount, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "") + bz := suite.chainA.Codec.MustMarshal(&transferData) + payload := channeltypesv2.NewPayload(types.PortID, types.PortID, types.V1, types.EncodingProtobuf, bz) + + // Set a timeout of 1 hour from the current block time on receiver chain + timeout := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix()) + + packet, err := suite.pathAToB.EndpointA.MsgSendPacket(timeout, payload) + suite.Require().NoError(err) + + err = suite.pathAToB.EndpointA.RelayPacket(packet) + suite.Require().NoError(err) + + escrowAddress := types.GetEscrowAddress(types.PortID, suite.pathAToB.EndpointA.ClientID) + // check that the balance for chainA is updated + chainABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom) + suite.Require().Equal(originalBalance.Amount.Sub(amount).Int64(), chainABalance.Amount.Int64()) + + // check that module account escrow address has locked the tokens + chainAEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), escrowAddress, originalCoin.Denom) + suite.Require().Equal(originalCoin, chainAEscrowBalance) + + traceAToB := types.NewHop(types.PortID, suite.pathAToB.EndpointB.ClientID) + + // check that voucher exists on chain B + chainBDenom := types.NewDenom(originalCoin.Denom, traceAToB) + chainBBalance := suite.chainB.GetSimApp().BankKeeper.GetBalance(suite.chainB.GetContext(), suite.chainB.SenderAccount.GetAddress(), chainBDenom.IBCDenom()) + coinSentFromAToB := sdk.NewCoin(chainBDenom.IBCDenom(), amount) + suite.Require().Equal(coinSentFromAToB, chainBBalance) +} + +func (suite *TransferTestSuite) TestMultiPayloadTransferV2Flow() { + + mockPayload := mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) + mockErrPayload := mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) + + var ( + timeout uint64 + payload channeltypesv2.Payload + payloads []channeltypesv2.Payload + ) + + type expResult int + const ( + success expResult = iota + sendError + recvError + ackError + timeoutError + ) + + testCases := []struct { + name string + malleate func() + expRes expResult + }{ + { + name: "success with transfer payloads", + malleate: func() { + payloads = []channeltypesv2.Payload{payload, payload} + }, + expRes: success, + }, + { + name: "success with transfer and mock payloads", + malleate: func() { + payloads = []channeltypesv2.Payload{payload, mockPayload, mockPayload, payload} + }, + expRes: success, + }, + { + name: "send error should revert transfer", + malleate: func() { + // mock the send packet callback to return an error + suite.pathAToB.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnSendPacket = func(ctx sdk.Context, sourceChannel, destinationChannel string, sequence uint64, data channeltypesv2.Payload, signer sdk.AccAddress) error { + return mock.MockApplicationCallbackError + } + payloads = []channeltypesv2.Payload{payload, mockPayload, payload} + }, + expRes: sendError, + }, + { + name: "recv error on mock should revert transfer", + malleate: func() { + payloads = []channeltypesv2.Payload{payload, mockPayload, mockErrPayload, payload} + }, + expRes: recvError, + }, + { + name: "ack error on mock should block refund on acknowledgement", + malleate: func() { + // mock the acknowledgement packet callback to return an error + suite.pathAToB.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnAcknowledgementPacket = func(ctx sdk.Context, sourceChannel, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, acknowledgement []byte, relayer sdk.AccAddress) error { + return mock.MockApplicationCallbackError + } + payloads = []channeltypesv2.Payload{payload, mockPayload, mockPayload, payload} + + }, + expRes: ackError, + }, + { + name: "timeout error on mock should block refund on timeout", + malleate: func() { + // mock the timeout packet callback to return an error + suite.pathAToB.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(ctx sdk.Context, sourceChannel, destinationChannel string, sequence uint64, payload channeltypesv2.Payload, relayer sdk.AccAddress) error { + return mock.MockApplicationCallbackError + } + // set the timeout to be 1 second from now so that the packet will timeout + timeout = uint64(suite.chainB.GetContext().BlockTime().Add(time.Second).Unix()) + payloads = []channeltypesv2.Payload{payload, mockPayload, mockPayload, payload} + }, + expRes: timeoutError, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() // reset + + originalBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(suite.chainA.GetContext(), suite.chainA.SenderAccount.GetAddress(), sdk.DefaultBondDenom) + + // total amount is the sum of all amounts in the payloads which is always 2 * amount + totalAmount, ok := sdkmath.NewIntFromString("9223372036854775808") // 2^63 (one above int64) + suite.Require().True(ok) + amount := totalAmount.QuoRaw(2) // divide by 2 to account for the two payloads + originalCoin := sdk.NewCoin(sdk.DefaultBondDenom, amount) + totalCoin := sdk.NewCoin(originalCoin.Denom, totalAmount) + + token := types.Token{ + Denom: types.Denom{Base: originalCoin.Denom}, + Amount: originalCoin.Amount.String(), + } + + transferData := types.NewFungibleTokenPacketData(token.Denom.Path(), token.Amount, suite.chainA.SenderAccount.GetAddress().String(), suite.chainB.SenderAccount.GetAddress().String(), "") + bz := suite.chainA.Codec.MustMarshal(&transferData) + + payload = channeltypesv2.NewPayload(types.PortID, types.PortID, types.V1, types.EncodingProtobuf, bz) + + escrowAddress := types.GetEscrowAddress(types.PortID, suite.pathAToB.EndpointA.ClientID) + + // Set a timeout of 1 hour from the current block time on receiver chain + timeout = uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix()) + + // malleate the test case to set up the payloads + // and modulate test case behavior + tc.malleate() + + packet, sendErr := suite.pathAToB.EndpointA.MsgSendPacket(timeout, payloads...) + + if tc.expRes == sendError { + suite.Require().Error(sendErr, "expected error when sending packet with send error") + } else { + suite.Require().NoError(sendErr, "unexpected error when sending packet") + + // relay the packet + relayErr := suite.pathAToB.EndpointA.RelayPacket(packet) + + // relayer should have error in response on ack error and timeout error + // recv error should not return an error since the error is handled as error acknowledgement + if tc.expRes == ackError || tc.expRes == timeoutError { + suite.Require().Error(relayErr, "expected error when relaying packet with acknowledgement error or timeout error") + } else { + suite.Require().NoError(relayErr, "unexpected error when relaying packet") + } + } + + ctxA := suite.pathAToB.EndpointA.Chain.GetContext() + ctxB := suite.pathAToB.EndpointB.Chain.GetContext() + + // GET TRANSFER STATE AFTER RELAY FOR TESTING CHECKS + // get account balances after relaying packet + chainABalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(ctxA, suite.chainA.SenderAccount.GetAddress(), originalCoin.Denom) + chainAEscrowBalance := suite.chainA.GetSimApp().BankKeeper.GetBalance(ctxA, escrowAddress, originalCoin.Denom) + + traceAToB := types.NewHop(types.PortID, suite.pathAToB.EndpointB.ClientID) + + // get chain B balance for voucer + chainBDenom := types.NewDenom(originalCoin.Denom, traceAToB) + chainBBalance := suite.chainB.GetSimApp().BankKeeper.GetBalance(ctxB, suite.chainB.SenderAccount.GetAddress(), chainBDenom.IBCDenom()) + + // calculate the expected coin sent from chain A to chain B + coinSentFromAToB := sdk.NewCoin(chainBDenom.IBCDenom(), amount.MulRaw(2)) + + // GET IBC STATE AFTER RELAY FOR TESTING CHECKS + nextSequenceSend, ok := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeperV2.GetNextSequenceSend(suite.pathAToB.EndpointA.Chain.GetContext(), suite.pathAToB.EndpointA.ClientID) + suite.Require().True(ok) + + packetCommitment := suite.chainA.GetSimApp().IBCKeeper.ChannelKeeperV2.GetPacketCommitment(ctxA, packet.SourceClient, packet.Sequence) + hasReceipt := suite.chainB.GetSimApp().IBCKeeper.ChannelKeeperV2.HasPacketReceipt(ctxB, packet.DestinationClient, packet.Sequence) + hasAck := suite.chainB.GetSimApp().IBCKeeper.ChannelKeeperV2.HasPacketAcknowledgement(ctxB, packet.DestinationClient, packet.Sequence) + + switch tc.expRes { + case success: + // check transfer state after successful relay + // check that the balance for chainA is updated + suite.Require().Equal(originalBalance.Amount.Sub(totalAmount), chainABalance.Amount, "chain A balance should be deducted after successful transfer") + // check that module account escrow address has locked the tokens + suite.Require().Equal(totalCoin, chainAEscrowBalance, "escrow balance should be locked after successful transfer") + // check that voucher exists on chain B + suite.Require().Equal(coinSentFromAToB, chainBBalance, "voucher balance should be updated after successful transfer") + + // check IBC state after successful relay + suite.Require().Equal(uint64(2), nextSequenceSend, "next sequence send was not incremented correctly") + // packet commitment should be cleared + suite.Require().Nil(packetCommitment) + + // packet receipt and acknowledgement should be written + suite.Require().True(hasReceipt, "packet receipt should exist") + suite.Require().True(hasAck, "packet acknowledgement should exist") + case sendError: + // check transfer state after send error + // check that the balance for chainA is unchanged + suite.Require().Equal(originalBalance.Amount, chainABalance.Amount, "chain A balance should be unchanged after send error") + // check that module account escrow address has not locked the tokens + suite.Require().Equal(sdk.NewCoin(originalCoin.Denom, sdkmath.ZeroInt()), chainAEscrowBalance, "escrow balance should be zero after send error") + // check that voucher does not exist on chain B + suite.Require().Equal(sdk.NewCoin(chainBDenom.IBCDenom(), sdkmath.ZeroInt()), chainBBalance, "voucher balance should be zero after send error") + + // check IBC state after send error + suite.Require().Equal(uint64(1), nextSequenceSend, "next sequence send should not be incremented after send error") + // packet commitment should not exist + suite.Require().Nil(packetCommitment, "packet commitment should not exist after send error") + // packet receipt and acknowledgement should not be written + suite.Require().False(hasReceipt, "packet receipt should not exist after send error") + suite.Require().False(hasAck, "packet acknowledgement should not exist after send error") + case recvError: + // check transfer state after receive error + // check that the balance for chainA is refunded after error acknowledgement is relayed + suite.Require().Equal(originalBalance.Amount, chainABalance.Amount, "chain A balance should be unchanged after receive error") + // check that module account escrow address has reverted the locked tokens + suite.Require().Equal(sdk.NewCoin(originalCoin.Denom, sdkmath.ZeroInt()), chainAEscrowBalance, "escrow balance should be reverted after receive error") + // check that voucher does not exist on chain B + suite.Require().Equal(sdk.NewCoin(chainBDenom.IBCDenom(), sdkmath.ZeroInt()), chainBBalance, "voucher balance should be zero after receive error") + + // check IBC state after receive error + suite.Require().Equal(uint64(2), nextSequenceSend, "next sequence send should be incremented after receive error") + // packet commitment should be cleared + suite.Require().Nil(packetCommitment, "packet commitment should be cleared after receive error") + // packet receipt should be written + suite.Require().True(hasReceipt, "packet receipt should exist after receive error") + // packet acknowledgement should be written + suite.Require().True(hasAck, "packet acknowledgement should exist after receive error") + case ackError: + // check transfer state after acknowledgement error + // check that the balance for chainA is still deducted since acknowledgement failed + suite.Require().Equal(originalBalance.Amount.Sub(totalAmount), chainABalance.Amount, "chain A balance should still be deducted after acknowledgement error") + // check that module account escrow address has still locked the tokens + suite.Require().Equal(totalCoin, chainAEscrowBalance, "escrow balance should still be locked after acknowledgement error") + // check that voucher does not exist on chain B since receive returned error acknowledgement + suite.Require().Equal(sdk.NewCoin(chainBDenom.IBCDenom(), totalAmount), chainBBalance, "voucher balance should be zero after acknowledgement error") + + // check IBC state after acknowledgement error + suite.Require().Equal(uint64(2), nextSequenceSend, "next sequence send should be incremented after acknowledgement error") + // packet commitment should not be cleared + suite.Require().NotNil(packetCommitment, "packet commitment should not be cleared after acknowledgement error") + // packet receipt should be written + suite.Require().True(hasReceipt, "packet receipt should exist after acknowledgement error") + // packet acknowledgement should be written + suite.Require().True(hasAck, "packet acknowledgement should exist after acknowledgement error") + case timeoutError: + // check transfer state after acknowledgement error + // check that the balance for chainA is still deducted since acknowledgement failed + suite.Require().Equal(originalBalance.Amount.Sub(totalAmount), chainABalance.Amount, "chain A balance should still be deducted after timeout error") + // check that module account escrow address has still locked the tokens + suite.Require().Equal(totalCoin, chainAEscrowBalance, "escrow balance should still be locked after timeout error") + // check that voucher does not exist on chain B since receive returned error acknowledgement + suite.Require().Equal(sdk.NewCoin(chainBDenom.IBCDenom(), sdkmath.ZeroInt()), chainBBalance, "voucher balance should be zero after timeout error") + + // check IBC state after timeout error + // check IBC state after acknowledgement error + suite.Require().Equal(uint64(2), nextSequenceSend, "next sequence send should be incremented after timeout error") + // packet commitment should not be cleared + suite.Require().NotNil(packetCommitment, "packet commitment should not be cleared after timeout error") + // packet receipt should not be written + suite.Require().False(hasReceipt, "packet receipt should not exist after timeout error") + // packet acknowledgement should not be written + suite.Require().False(hasAck, "packet acknowledgement should not exist after timeout error") + } + + }) + } + +} diff --git a/modules/core/04-channel/v2/keeper/msg_server.go b/modules/core/04-channel/v2/keeper/msg_server.go index 38a765fbdd9..f8e029d4ad3 100644 --- a/modules/core/04-channel/v2/keeper/msg_server.go +++ b/modules/core/04-channel/v2/keeper/msg_server.go @@ -85,9 +85,10 @@ func (k *Keeper) RecvPacket(goCtx context.Context, msg *types.MsgRecvPacket) (*t var isAsync bool isSuccess := true + // Cache context before doing any application callbacks + // so that we may write or discard state changes from callbacks atomically. + cacheCtx, writeFn = ctx.CacheContext() for _, pd := range msg.Packet.Payloads { - // Cache context so that we may discard state changes from callback if the acknowledgement is unsuccessful. - cacheCtx, writeFn = ctx.CacheContext() cb := k.Router.Route(pd.DestinationPort) res := cb.OnRecvPacket(cacheCtx, msg.Packet.SourceClient, msg.Packet.DestinationClient, msg.Packet.Sequence, pd, signer) @@ -96,8 +97,6 @@ func (k *Keeper) RecvPacket(goCtx context.Context, msg *types.MsgRecvPacket) (*t if bytes.Equal(res.GetAcknowledgement(), types.ErrorAcknowledgement[:]) { return nil, errorsmod.Wrapf(types.ErrInvalidAcknowledgement, "application acknowledgement cannot be sentinel error acknowledgement") } - // write application state changes for asynchronous and successful acknowledgements - writeFn() // append app acknowledgement to the overall acknowledgement ack.AppAcknowledgements = append(ack.AppAcknowledgements, res.Acknowledgement) } else { @@ -122,18 +121,19 @@ func (k *Keeper) RecvPacket(goCtx context.Context, msg *types.MsgRecvPacket) (*t } } + // write application state changes for asynchronous and successful acknowledgements + // if any application returns a failure, then we discard all state changes + // to ensure an atomic execution of all payloads + if isSuccess { + writeFn() + } + if !isAsync { - // If the application callback was successful, the acknowledgement must have the same number of app acknowledgements as the packet payloads. - if isSuccess { - if len(ack.AppAcknowledgements) != len(msg.Packet.Payloads) { - return nil, errorsmod.Wrapf(types.ErrInvalidAcknowledgement, "length of app acknowledgement %d does not match length of app payload %d", len(ack.AppAcknowledgements), len(msg.Packet.Payloads)) - } + // sanity check to ensure returned acknowledgement and calculated isSuccess boolean matches + if ack.Success() != isSuccess { + panic("acknowledgement success does not match isSuccess") } - // Validate ack before forwarding to WriteAcknowledgement. - if err := ack.Validate(); err != nil { - return nil, err - } // Set packet acknowledgement only if the acknowledgement is not async. // NOTE: IBC applications modules may call the WriteAcknowledgement asynchronously if the // acknowledgement is async. diff --git a/modules/core/04-channel/v2/keeper/msg_server_test.go b/modules/core/04-channel/v2/keeper/msg_server_test.go index f853e96ec49..0df44ee2377 100644 --- a/modules/core/04-channel/v2/keeper/msg_server_test.go +++ b/modules/core/04-channel/v2/keeper/msg_server_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "bytes" "errors" "time" @@ -13,6 +14,7 @@ import ( ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" ibctesting "github.com/cosmos/ibc-go/v10/testing" "github.com/cosmos/ibc-go/v10/testing/mock" + mockv1 "github.com/cosmos/ibc-go/v10/testing/mock" mockv2 "github.com/cosmos/ibc-go/v10/testing/mock/v2" ) @@ -21,7 +23,7 @@ func (suite *KeeperTestSuite) TestMsgSendPacket() { path *ibctesting.Path expectedPacket types.Packet timeoutTimestamp uint64 - payload types.Payload + payloads []types.Payload ) testCases := []struct { @@ -34,12 +36,19 @@ func (suite *KeeperTestSuite) TestMsgSendPacket() { malleate: func() {}, expError: nil, }, + { + name: "success multiple payloads", + malleate: func() { + payloads = append(payloads, payloads[0]) + }, + expError: nil, + }, { name: "success: valid timeout timestamp", malleate: func() { // ensure a message timeout. timeoutTimestamp = uint64(suite.chainA.GetContext().BlockTime().Add(types.MaxTimeoutDelta - 10*time.Second).Unix()) - expectedPacket = types.NewPacket(1, path.EndpointA.ClientID, path.EndpointB.ClientID, timeoutTimestamp, payload) + expectedPacket = types.NewPacket(1, path.EndpointA.ClientID, path.EndpointB.ClientID, timeoutTimestamp, payloads...) }, expError: nil, }, @@ -83,6 +92,19 @@ func (suite *KeeperTestSuite) TestMsgSendPacket() { }, expError: mock.MockApplicationCallbackError, }, + { + name: "failure: multiple payload application callback error", + malleate: func() { + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnSendPacket = func(ctx sdk.Context, sourceID string, destinationID string, sequence uint64, data types.Payload, signer sdk.AccAddress) error { + if bytes.Equal(mockv1.MockFailPacketData, data.Value) { + return mock.MockApplicationCallbackError + } + return nil + } + payloads = append(payloads, mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)) + }, + expError: mock.MockApplicationCallbackError, + }, { name: "failure: client not found", malleate: func() { @@ -93,7 +115,7 @@ func (suite *KeeperTestSuite) TestMsgSendPacket() { { name: "failure: route to non existing app", malleate: func() { - payload.SourcePort = "foo" + payloads[0].SourcePort = "foo" }, expError: errors.New("no route for foo"), }, @@ -107,13 +129,12 @@ func (suite *KeeperTestSuite) TestMsgSendPacket() { path.SetupV2() timeoutTimestamp = suite.chainA.GetTimeoutTimestampSecs() - payload = mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) - - expectedPacket = types.NewPacket(1, path.EndpointA.ClientID, path.EndpointB.ClientID, timeoutTimestamp, payload) + payloads = []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)} tc.malleate() - packet, err := path.EndpointA.MsgSendPacket(timeoutTimestamp, payload) + expectedPacket = types.NewPacket(1, path.EndpointA.ClientID, path.EndpointB.ClientID, timeoutTimestamp, payloads...) + packet, err := path.EndpointA.MsgSendPacket(timeoutTimestamp, payloads...) expPass := tc.expError == nil if expPass { @@ -142,45 +163,46 @@ func (suite *KeeperTestSuite) TestMsgSendPacket() { func (suite *KeeperTestSuite) TestMsgRecvPacket() { var ( - path *ibctesting.Path - packet types.Packet - expRecvRes types.RecvPacketResult + path *ibctesting.Path + packet types.Packet + expAck types.Acknowledgement ) testCases := []struct { name string + payloads []types.Payload malleate func() expError error expAckWritten bool }{ { name: "success", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() {}, expError: nil, expAckWritten: true, }, { - name: "success: failed recv result", + name: "success: error ack", + payloads: []types.Payload{mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() { - expRecvRes = types.RecvPacketResult{ - Status: types.PacketStatus_Failure, + expAck = types.Acknowledgement{ + AppAcknowledgements: [][]byte{types.ErrorAcknowledgement[:]}, } }, expError: nil, expAckWritten: true, }, { - name: "success: async recv result", - malleate: func() { - expRecvRes = types.RecvPacketResult{ - Status: types.PacketStatus_Async, - } - }, + name: "success: async recv result", + payloads: []types.Payload{mockv2.NewAsyncMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, + malleate: func() {}, expError: nil, expAckWritten: false, }, { - name: "success: NoOp", + name: "success: NoOp", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() { suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetPacketReceipt(suite.chainB.GetContext(), packet.DestinationClient, packet.Sequence) }, @@ -188,7 +210,43 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { expAckWritten: false, }, { - name: "success: receive permissioned with msg sender", + name: "success: multiple payloads", + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, + malleate: func() { + expAck = types.Acknowledgement{ + AppAcknowledgements: [][]byte{ + mockv2.MockRecvPacketResult.Acknowledgement, + mockv2.MockRecvPacketResult.Acknowledgement, + }, + } + }, + expError: nil, + expAckWritten: true, + }, + { + name: "success: multiple payloads with error ack", + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, + malleate: func() { + expAck = types.Acknowledgement{ + AppAcknowledgements: [][]byte{ + types.ErrorAcknowledgement[:], + }, + } + }, + expError: nil, + expAckWritten: true, + }, + { + name: "success: receive permissioned with msg sender", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, + malleate: func() { creator := suite.chainB.SenderAccount.GetAddress() msg := clientv2types.NewMsgUpdateClientConfig(path.EndpointB.ClientID, creator.String(), clientv2types.NewConfig(suite.chainA.SenderAccount.GetAddress().String(), creator.String())) @@ -199,7 +257,8 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { expAckWritten: true, }, { - name: "failure: relayer not permissioned", + name: "failure: relayer not permissioned", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() { creator := suite.chainB.SenderAccount.GetAddress() msg := clientv2types.NewMsgUpdateClientConfig(path.EndpointB.ClientID, creator.String(), clientv2types.NewConfig(suite.chainA.SenderAccount.GetAddress().String())) @@ -209,7 +268,8 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { expError: ibcerrors.ErrUnauthorized, }, { - name: "failure: counterparty not found", + name: "failure: counterparty not found", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() { // change the destination id to a non-existent channel. packet.DestinationClient = ibctesting.InvalidID @@ -217,7 +277,8 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { expError: clientv2types.ErrCounterpartyNotFound, }, { - name: "failure: invalid proof", + name: "failure: invalid proof", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() { // proof verification fails because the packet commitment is different due to a different sequence. packet.Sequence = 10 @@ -225,15 +286,29 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { expError: commitmenttypes.ErrInvalidProof, }, { - name: "failure: invalid acknowledgement", + name: "failure: invalid acknowledgement", + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, malleate: func() { - expRecvRes = types.RecvPacketResult{ - Status: types.PacketStatus_Success, - Acknowledgement: []byte(""), + // modify the callback to return the expected recv result. + path.EndpointB.Chain.GetSimApp().MockModuleV2B.IBCApp.OnRecvPacket = func(ctx sdk.Context, sourceChannel string, destinationChannel string, sequence uint64, data types.Payload, relayer sdk.AccAddress) types.RecvPacketResult { + return types.RecvPacketResult{ + Status: types.PacketStatus_Success, + Acknowledgement: []byte(""), + } } }, expError: types.ErrInvalidAcknowledgement, }, + { + name: "failure: async payload with other payloads", + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewAsyncMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, + malleate: func() {}, + expError: types.ErrInvalidPacket, + expAckWritten: false, + }, } for _, tc := range testCases { @@ -246,26 +321,15 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { timeoutTimestamp := suite.chainA.GetTimeoutTimestampSecs() var err error - packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)) + packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, tc.payloads...) suite.Require().NoError(err) - // default expected receive result is a single successful recv result for moduleB. - expRecvRes = mockv2.MockRecvPacketResult - - tc.malleate() - - // expectedAck is derived from the expected recv result. - var expectedAck types.Acknowledgement - if expRecvRes.Status == types.PacketStatus_Success { - expectedAck = types.Acknowledgement{AppAcknowledgements: [][]byte{expRecvRes.Acknowledgement}} - } else { - expectedAck = types.Acknowledgement{AppAcknowledgements: [][]byte{types.ErrorAcknowledgement[:]}} + // default expected acknowledgement is a single successful acknowledgement for moduleB. + expAck = types.Acknowledgement{ + AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement}, } - // modify the callback to return the expected recv result. - path.EndpointB.Chain.GetSimApp().MockModuleV2B.IBCApp.OnRecvPacket = func(ctx sdk.Context, sourceChannel string, destinationChannel string, sequence uint64, data types.Payload, relayer sdk.AccAddress) types.RecvPacketResult { - return expRecvRes - } + tc.malleate() // err is checking under expPass err = path.EndpointB.MsgRecvPacket(packet) @@ -287,7 +351,7 @@ func (suite *KeeperTestSuite) TestMsgRecvPacket() { } else { // successful or failed acknowledgement // ack should be written for synchronous app (default mock application behaviour). suite.Require().True(ackWritten) - expectedBz := types.CommitAcknowledgement(expectedAck) + expectedBz := types.CommitAcknowledgement(expAck) actualAckBz := ck.GetPacketAcknowledgement(path.EndpointB.Chain.GetContext(), packet.DestinationClient, packet.Sequence) suite.Require().Equal(expectedBz, actualAckBz) @@ -311,13 +375,13 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { testCases := []struct { name string malleate func() - payload types.Payload + payloads []types.Payload expError error }{ { name: "success", malleate: func() {}, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, }, { name: "success: NoOp", @@ -330,14 +394,44 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { return mock.MockApplicationCallbackError } }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, }, { name: "success: failed ack result", malleate: func() { ack.AppAcknowledgements[0] = types.ErrorAcknowledgement[:] }, - payload: mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, + }, + { + name: "success: multiple payloads", + malleate: func() { + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{ + mockv2.MockRecvPacketResult.Acknowledgement, + mockv2.MockRecvPacketResult.Acknowledgement, + }, + } + }, + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, + }, + { + name: "success: multiple payloads with error ack", + malleate: func() { + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{ + types.ErrorAcknowledgement[:], + }, + } + }, + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, }, { name: "success: relayer permissioned with msg sender", @@ -347,7 +441,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { _, err := suite.chainA.App.GetIBCKeeper().UpdateClientConfig(suite.chainA.GetContext(), msg) suite.Require().NoError(err) }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, }, { name: "failure: relayer not permissioned", @@ -357,7 +451,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { _, err := suite.chainA.App.GetIBCKeeper().UpdateClientConfig(suite.chainA.GetContext(), msg) suite.Require().NoError(err) }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: ibcerrors.ErrUnauthorized, }, { @@ -367,7 +461,32 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { return mock.MockApplicationCallbackError } }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, + expError: mock.MockApplicationCallbackError, + }, + { + name: "failure: callback fails on one of the multiple payloads", + malleate: func() { + // create custom callback that fails on one of the payloads in the test case. + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnAcknowledgementPacket = func(ctx sdk.Context, sourceClient string, destinationClient string, sequence uint64, data types.Payload, acknowledgement []byte, relayer sdk.AccAddress) error { + if data.DestinationPort == mockv2.ModuleNameB { + return mock.MockApplicationCallbackError + } + return nil + } + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{ + mockv2.MockRecvPacketResult.Acknowledgement, + mockv2.MockRecvPacketResult.Acknowledgement, + mockv2.MockRecvPacketResult.Acknowledgement, // this one will not be processed + }, + } + }, + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameA), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameA), + }, expError: mock.MockApplicationCallbackError, }, { @@ -376,7 +495,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { // change the source id to a non-existent channel. packet.SourceClient = "not-existent-channel" }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: clientv2types.ErrCounterpartyNotFound, }, { @@ -384,7 +503,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { malleate: func() { suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SetPacketCommitment(suite.chainA.GetContext(), packet.SourceClient, packet.Sequence, []byte("foo")) }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: types.ErrInvalidPacket, }, { @@ -392,10 +511,11 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { malleate: func() { ack.AppAcknowledgements[0] = mock.MockFailPacketData }, - payload: mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: errors.New("failed packet acknowledgement verification"), }, } + for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() @@ -407,7 +527,7 @@ func (suite *KeeperTestSuite) TestMsgAcknowledgement() { var err error // Send packet from A to B - packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, tc.payload) + packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, tc.payloads...) suite.Require().NoError(err) err = path.EndpointB.MsgRecvPacket(packet) @@ -440,6 +560,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { testCases := []struct { name string malleate func() + payloads []types.Payload expError error }{ { @@ -447,6 +568,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { malleate: func() { suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, }, { name: "success: no-op", @@ -460,6 +582,18 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { } suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, + }, + { + name: "success: multiple payloads", + malleate: func() { + suite.Require().NoError(path.EndpointA.UpdateClient()) + }, + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, + expError: nil, }, { name: "success: relayer permissioned with msg sender", @@ -470,6 +604,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { suite.Require().NoError(err) suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, }, { name: "failure: relayer not permissioned", @@ -481,6 +616,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { _, err := suite.chainA.App.GetIBCKeeper().UpdateClientConfig(suite.chainA.GetContext(), msg) suite.Require().NoError(err) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: ibcerrors.ErrUnauthorized, }, { @@ -491,6 +627,26 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { } suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, + expError: mock.MockApplicationCallbackError, + }, + { + name: "failure: callback fails on one of the multiple payloads", + malleate: func() { + // create custom callback that fails on one of the payloads in the test case. + path.EndpointA.Chain.GetSimApp().MockModuleV2A.IBCApp.OnTimeoutPacket = func(ctx sdk.Context, sourceChannel string, destinationChannel string, sequence uint64, data types.Payload, relayer sdk.AccAddress) error { + if bytes.Equal(mockv1.MockFailPacketData, data.Value) { + return mock.MockApplicationCallbackError + } + return nil + } + suite.Require().NoError(path.EndpointA.UpdateClient()) + }, + payloads: []types.Payload{ + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewErrorMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB), + }, expError: mock.MockApplicationCallbackError, }, { @@ -500,6 +656,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { packet.SourceClient = "not-existent-client" suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: clientv2types.ErrCounterpartyNotFound, }, { @@ -508,6 +665,7 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SetPacketCommitment(suite.chainA.GetContext(), packet.SourceClient, packet.Sequence, []byte("foo")) suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: types.ErrInvalidPacket, }, { @@ -517,9 +675,11 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { suite.Require().NoError(path.EndpointB.UpdateClient()) suite.Require().NoError(path.EndpointA.UpdateClient()) }, + payloads: []types.Payload{mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)}, expError: commitmenttypes.ErrInvalidProof, }, } + for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupTest() @@ -531,10 +691,9 @@ func (suite *KeeperTestSuite) TestMsgTimeout() { // make timeoutTimestamp 1 second more than sending chain time to ensure it passes SendPacket // and times out successfully after update timeoutTimestamp := uint64(suite.chainA.GetContext().BlockTime().Add(time.Second).Unix()) - mockData := mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) var err error - packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, mockData) + packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, tc.payloads...) suite.Require().NoError(err) suite.Require().NotEmpty(packet) diff --git a/modules/core/04-channel/v2/keeper/packet.go b/modules/core/04-channel/v2/keeper/packet.go index a59b8020c7f..32fe4127ec8 100644 --- a/modules/core/04-channel/v2/keeper/packet.go +++ b/modules/core/04-channel/v2/keeper/packet.go @@ -180,6 +180,19 @@ func (k Keeper) writeAcknowledgement( packet types.Packet, ack types.Acknowledgement, ) error { + // Validate the acknowledgement + if err := ack.Validate(); err != nil { + ctx.Logger().Error("write acknowledgement failed", "error", errorsmod.Wrap(err, "invalid acknowledgement")) + return errorsmod.Wrap(err, "invalid acknowledgement") + } + + // Validate the acknowledgement against the payload length + if ack.Success() { + if len(ack.AppAcknowledgements) != len(packet.Payloads) { + return errorsmod.Wrapf(types.ErrInvalidAcknowledgement, "length of app acknowledgement %d does not match length of app payload %d", len(ack.AppAcknowledgements), len(packet.Payloads)) + } + } + // lookup counterparty from packet identifiers // note this will be either the client identifier for IBC V2 paths // or an aliased channel identifier for IBC V1 paths @@ -219,12 +232,7 @@ func (k Keeper) writeAcknowledgement( // WriteAcknowledgement writes the acknowledgement and emits events for asynchronous acknowledgements // this is the method to be called by external apps when they want to write an acknowledgement asyncrhonously func (k *Keeper) WriteAcknowledgement(ctx sdk.Context, clientID string, sequence uint64, ack types.Acknowledgement) error { - // Validate the acknowledgement - if err := ack.Validate(); err != nil { - ctx.Logger().Error("write acknowledgement failed", "error", errorsmod.Wrap(err, "invalid acknowledgement")) - return errorsmod.Wrap(err, "invalid acknowledgement") - } - + // get saved async packet from store packet, ok := k.GetAsyncPacket(ctx, clientID, sequence) if !ok { return errorsmod.Wrapf(types.ErrInvalidAcknowledgement, "packet with clientID (%s) and sequence (%d) not found for async acknowledgement", clientID, sequence) diff --git a/modules/core/04-channel/v2/keeper/packet_test.go b/modules/core/04-channel/v2/keeper/packet_test.go index a2996ba9591..95f614315f3 100644 --- a/modules/core/04-channel/v2/keeper/packet_test.go +++ b/modules/core/04-channel/v2/keeper/packet_test.go @@ -26,6 +26,7 @@ func (suite *KeeperTestSuite) TestSendPacket() { var ( path *ibctesting.Path packet types.Packet + payload types.Payload expSequence uint64 ) @@ -39,6 +40,13 @@ func (suite *KeeperTestSuite) TestSendPacket() { func() {}, nil, }, + { + "success multiple payloads", + func() { + packet.Payloads = append(packet.Payloads, payload) + }, + nil, + }, { "success with later packet", func() { @@ -64,6 +72,13 @@ func (suite *KeeperTestSuite) TestSendPacket() { }, types.ErrInvalidPacket, }, + { + "multiple payload failed packet validation", + func() { + packet.Payloads = append(packet.Payloads, types.Payload{}) + }, + types.ErrInvalidPacket, + }, { "client status invalid", func() { @@ -108,7 +123,7 @@ func (suite *KeeperTestSuite) TestSendPacket() { path = ibctesting.NewPath(suite.chainA, suite.chainB) path.SetupV2() - payload := mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) + payload = mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix()) @@ -217,8 +232,8 @@ func (suite *KeeperTestSuite) TestRecvPacket() { timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix()) - // send packet - packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, payload) + // send packet with multiple payloads + packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, payload, payload) suite.Require().NoError(err) tc.malleate() @@ -245,8 +260,9 @@ func (suite *KeeperTestSuite) TestRecvPacket() { func (suite *KeeperTestSuite) TestWriteAcknowledgement() { var ( - packet types.Packet - ack types.Acknowledgement + packet types.Packet + payload types.Payload + ack types.Acknowledgement ) testCases := []struct { @@ -259,6 +275,63 @@ func (suite *KeeperTestSuite) TestWriteAcknowledgement() { func() {}, nil, }, + { + "success with error ack", + func() { + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{types.ErrorAcknowledgement[:]}, + } + }, + nil, + }, + { + "success multiple payloads", + func() { + packet.Payloads = append(packet.Payloads, payload) + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement, mockv2.MockRecvPacketResult.Acknowledgement}, + } + }, + nil, + }, + { + "success multiple payloads with error ack", + func() { + packet.Payloads = append(packet.Payloads, payload) + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{types.ErrorAcknowledgement[:]}, + } + }, + nil, + }, + { + "failure: multiple payloads length doesn't match ack length", + func() { + packet.Payloads = append(packet.Payloads, payload, payload) + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement, mockv2.MockRecvPacketResult.Acknowledgement}, + } + }, + types.ErrInvalidAcknowledgement, + }, + { + "failure: single payload length doesn't match ack", + func() { + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement, mockv2.MockRecvPacketResult.Acknowledgement}, + } + }, + types.ErrInvalidAcknowledgement, + }, + { + "failure: invalid acknowledgement, error acknowledgement with success acknowledgement together", + func() { + ack = types.Acknowledgement{ + AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement, types.ErrorAcknowledgement[:]}, + } + }, + types.ErrInvalidAcknowledgement, + }, { "failure: client not found", func() { @@ -296,7 +369,8 @@ func (suite *KeeperTestSuite) TestWriteAcknowledgement() { { "failure: async packet not found", func() { - suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.DeleteAsyncPacket(suite.chainB.GetContext(), packet.DestinationClient, packet.Sequence) + packet.Sequence = 2 + suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetPacketReceipt(suite.chainB.GetContext(), packet.DestinationClient, packet.Sequence) }, types.ErrInvalidAcknowledgement, }, @@ -309,7 +383,7 @@ func (suite *KeeperTestSuite) TestWriteAcknowledgement() { path := ibctesting.NewPath(suite.chainA, suite.chainB) path.SetupV2() - payload := mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) + payload = mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix()) @@ -322,12 +396,14 @@ func (suite *KeeperTestSuite) TestWriteAcknowledgement() { AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement}, } - // mock receive with async acknowledgement - suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetPacketReceipt(suite.chainB.GetContext(), packet.DestinationClient, packet.Sequence) - suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetAsyncPacket(suite.chainB.GetContext(), packet.DestinationClient, packet.Sequence, packet) - tc.malleate() + // mock receive with async acknowledgement + // we mock the receive of a sequence 1 manually so that the malleate can change the packet sequence + // in order to not have the keys do not match the packet sequence + suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetPacketReceipt(suite.chainB.GetContext(), packet.DestinationClient, 1) + suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetAsyncPacket(suite.chainB.GetContext(), packet.DestinationClient, 1, packet) + err := suite.chainB.App.GetIBCKeeper().ChannelKeeperV2.WriteAcknowledgement(suite.chainB.GetContext(), packet.DestinationClient, packet.Sequence, ack) expPass := tc.expError == nil @@ -349,7 +425,11 @@ func (suite *KeeperTestSuite) TestAcknowledgePacket() { packet types.Packet err error ack = types.Acknowledgement{ - AppAcknowledgements: [][]byte{mockv2.MockRecvPacketResult.Acknowledgement}, + AppAcknowledgements: [][]byte{ + mockv2.MockRecvPacketResult.Acknowledgement, + mockv2.MockRecvPacketResult.Acknowledgement, + mockv2.MockRecvPacketResult.Acknowledgement, + }, } freezeClient bool ) @@ -422,8 +502,8 @@ func (suite *KeeperTestSuite) TestAcknowledgePacket() { timeoutTimestamp := uint64(suite.chainB.GetContext().BlockTime().Add(time.Hour).Unix()) - // send packet - packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, payload) + // send packet with multiple payloads + packet, err = path.EndpointA.MsgSendPacket(timeoutTimestamp, payload, payload, payload) suite.Require().NoError(err) err = path.EndpointB.MsgRecvPacket(packet) @@ -458,6 +538,7 @@ func (suite *KeeperTestSuite) TestTimeoutPacket() { var ( path *ibctesting.Path packet types.Packet + payload types.Payload freezeClient bool ) @@ -476,6 +557,17 @@ func (suite *KeeperTestSuite) TestTimeoutPacket() { }, nil, }, + { + "success multiple payloads", + func() { + // send packet with multiple payloads + packet.Payloads = append(packet.Payloads, payload) + _, _, err := suite.chainA.App.GetIBCKeeper().ChannelKeeperV2.SendPacketTest(suite.chainA.GetContext(), packet.SourceClient, + packet.TimeoutTimestamp, packet.Payloads) + suite.Require().NoError(err, "send packet failed") + }, + nil, + }, { "failure: client not found", func() { @@ -566,7 +658,7 @@ func (suite *KeeperTestSuite) TestTimeoutPacket() { path.SetupV2() // create default packet with a timed out timestamp - payload := mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) + payload = mockv2.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB) // make timeoutTimestamp 1 second more than sending chain time to ensure it passes SendPacket // and times out successfully after update diff --git a/modules/core/04-channel/v2/types/acknowledgement.go b/modules/core/04-channel/v2/types/acknowledgement.go index 0fe321614e9..6f79ad38520 100644 --- a/modules/core/04-channel/v2/types/acknowledgement.go +++ b/modules/core/04-channel/v2/types/acknowledgement.go @@ -23,14 +23,24 @@ func NewAcknowledgement(appAcknowledgements ...[]byte) Acknowledgement { // Validate performs a basic validation of the acknowledgement func (ack Acknowledgement) Validate() error { - if len(ack.AppAcknowledgements) != 1 { - return errorsmod.Wrap(ErrInvalidAcknowledgement, "app acknowledgements must be of length one") + // acknowledgement list should be non-empty + if len(ack.AppAcknowledgements) == 0 { + return errorsmod.Wrap(ErrInvalidAcknowledgement, "app acknowledgements must be non-empty") } - for _, ack := range ack.AppAcknowledgements { - if len(ack) == 0 { + for _, a := range ack.AppAcknowledgements { + // Each app acknowledgement should be non-empty + if len(a) == 0 { return errorsmod.Wrap(ErrInvalidAcknowledgement, "app acknowledgement cannot be empty") } + + // Ensure that the app acknowledgement contains ErrorAcknowledgement + // **if and only if** the app acknowledgement list has a single element + if len(ack.AppAcknowledgements) > 1 { + if bytes.Equal(a, ErrorAcknowledgement[:]) { + return errorsmod.Wrap(ErrInvalidAcknowledgement, "cannot have the error acknowledgement in multi acknowledgement list") + } + } } return nil diff --git a/modules/core/04-channel/v2/types/acknowledgement_test.go b/modules/core/04-channel/v2/types/acknowledgement_test.go index 458e0c6b2e5..e9fd3caf53e 100644 --- a/modules/core/04-channel/v2/types/acknowledgement_test.go +++ b/modules/core/04-channel/v2/types/acknowledgement_test.go @@ -22,8 +22,13 @@ func (s *TypesTestSuite) Test_ValidateAcknowledgement() { nil, }, { - "failure: more than one app acknowledgements", + "success: more than one app acknowledgements", types.NewAcknowledgement([]byte("appAck1"), []byte("appAck2")), + nil, + }, + { + "failure: empty acknowledgement", + types.NewAcknowledgement(), types.ErrInvalidAcknowledgement, }, { @@ -31,6 +36,11 @@ func (s *TypesTestSuite) Test_ValidateAcknowledgement() { types.NewAcknowledgement([]byte("")), types.ErrInvalidAcknowledgement, }, + { + "failure: error acknowledgment in multiple payload list", + types.NewAcknowledgement(types.ErrorAcknowledgement[:], []byte("appAck2")), + types.ErrInvalidAcknowledgement, + }, } for _, tc := range testCases { diff --git a/modules/core/04-channel/v2/types/msgs.go b/modules/core/04-channel/v2/types/msgs.go index 0843e6acc32..209369db90a 100644 --- a/modules/core/04-channel/v2/types/msgs.go +++ b/modules/core/04-channel/v2/types/msgs.go @@ -49,8 +49,8 @@ func (msg *MsgSendPacket) ValidateBasic() error { return errorsmod.Wrap(ErrInvalidTimeout, "timeout must not be 0") } - if len(msg.Payloads) != 1 { - return errorsmod.Wrapf(ErrInvalidPayload, "payloads must be of length 1, got %d instead", len(msg.Payloads)) + if len(msg.Payloads) == 0 { + return errorsmod.Wrapf(ErrInvalidPayload, "payload length must be greater than 0") } for _, pd := range msg.Payloads { diff --git a/modules/core/04-channel/v2/types/msgs_test.go b/modules/core/04-channel/v2/types/msgs_test.go index dc251cca0cc..86ca6ca2d40 100644 --- a/modules/core/04-channel/v2/types/msgs_test.go +++ b/modules/core/04-channel/v2/types/msgs_test.go @@ -12,6 +12,7 @@ import ( host "github.com/cosmos/ibc-go/v10/modules/core/24-host" ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" ibctesting "github.com/cosmos/ibc-go/v10/testing" + "github.com/cosmos/ibc-go/v10/testing/mock/v2" mockv2 "github.com/cosmos/ibc-go/v10/testing/mock/v2" ) @@ -37,6 +38,7 @@ func TestTypesTestSuite(t *testing.T) { func (s *TypesTestSuite) TestMsgSendPacketValidateBasic() { var msg *types.MsgSendPacket + var payload types.Payload testCases := []struct { name string malleate func() @@ -46,6 +48,12 @@ func (s *TypesTestSuite) TestMsgSendPacketValidateBasic() { name: "success", malleate: func() {}, }, + { + name: "success, multiple payloads", + malleate: func() { + msg.Payloads = append(msg.Payloads, payload) + }, + }, { name: "failure: invalid source channel", malleate: func() { @@ -63,16 +71,16 @@ func (s *TypesTestSuite) TestMsgSendPacketValidateBasic() { { name: "failure: invalid length for payload", malleate: func() { - msg.Payloads = []types.Payload{{}, {}} + msg.Payloads = []types.Payload{} }, expError: types.ErrInvalidPayload, }, { name: "failure: invalid packetdata", malleate: func() { - msg.Payloads = []types.Payload{} + msg.Payloads = []types.Payload{{}} }, - expError: types.ErrInvalidPayload, + expError: host.ErrInvalidID, }, { name: "failure: invalid payload", @@ -81,6 +89,14 @@ func (s *TypesTestSuite) TestMsgSendPacketValidateBasic() { }, expError: host.ErrInvalidID, }, + { + name: "failure: invalid multiple payload", + malleate: func() { + payload.DestinationPort = "" + msg.Payloads = append(msg.Payloads, payload) + }, + expError: host.ErrInvalidID, + }, { name: "failure: invalid signer", malleate: func() { @@ -91,10 +107,11 @@ func (s *TypesTestSuite) TestMsgSendPacketValidateBasic() { } for _, tc := range testCases { s.Run(tc.name, func() { + payload = types.Payload{SourcePort: ibctesting.MockPort, DestinationPort: ibctesting.MockPort, Version: "ics20-1", Encoding: transfertypes.EncodingJSON, Value: ibctesting.MockPacketData} msg = types.NewMsgSendPacket( ibctesting.FirstChannelID, s.chainA.GetTimeoutTimestamp(), s.chainA.SenderAccount.GetAddress().String(), - types.Payload{SourcePort: ibctesting.MockPort, DestinationPort: ibctesting.MockPort, Version: "ics20-1", Encoding: transfertypes.EncodingJSON, Value: ibctesting.MockPacketData}, + payload, ) tc.malleate() @@ -104,7 +121,8 @@ func (s *TypesTestSuite) TestMsgSendPacketValidateBasic() { if expPass { s.Require().NoError(err) } else { - ibctesting.RequireErrorIsOrContains(s.T(), err, tc.expError) + s.Require().Error(err) + ibctesting.RequireErrorIsOrContains(s.T(), err, tc.expError, err.Error()) } }) } @@ -122,11 +140,17 @@ func (s *TypesTestSuite) TestMsgRecvPacketValidateBasic() { malleate: func() {}, }, { - name: "failure: invalid packet", + name: "success, multiple payloads", + malleate: func() { + msg.Packet.Payloads = append(msg.Packet.Payloads, mock.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)) + }, + }, + { + name: "failure: invalid payload", malleate: func() { msg.Packet.Payloads = []types.Payload{} }, - expError: types.ErrInvalidPacket, + expError: types.ErrInvalidPayload, }, { name: "failure: invalid proof commitment", @@ -138,9 +162,23 @@ func (s *TypesTestSuite) TestMsgRecvPacketValidateBasic() { { name: "failure: invalid length for packet payloads", malleate: func() { - msg.Packet.Payloads = []types.Payload{{}, {}} + msg.Packet.Payloads = []types.Payload{} }, - expError: types.ErrInvalidPacket, + expError: types.ErrInvalidPayload, + }, + { + name: "failure: invalid individual payload", + malleate: func() { + msg.Packet.Payloads = []types.Payload{{}} + }, + expError: host.ErrInvalidID, + }, + { + name: "failure: invalid multiple payload", + malleate: func() { + msg.Packet.Payloads = append(msg.Packet.Payloads, types.Payload{}) + }, + expError: host.ErrInvalidID, }, { name: "failure: invalid signer", @@ -182,6 +220,12 @@ func (s *TypesTestSuite) TestMsgAcknowledge_ValidateBasic() { name: "success", malleate: func() {}, }, + { + name: "success, multiple payloads", + malleate: func() { + msg.Packet.Payloads = append(msg.Packet.Payloads, mock.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)) + }, + }, { name: "failure: invalid proof of acknowledgement", malleate: func() { @@ -192,9 +236,23 @@ func (s *TypesTestSuite) TestMsgAcknowledge_ValidateBasic() { { name: "failure: invalid length for packet payloads", malleate: func() { - msg.Packet.Payloads = []types.Payload{{}, {}} + msg.Packet.Payloads = []types.Payload{} }, - expError: types.ErrInvalidPacket, + expError: types.ErrInvalidPayload, + }, + { + name: "failure: invalid individual payload", + malleate: func() { + msg.Packet.Payloads = []types.Payload{{}} + }, + expError: host.ErrInvalidID, + }, + { + name: "failure: invalid multiple payload", + malleate: func() { + msg.Packet.Payloads = append(msg.Packet.Payloads, types.Payload{}) + }, + expError: host.ErrInvalidID, }, { name: "failure: invalid signer", @@ -253,6 +311,12 @@ func (s *TypesTestSuite) TestMsgTimeoutValidateBasic() { name: "success", malleate: func() {}, }, + { + name: "success, multiple payloads", + malleate: func() { + msg.Packet.Payloads = append(msg.Packet.Payloads, mock.NewMockPayload(mockv2.ModuleNameA, mockv2.ModuleNameB)) + }, + }, { name: "failure: invalid signer", malleate: func() { @@ -263,9 +327,23 @@ func (s *TypesTestSuite) TestMsgTimeoutValidateBasic() { { name: "failure: invalid length for packet payloads", malleate: func() { - msg.Packet.Payloads = []types.Payload{{}, {}} + msg.Packet.Payloads = []types.Payload{} }, - expError: types.ErrInvalidPacket, + expError: types.ErrInvalidPayload, + }, + { + name: "failure: invalid individual payload", + malleate: func() { + msg.Packet.Payloads = []types.Payload{{}} + }, + expError: host.ErrInvalidID, + }, + { + name: "failure: invalid multiple payload", + malleate: func() { + msg.Packet.Payloads = append(msg.Packet.Payloads, types.Payload{}) + }, + expError: host.ErrInvalidID, }, { name: "failure: invalid packet", diff --git a/modules/core/04-channel/v2/types/packet.go b/modules/core/04-channel/v2/types/packet.go index cf2c63b11bf..85e0c41891f 100644 --- a/modules/core/04-channel/v2/types/packet.go +++ b/modules/core/04-channel/v2/types/packet.go @@ -33,8 +33,8 @@ func NewPayload(sourcePort, destPort, version, encoding string, value []byte) Pa // ValidateBasic validates that a Packet satisfies the basic requirements. func (p Packet) ValidateBasic() error { - if len(p.Payloads) != 1 { - return errorsmod.Wrap(ErrInvalidPacket, "payloads must contain exactly one payload") + if len(p.Payloads) == 0 { + return errorsmod.Wrapf(ErrInvalidPayload, "payload length must be greater than 0") } totalPayloadsSize := 0 @@ -46,7 +46,7 @@ func (p Packet) ValidateBasic() error { } if totalPayloadsSize > channeltypesv1.MaximumPayloadsSize { - return errorsmod.Wrapf(ErrInvalidPacket, "packet data bytes cannot exceed %d bytes", channeltypesv1.MaximumPayloadsSize) + return errorsmod.Wrapf(ErrInvalidPayload, "packet data bytes cannot exceed %d bytes", channeltypesv1.MaximumPayloadsSize) } if err := host.ChannelIdentifierValidator(p.SourceClient); err != nil { diff --git a/modules/core/04-channel/v2/types/packet_test.go b/modules/core/04-channel/v2/types/packet_test.go index 9a2f967473d..f32666daaa4 100644 --- a/modules/core/04-channel/v2/types/packet_test.go +++ b/modules/core/04-channel/v2/types/packet_test.go @@ -17,6 +17,7 @@ import ( // TestValidateBasic tests the ValidateBasic function of Packet func TestValidateBasic(t *testing.T) { var packet types.Packet + var payload types.Payload testCases := []struct { name string malleate func() @@ -34,28 +35,42 @@ func TestValidateBasic(t *testing.T) { }, nil, }, + { + "success, multiple payloads", + func() { + packet.Payloads = append(packet.Payloads, payload) + }, + nil, + }, { "failure: invalid single payloads size", func() { // bytes that are larger than MaxPayloadsSize packet.Payloads[0].Value = make([]byte, channeltypesv1.MaximumPayloadsSize+1) }, - types.ErrInvalidPacket, + types.ErrInvalidPayload, + }, + { + "failure: invalid total payloads size", + func() { + payload.Value = make([]byte, channeltypesv1.MaximumPayloadsSize-1) + packet.Payloads = append(packet.Payloads, payload) + }, + types.ErrInvalidPayload, }, - // TODO: add test cases for multiple payloads when enabled (#7008) { "failure: payloads is nil", func() { packet.Payloads = nil }, - types.ErrInvalidPacket, + types.ErrInvalidPayload, }, { "failure: empty payload", func() { packet.Payloads = []types.Payload{} }, - types.ErrInvalidPacket, + types.ErrInvalidPayload, }, { "failure: invalid payload source port ID", @@ -123,13 +138,14 @@ func TestValidateBasic(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - packet = types.NewPacket(1, ibctesting.FirstChannelID, ibctesting.SecondChannelID, uint64(time.Now().Unix()), types.Payload{ + payload = types.Payload{ SourcePort: ibctesting.MockPort, DestinationPort: ibctesting.MockPort, Version: "ics20-v2", Encoding: transfertypes.EncodingProtobuf, Value: mock.MockPacketData, - }) + } + packet = types.NewPacket(1, ibctesting.FirstChannelID, ibctesting.SecondChannelID, uint64(time.Now().Unix()), payload) tc.malleate() diff --git a/testing/endpoint_v2.go b/testing/endpoint_v2.go index ca6b9c5305b..23b0ebbe12f 100644 --- a/testing/endpoint_v2.go +++ b/testing/endpoint_v2.go @@ -21,18 +21,18 @@ func (endpoint *Endpoint) RegisterCounterparty() (err error) { } // MsgSendPacket sends a packet on the associated endpoint using a predefined sender. The constructed packet is returned. -func (endpoint *Endpoint) MsgSendPacket(timeoutTimestamp uint64, payload channeltypesv2.Payload) (channeltypesv2.Packet, error) { +func (endpoint *Endpoint) MsgSendPacket(timeoutTimestamp uint64, payloads ...channeltypesv2.Payload) (channeltypesv2.Packet, error) { senderAccount := SenderAccount{ SenderPrivKey: endpoint.Chain.SenderPrivKey, SenderAccount: endpoint.Chain.SenderAccount, } - return endpoint.MsgSendPacketWithSender(timeoutTimestamp, payload, senderAccount) + return endpoint.MsgSendPacketWithSender(timeoutTimestamp, payloads, senderAccount) } // MsgSendPacketWithSender sends a packet on the associated endpoint using the provided sender. The constructed packet is returned. -func (endpoint *Endpoint) MsgSendPacketWithSender(timeoutTimestamp uint64, payload channeltypesv2.Payload, sender SenderAccount) (channeltypesv2.Packet, error) { - msgSendPacket := channeltypesv2.NewMsgSendPacket(endpoint.ClientID, timeoutTimestamp, sender.SenderAccount.GetAddress().String(), payload) +func (endpoint *Endpoint) MsgSendPacketWithSender(timeoutTimestamp uint64, payloads []channeltypesv2.Payload, sender SenderAccount) (channeltypesv2.Packet, error) { + msgSendPacket := channeltypesv2.NewMsgSendPacket(endpoint.ClientID, timeoutTimestamp, sender.SenderAccount.GetAddress().String(), payloads...) res, err := endpoint.Chain.SendMsgsWithSender(sender, msgSendPacket) if err != nil { @@ -56,7 +56,12 @@ func (endpoint *Endpoint) MsgSendPacketWithSender(timeoutTimestamp uint64, paylo if err != nil { return channeltypesv2.Packet{}, err } - packet := channeltypesv2.NewPacket(sendResponse.Sequence, endpoint.ClientID, endpoint.Counterparty.ClientID, timeoutTimestamp, payload) + packet := channeltypesv2.NewPacket(sendResponse.Sequence, endpoint.ClientID, endpoint.Counterparty.ClientID, timeoutTimestamp, payloads...) + + err = endpoint.Counterparty.UpdateClient() + if err != nil { + return channeltypesv2.Packet{}, err + } return packet, nil } diff --git a/testing/mock/v2/ibc_module.go b/testing/mock/v2/ibc_module.go index 5e53f7e397b..33ead1f02ae 100644 --- a/testing/mock/v2/ibc_module.go +++ b/testing/mock/v2/ibc_module.go @@ -50,6 +50,9 @@ func (im IBCModule) OnRecvPacket(ctx sdk.Context, sourceChannel string, destinat if bytes.Equal(payload.Value, mockv1.MockPacketData) { return MockRecvPacketResult } + if bytes.Equal(payload.Value, mockv1.MockAsyncPacketData) { + return channeltypesv2.RecvPacketResult{Status: channeltypesv2.PacketStatus_Async} + } return channeltypesv2.RecvPacketResult{Status: channeltypesv2.PacketStatus_Failure} } diff --git a/testing/mock/v2/mock.go b/testing/mock/v2/mock.go index 2e120dd79a0..3a3b3e989e4 100644 --- a/testing/mock/v2/mock.go +++ b/testing/mock/v2/mock.go @@ -34,3 +34,13 @@ func NewErrorMockPayload(sourcePort, destPort string) channeltypesv2.Payload { Version: mockv1.Version, } } + +func NewAsyncMockPayload(sourcePort, destPort string) channeltypesv2.Payload { + return channeltypesv2.Payload{ + SourcePort: sourcePort, + DestinationPort: destPort, + Encoding: transfertypes.EncodingProtobuf, + Value: mockv1.MockAsyncPacketData, + Version: mockv1.Version, + } +}