From 4e24f41a489f55d11e7477a1365e77d9acdf236a Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 15:55:57 +0000 Subject: [PATCH 01/22] gmp tests --- e2e/tests/gmp/base_test.go | 263 ++++++++++++++ e2e/testsuite/codec.go | 2 + modules/apps/27-gmp/ibc_module_test.go | 128 +++++++ modules/apps/27-gmp/keeper/export_test.go | 10 + modules/apps/27-gmp/keeper/msg_server_test.go | 86 +++++ .../apps/27-gmp/keeper/query_server_test.go | 152 ++++++++ modules/apps/27-gmp/keeper/relay_test.go | 236 +++++++++++++ modules/apps/27-gmp/types/account_test.go | 187 ++++++++++ modules/apps/27-gmp/types/packet_test.go | 330 ++++++++++++++++++ .../apps/27-gmp/types/solidity_abi_test.go | 205 +++++++++++ simapp/app.go | 21 +- 11 files changed, 1618 insertions(+), 2 deletions(-) create mode 100644 e2e/tests/gmp/base_test.go create mode 100644 modules/apps/27-gmp/ibc_module_test.go create mode 100644 modules/apps/27-gmp/keeper/export_test.go create mode 100644 modules/apps/27-gmp/keeper/msg_server_test.go create mode 100644 modules/apps/27-gmp/keeper/query_server_test.go create mode 100644 modules/apps/27-gmp/keeper/relay_test.go create mode 100644 modules/apps/27-gmp/types/account_test.go create mode 100644 modules/apps/27-gmp/types/packet_test.go create mode 100644 modules/apps/27-gmp/types/solidity_abi_test.go diff --git a/e2e/tests/gmp/base_test.go b/e2e/tests/gmp/base_test.go new file mode 100644 index 00000000000..edd9b00df71 --- /dev/null +++ b/e2e/tests/gmp/base_test.go @@ -0,0 +1,263 @@ +//go:build !test_e2e + +package gmp + +import ( + "context" + "crypto/ecdsa" + "crypto/sha256" + "testing" + "time" + + "github.com/cosmos/gogoproto/proto" + "github.com/cosmos/interchaintest/v10/ibc" + test "github.com/cosmos/interchaintest/v10/testutil" + "github.com/ethereum/go-ethereum/crypto" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testsuite/query" + "github.com/cosmos/ibc-go/e2e/testvalues" + gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + clientv2types "github.com/cosmos/ibc-go/v10/modules/core/02-client/v2/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + hostv2 "github.com/cosmos/ibc-go/v10/modules/core/24-host/v2" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" + "github.com/cosmos/ibc-go/v10/modules/light-clients/attestations" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +const ( + testSalt = "test-salt" + numAttestors = 3 + quorumThreshold = 2 + proofHeight = 100 +) + +func TestGMPTestSuite(t *testing.T) { + testifysuite.Run(t, new(GMPTestSuite)) +} + +type GMPTestSuite struct { + testsuite.E2ETestSuite + attestorKeys []*ecdsa.PrivateKey +} + +func (s *GMPTestSuite) SetupSuite() { + s.SetupChains(context.TODO(), 1, nil) + s.setupAttestors() +} + +// TestMsgSendCall_BankTransfer tests the full GMP flow using attestations light clients: +// 1. Create two attestations clients on a single chain +// 2. Send MsgSendCall to create GMP packet +// 3. Relay packet with attestation proof +// 4. Verify bank transfer executed on destination +func (s *GMPTestSuite) TestMsgSendCall_BankTransfer() { + t := s.T() + ctx := context.TODO() + + chain := s.GetAllChains()[0] + chainDenom := chain.Config().Denom + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + senderWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + recipientWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + clientIDA, clientIDB string + packet channeltypesv2.Packet + ack channeltypesv2.Acknowledgement + gmpAccountAddr string + initialBalance sdkmath.Int + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chain), "failed to wait for blocks") + + proofTimestamp := uint64(time.Now().UnixNano()) + + t.Run("create attestations clients", func(t *testing.T) { + clientIDA = s.createAttestationsClient(ctx, chain, rlyWallet, proofTimestamp) + clientIDB = s.createAttestationsClient(ctx, chain, rlyWallet, proofTimestamp) + t.Logf("Created clients: %s, %s", clientIDA, clientIDB) + }) + + t.Run("verify client status", func(t *testing.T) { + for _, clientID := range []string{clientIDA, clientIDB} { + status, err := query.ClientStatus(ctx, chain, clientID) + s.Require().NoError(err) + s.Require().Equal(ibcexported.Active.String(), status) + } + }) + + t.Run("register counterparties", func(t *testing.T) { + s.registerCounterparty(ctx, chain, rlyWallet, clientIDA, clientIDB) + s.registerCounterparty(ctx, chain, rlyWallet, clientIDB, clientIDA) + t.Logf("Registered counterparties: %s <-> %s", clientIDA, clientIDB) + }) + + t.Run("get initial balance", func(t *testing.T) { + balance, err := query.Balance(ctx, chain, recipientWallet.FormattedAddress(), chainDenom) + s.Require().NoError(err) + initialBalance = balance + }) + + t.Run("compute and fund GMP account", func(t *testing.T) { + // GMP account is derived from destination client, sender, and salt + accountID := gmptypes.NewAccountIdentifier(clientIDB, senderWallet.FormattedAddress(), []byte(testSalt)) + addr, err := gmptypes.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + gmpAccountAddr = sdk.AccAddress(addr).String() + + msgSend := &banktypes.MsgSend{ + FromAddress: rlyWallet.FormattedAddress(), + ToAddress: gmpAccountAddr, + Amount: sdk.NewCoins(sdk.NewCoin(chainDenom, sdkmath.NewInt(testvalues.StartingTokenAmount))), + } + txResp := s.BroadcastMessages(ctx, chain, rlyWallet, msgSend) + s.AssertTxSuccess(txResp) + t.Logf("GMP account: %s", gmpAccountAddr) + }) + + t.Run("send MsgSendCall", func(t *testing.T) { + msgSend := &banktypes.MsgSend{ + FromAddress: gmpAccountAddr, + ToAddress: recipientWallet.FormattedAddress(), + Amount: sdk.NewCoins(sdk.NewCoin(chainDenom, sdkmath.NewInt(testvalues.IBCTransferAmount))), + } + + payload, err := gmptypes.SerializeCosmosTx(testsuite.Codec(), []proto.Message{msgSend}) + s.Require().NoError(err) + + msgSendCall := gmptypes.NewMsgSendCall( + clientIDA, + senderWallet.FormattedAddress(), + "", + payload, + []byte(testSalt), + uint64(time.Now().Add(10*time.Minute).Unix()), + gmptypes.EncodingProtobuf, + "", + ) + + txResp := s.BroadcastMessages(ctx, chain, senderWallet, msgSendCall) + s.AssertTxSuccess(txResp) + + packet, err = ibctesting.ParseV2PacketFromEvents(txResp.Events) + s.Require().NoError(err) + t.Logf("Packet sent: seq=%d", packet.Sequence) + }) + + t.Run("recv packet", func(t *testing.T) { + commitment := channeltypesv2.CommitPacket(packet) + path := s.hashPath(hostv2.PacketCommitmentKey(packet.SourceClient, packet.Sequence)) + proof := s.createAttestationProof(path, commitment) + + msgRecvPacket := channeltypesv2.NewMsgRecvPacket( + packet, proof, clienttypes.NewHeight(0, proofHeight), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chain, rlyWallet, msgRecvPacket) + s.AssertTxSuccess(txResp) + + ackBz, err := ibctesting.ParseAckV2FromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NoError(proto.Unmarshal(ackBz, &ack)) + }) + + t.Run("acknowledge packet", func(t *testing.T) { + commitment := channeltypesv2.CommitAcknowledgement(ack) + path := s.hashPath(hostv2.PacketAcknowledgementKey(packet.DestinationClient, packet.Sequence)) + proof := s.createAttestationProof(path, commitment) + + msgAck := channeltypesv2.NewMsgAcknowledgement( + packet, ack, proof, clienttypes.NewHeight(0, proofHeight), rlyWallet.FormattedAddress(), + ) + + txResp := s.BroadcastMessages(ctx, chain, rlyWallet, msgAck) + s.AssertTxSuccess(txResp) + }) + + t.Run("verify transfer", func(t *testing.T) { + balance, err := query.Balance(ctx, chain, recipientWallet.FormattedAddress(), chainDenom) + s.Require().NoError(err) + + expected := initialBalance.Add(sdkmath.NewInt(testvalues.IBCTransferAmount)) + s.Require().Equal(expected, balance) + t.Logf("Recipient balance: %s -> %s", initialBalance, balance) + }) +} + +func (s *GMPTestSuite) setupAttestors() { + for range numAttestors { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + s.attestorKeys = append(s.attestorKeys, key) + } +} + +func (s *GMPTestSuite) getAttestorAddresses() []string { + addresses := make([]string, len(s.attestorKeys)) + for i, key := range s.attestorKeys { + addresses[i] = crypto.PubkeyToAddress(key.PublicKey).Hex() + } + return addresses +} + +func (s *GMPTestSuite) createAttestationsClient(ctx context.Context, chain ibc.Chain, wallet ibc.Wallet, timestamp uint64) string { + clientState := attestations.NewClientState(s.getAttestorAddresses(), quorumThreshold, proofHeight) + consensusState := &attestations.ConsensusState{Timestamp: timestamp} + + msg, err := clienttypes.NewMsgCreateClient(clientState, consensusState, wallet.FormattedAddress()) + s.Require().NoError(err) + + txResp := s.BroadcastMessages(ctx, chain, wallet, msg) + s.AssertTxSuccess(txResp) + + var res clienttypes.MsgCreateClientResponse + s.Require().NoError(testsuite.UnmarshalMsgResponses(txResp, &res)) + return res.ClientId +} + +func (s *GMPTestSuite) registerCounterparty(ctx context.Context, chain ibc.Chain, wallet ibc.Wallet, clientID, counterpartyID string) { + msg := clientv2types.NewMsgRegisterCounterparty( + clientID, + [][]byte{[]byte("")}, + counterpartyID, + wallet.FormattedAddress(), + ) + txResp := s.BroadcastMessages(ctx, chain, wallet, msg) + s.AssertTxSuccess(txResp) +} + +func (s *GMPTestSuite) hashPath(key []byte) []byte { + return crypto.Keccak256(key) +} + +func (s *GMPTestSuite) createAttestationProof(path, commitment []byte) []byte { + attestation := &attestations.PacketAttestation{ + Height: proofHeight, + Packets: []attestations.PacketCompact{{Path: path, Commitment: commitment}}, + } + data, err := attestation.ABIEncode() + s.Require().NoError(err) + + hash := sha256.Sum256(data) + signatures := make([][]byte, len(s.attestorKeys)) + for i, key := range s.attestorKeys { + sig, err := crypto.Sign(hash[:], key) + s.Require().NoError(err) + signatures[i] = sig + } + + proof := &attestations.AttestationProof{AttestationData: data, Signatures: signatures} + proofBz, err := proto.Marshal(proof) + s.Require().NoError(err) + return proofBz +} diff --git a/e2e/testsuite/codec.go b/e2e/testsuite/codec.go index 9840e70914b..b93671ff32d 100644 --- a/e2e/testsuite/codec.go +++ b/e2e/testsuite/codec.go @@ -23,6 +23,7 @@ import ( proposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types" + gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" icacontrollertypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/controller/types" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" packetforwardtypes "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" @@ -63,6 +64,7 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, testutil.TestEncodingConfig) { cfg := testutil.MakeTestEncodingConfig() // ibc types + gmptypes.RegisterInterfaces(cfg.InterfaceRegistry) icacontrollertypes.RegisterInterfaces(cfg.InterfaceRegistry) icahosttypes.RegisterInterfaces(cfg.InterfaceRegistry) solomachine.RegisterInterfaces(cfg.InterfaceRegistry) diff --git a/modules/apps/27-gmp/ibc_module_test.go b/modules/apps/27-gmp/ibc_module_test.go new file mode 100644 index 00000000000..95a1452d676 --- /dev/null +++ b/modules/apps/27-gmp/ibc_module_test.go @@ -0,0 +1,128 @@ +package gmp_test + +import ( + "testing" + + testifysuite "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + + gmp "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp" + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +type IBCModuleTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + chainA *ibctesting.TestChain +} + +const ( + validClientID = ibctesting.FirstClientID + invalidClientID = "invalid" +) + +func TestIBCModuleTestSuite(t *testing.T) { + testifysuite.Run(t, new(IBCModuleTestSuite)) +} + +func (s *IBCModuleTestSuite) SetupTest() { + s.coordinator = ibctesting.NewCoordinator(s.T(), 1) + s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func (s *IBCModuleTestSuite) TestOnSendPacket() { + var ( + module *gmp.IBCModule + payload channeltypesv2.Payload + signer sdk.AccAddress + sourceClient string + destClient string + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() {}, + nil, + }, + { + "failure: invalid source port", + func() { + payload.SourcePort = "invalid-port" + }, + channeltypesv2.ErrInvalidPacket, + }, + { + "failure: invalid destination port", + func() { + payload.DestinationPort = "invalid-port" + }, + channeltypesv2.ErrInvalidPacket, + }, + { + "failure: invalid source client ID", + func() { + sourceClient = invalidClientID + }, + channeltypesv2.ErrInvalidPacket, + }, + { + "failure: invalid destination client ID", + func() { + destClient = invalidClientID + }, + channeltypesv2.ErrInvalidPacket, + }, + { + "failure: sender != signer", + func() { + signer = s.chainA.SenderAccounts[1].SenderAccount.GetAddress() + }, + ibcerrors.ErrUnauthorized, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + module = gmp.NewIBCModule(s.chainA.GetSimApp().GMPKeeper) + signer = s.chainA.SenderAccount.GetAddress() + sourceClient = validClientID + destClient = validClientID + + packetData := types.NewGMPPacketData(signer.String(), "", []byte("salt"), []byte("payload"), "") + dataBz, err := types.MarshalPacketData(&packetData, types.Version, types.EncodingProtobuf) + s.Require().NoError(err) + + payload = channeltypesv2.NewPayload(types.PortID, types.PortID, types.Version, types.EncodingProtobuf, dataBz) + + tc.malleate() + + err = module.OnSendPacket( + s.chainA.GetContext(), + sourceClient, + destClient, + 1, + payload, + signer, + ) + + expPass := tc.expErr == nil + if expPass { + s.Require().NoError(err) + } else { + s.Require().ErrorIs(err, tc.expErr) + } + }) + } +} diff --git a/modules/apps/27-gmp/keeper/export_test.go b/modules/apps/27-gmp/keeper/export_test.go new file mode 100644 index 00000000000..16efe9640d4 --- /dev/null +++ b/modules/apps/27-gmp/keeper/export_test.go @@ -0,0 +1,10 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// AuthenticateTx is exported for testing purposes. +func (k *Keeper) AuthenticateTx(ctx sdk.Context, account sdk.AccountI, msgs []sdk.Msg) error { + return k.authenticateTx(ctx, account, msgs) +} diff --git a/modules/apps/27-gmp/keeper/msg_server_test.go b/modules/apps/27-gmp/keeper/msg_server_test.go new file mode 100644 index 00000000000..836c75b5bc0 --- /dev/null +++ b/modules/apps/27-gmp/keeper/msg_server_test.go @@ -0,0 +1,86 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +func (s *KeeperTestSuite) TestSendCall() { + var msg *types.MsgSendCall + + testCases := []struct { + name string + malleate func() + expEncoding string + }{ + { + "success: empty encoding defaults to ABI", + func() { + msg.Encoding = "" + }, + types.EncodingABI, + }, + { + "success: protobuf encoding", + func() { + msg.Encoding = types.EncodingProtobuf + }, + types.EncodingProtobuf, + }, + { + "success: JSON encoding", + func() { + msg.Encoding = types.EncodingJSON + }, + types.EncodingJSON, + }, + { + "success: ABI encoding", + func() { + msg.Encoding = types.EncodingABI + }, + types.EncodingABI, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + path := ibctesting.NewPath(s.chainA, s.chainB) + path.SetupV2() + + sender := s.chainA.SenderAccount.GetAddress() + recipient := s.chainB.SenderAccount.GetAddress() + payload := s.serializeMsgs(&banktypes.MsgSend{ + FromAddress: sender.String(), + ToAddress: recipient.String(), + Amount: sdk.NewCoins(ibctesting.TestCoin), + }) + + msg = types.NewMsgSendCall( + path.EndpointA.ClientID, + sender.String(), + "", + payload, + []byte(testSalt), + uint64(s.chainA.GetContext().BlockTime().Add(time.Hour).Unix()), + types.EncodingProtobuf, + "", + ) + + tc.malleate() + + resp, err := s.chainA.GetSimApp().GMPKeeper.SendCall(s.chainA.GetContext(), msg) + + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(uint64(1), resp.Sequence) + }) + } +} diff --git a/modules/apps/27-gmp/keeper/query_server_test.go b/modules/apps/27-gmp/keeper/query_server_test.go new file mode 100644 index 00000000000..54acc0e4cd7 --- /dev/null +++ b/modules/apps/27-gmp/keeper/query_server_test.go @@ -0,0 +1,152 @@ +package keeper_test + +import ( + "encoding/hex" + + "cosmossdk.io/collections" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +func (s *KeeperTestSuite) TestQueryAccountAddress() { + var req *types.QueryAccountAddressRequest + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() {}, + nil, + }, + { + "success: empty salt", + func() { + req.Salt = "" + }, + nil, + }, + { + "failure: invalid salt hex", + func() { + req.Salt = "not-hex" + }, + hex.InvalidByteError('n'), + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + req = &types.QueryAccountAddressRequest{ + ClientId: ibctesting.FirstClientID, + Sender: s.chainA.SenderAccount.GetAddress().String(), + Salt: hex.EncodeToString([]byte(testSalt)), + } + + tc.malleate() + + resp, err := s.chainA.GetSimApp().GMPKeeper.AccountAddress(s.chainA.GetContext(), req) + + expPass := tc.expErr == nil + if expPass { + s.Require().NoError(err) + s.Require().NotEmpty(resp.AccountAddress) + + _, err := sdk.AccAddressFromBech32(resp.AccountAddress) + s.Require().NoError(err) + } else { + s.Require().ErrorContains(err, tc.expErr.Error()) + } + }) + } +} + +func (s *KeeperTestSuite) TestQueryAccountIdentifier() { + var ( + req *types.QueryAccountIdentifierRequest + gmpAccountAddr string + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() { + s.createGMPAccount(gmpAccountAddr) + }, + nil, + }, + { + "failure: invalid address", + func() { + req.AccountAddress = "invalid" + }, + ibcerrors.ErrInvalidAddress, + }, + { + "failure: account not found", + func() {}, + collections.ErrNotFound, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + sender := s.chainB.SenderAccount.GetAddress().String() + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + gmpAccountAddr = sdk.AccAddress(addr).String() + + req = &types.QueryAccountIdentifierRequest{ + AccountAddress: gmpAccountAddr, + } + + tc.malleate() + + resp, err := s.chainA.GetSimApp().GMPKeeper.AccountIdentifier(s.chainA.GetContext(), req) + + expPass := tc.expErr == nil + if expPass { + s.Require().NoError(err) + s.Require().Equal(ibctesting.FirstClientID, resp.AccountId.ClientId) + s.Require().Equal(sender, resp.AccountId.Sender) + s.Require().Equal([]byte(testSalt), resp.AccountId.Salt) + } else { + s.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (s *KeeperTestSuite) createGMPAccount(gmpAccountAddr string) { + sender := s.chainB.SenderAccount.GetAddress().String() + recipient := s.chainA.SenderAccount.GetAddress() + + gmpAddr, _ := sdk.AccAddressFromBech32(gmpAccountAddr) + s.fundAccount(gmpAddr, sdk.NewCoins(ibctesting.TestCoin)) + + data := types.NewGMPPacketData(sender, "", []byte(testSalt), nil, "") + data.Payload = s.serializeMsgs(s.newMsgSend(gmpAddr, recipient)) + + _, err := s.chainA.GetSimApp().GMPKeeper.OnRecvPacket( + s.chainA.GetContext(), + &data, + types.PortID, ibctesting.FirstClientID, + types.PortID, ibctesting.FirstClientID, + ) + s.Require().NoError(err) +} diff --git a/modules/apps/27-gmp/keeper/relay_test.go b/modules/apps/27-gmp/keeper/relay_test.go new file mode 100644 index 00000000000..308579b7c90 --- /dev/null +++ b/modules/apps/27-gmp/keeper/relay_test.go @@ -0,0 +1,236 @@ +package keeper_test + +import ( + "testing" + + "github.com/cosmos/gogoproto/proto" + testifysuite "github.com/stretchr/testify/suite" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/keeper" + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +const testSalt = "test-salt" + +type KeeperTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain +} + +func TestKeeperTestSuite(t *testing.T) { + testifysuite.Run(t, new(KeeperTestSuite)) +} + +func (s *KeeperTestSuite) SetupTest() { + s.coordinator = ibctesting.NewCoordinator(s.T(), 2) + s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) + s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) +} + +func (s *KeeperTestSuite) TestAuthenticateTx() { + var ( + gmpKeeper *keeper.Keeper + account sdk.AccountI + msgs []sdk.Msg + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: single message", + func() { + msgs = []sdk.Msg{s.newMsgSend(account.GetAddress(), s.chainB.SenderAccount.GetAddress())} + }, + nil, + }, + { + "success: multiple messages", + func() { + msgs = []sdk.Msg{ + s.newMsgSend(account.GetAddress(), s.chainB.SenderAccount.GetAddress()), + s.newMsgSend(account.GetAddress(), s.chainB.SenderAccount.GetAddress()), + } + }, + nil, + }, + { + "failure: empty messages", + func() { + msgs = []sdk.Msg{} + }, + types.ErrInvalidPayload, + }, + { + "failure: wrong signer", + func() { + msgs = []sdk.Msg{s.newMsgSend(s.chainB.SenderAccount.GetAddress(), s.chainA.SenderAccount.GetAddress())} + }, + ibcerrors.ErrUnauthorized, + }, + { + "failure: one wrong signer in multiple messages", + func() { + msgs = []sdk.Msg{ + s.newMsgSend(account.GetAddress(), s.chainB.SenderAccount.GetAddress()), + s.newMsgSend(s.chainB.SenderAccount.GetAddress(), s.chainA.SenderAccount.GetAddress()), + } + }, + ibcerrors.ErrUnauthorized, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + gmpKeeper = s.chainA.GetSimApp().GMPKeeper + account = s.chainA.SenderAccount + + tc.malleate() + + err := gmpKeeper.AuthenticateTx(s.chainA.GetContext(), account, msgs) + expPass := tc.expErr == nil + if expPass { + s.Require().NoError(err) + } else { + s.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (s *KeeperTestSuite) TestOnRecvPacket() { + var ( + gmpKeeper *keeper.Keeper + packetData *types.GMPPacketData + gmpAccountAddr sdk.AccAddress + sender string + recipient sdk.AccAddress + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: bank transfer", + func() { + s.fundAccount(gmpAccountAddr, sdk.NewCoins(ibctesting.TestCoin)) + packetData.Payload = s.serializeMsgs(s.newMsgSend(gmpAccountAddr, recipient)) + }, + nil, + }, + { + "success: multiple messages", + func() { + amount := sdk.NewCoin(ibctesting.TestCoin.Denom, sdkmath.NewInt(500)) + s.fundAccount(gmpAccountAddr, sdk.NewCoins(sdk.NewCoin(ibctesting.TestCoin.Denom, sdkmath.NewInt(2000)))) + packetData.Payload = s.serializeMsgs( + s.newMsgSendWithAmount(gmpAccountAddr, recipient, amount), + s.newMsgSendWithAmount(gmpAccountAddr, recipient, amount), + ) + }, + nil, + }, + { + "failure: unauthorized signer", + func() { + senderAddr, _ := sdk.AccAddressFromBech32(sender) + packetData.Payload = s.serializeMsgs(s.newMsgSend(senderAddr, recipient)) + }, + ibcerrors.ErrUnauthorized, + }, + { + "failure: invalid payload", + func() { + packetData.Payload = []byte("invalid") + }, + ibcerrors.ErrInvalidType, + }, + { + "failure: empty payload", + func() { + packetData.Payload = []byte{} + }, + types.ErrInvalidPayload, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + gmpKeeper = s.chainA.GetSimApp().GMPKeeper + sender = s.chainB.SenderAccount.GetAddress().String() + recipient = s.chainA.SenderAccount.GetAddress() + + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + gmpAccountAddr = addr + + data := types.NewGMPPacketData(sender, "", []byte(testSalt), nil, "") + packetData = &data + + tc.malleate() + + result, err := gmpKeeper.OnRecvPacket( + s.chainA.GetContext(), + packetData, + types.PortID, ibctesting.FirstClientID, + types.PortID, ibctesting.FirstClientID, + ) + + expPass := tc.expErr == nil + if expPass { + s.Require().NoError(err) + s.Require().NotEmpty(result) + } else { + s.Require().ErrorIs(err, tc.expErr) + s.Require().Nil(result) + } + }) + } +} + +func (s *KeeperTestSuite) fundAccount(addr sdk.AccAddress, coins sdk.Coins) { + err := s.chainA.GetSimApp().BankKeeper.SendCoins( + s.chainA.GetContext(), + s.chainA.SenderAccount.GetAddress(), + addr, + coins, + ) + s.Require().NoError(err) +} + +func (s *KeeperTestSuite) newMsgSend(from, to sdk.AccAddress) *banktypes.MsgSend { + return s.newMsgSendWithAmount(from, to, ibctesting.TestCoin) +} + +func (s *KeeperTestSuite) newMsgSendWithAmount(from, to sdk.AccAddress, amount sdk.Coin) *banktypes.MsgSend { + return &banktypes.MsgSend{ + FromAddress: from.String(), + ToAddress: to.String(), + Amount: sdk.NewCoins(amount), + } +} + +func (s *KeeperTestSuite) serializeMsgs(msgs ...proto.Message) []byte { + payload, err := types.SerializeCosmosTx(s.chainA.GetSimApp().AppCodec(), msgs) + s.Require().NoError(err) + return payload +} diff --git a/modules/apps/27-gmp/types/account_test.go b/modules/apps/27-gmp/types/account_test.go new file mode 100644 index 00000000000..e0334b2a175 --- /dev/null +++ b/modules/apps/27-gmp/types/account_test.go @@ -0,0 +1,187 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + host "github.com/cosmos/ibc-go/v10/modules/core/24-host" + ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" +) + +func TestBuildAddressPredictable(t *testing.T) { + testCases := []struct { + name string + accountID *types.AccountIdentifier + expErr error + }{ + { + "success: valid account identifier", + &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: []byte("randomsalt"), + }, + nil, + }, + { + "success: empty salt is allowed", + &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: []byte{}, + }, + nil, + }, + { + "success: nil salt is allowed", + &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: nil, + }, + nil, + }, + { + "failure: invalid client ID format - too short", + &types.AccountIdentifier{ + ClientId: "abc", + Sender: "cosmos1sender", + Salt: []byte("salt"), + }, + host.ErrInvalidID, + }, + { + "failure: empty client ID", + &types.AccountIdentifier{ + ClientId: "", + Sender: "cosmos1sender", + Salt: []byte("salt"), + }, + host.ErrInvalidID, + }, + { + "failure: empty sender", + &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "", + Salt: []byte("salt"), + }, + ibcerrors.ErrInvalidAddress, + }, + { + "failure: whitespace-only sender", + &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: " ", + Salt: []byte("salt"), + }, + ibcerrors.ErrInvalidAddress, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + addr, err := types.BuildAddressPredictable(tc.accountID) + if tc.expErr != nil { + require.ErrorIs(t, err, tc.expErr) + require.Nil(t, addr) + } else { + require.NoError(t, err) + require.NotNil(t, addr) + require.Len(t, addr, types.AccountAddrLen, "address should be exactly %d bytes", types.AccountAddrLen) + } + }) + } +} + +func TestBuildAddressPredictable_Determinism(t *testing.T) { + accountID := &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: []byte("randomsalt"), + } + + // Generate address multiple times + addr1, err := types.BuildAddressPredictable(accountID) + require.NoError(t, err) + + addr2, err := types.BuildAddressPredictable(accountID) + require.NoError(t, err) + + addr3, err := types.BuildAddressPredictable(accountID) + require.NoError(t, err) + + // All addresses should be identical + require.Equal(t, addr1, addr2, "addresses should be deterministic") + require.Equal(t, addr2, addr3, "addresses should be deterministic") +} + +func TestBuildAddressPredictable_Uniqueness(t *testing.T) { + // Base account identifier + baseAccountID := &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: []byte("salt1"), + } + + baseAddr, err := types.BuildAddressPredictable(baseAccountID) + require.NoError(t, err) + + // Different salt should produce different address + differentSaltID := &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: []byte("salt2"), + } + differentSaltAddr, err := types.BuildAddressPredictable(differentSaltID) + require.NoError(t, err) + require.NotEqual(t, baseAddr, differentSaltAddr, "different salts should produce different addresses") + + // Different sender should produce different address + differentSenderID := &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1different", + Salt: []byte("salt1"), + } + differentSenderAddr, err := types.BuildAddressPredictable(differentSenderID) + require.NoError(t, err) + require.NotEqual(t, baseAddr, differentSenderAddr, "different senders should produce different addresses") + + // Different client ID should produce different address + differentClientID := &types.AccountIdentifier{ + ClientId: "07-tendermint-1", + Sender: "cosmos1sender", + Salt: []byte("salt1"), + } + differentClientAddr, err := types.BuildAddressPredictable(differentClientID) + require.NoError(t, err) + require.NotEqual(t, baseAddr, differentClientAddr, "different client IDs should produce different addresses") +} + +func TestNewAccountIdentifier(t *testing.T) { + clientID := "07-tendermint-0" + sender := "cosmos1sender" + salt := []byte("salt") + + accountID := types.NewAccountIdentifier(clientID, sender, salt) + + require.Equal(t, clientID, accountID.ClientId) + require.Equal(t, sender, accountID.Sender) + require.Equal(t, salt, accountID.Salt) +} + +func TestNewICS27Account(t *testing.T) { + addr := "cosmos1address" + accountID := &types.AccountIdentifier{ + ClientId: "07-tendermint-0", + Sender: "cosmos1sender", + Salt: []byte("salt"), + } + + account := types.NewICS27Account(addr, accountID) + + require.Equal(t, addr, account.Address) + require.Equal(t, accountID, account.AccountId) +} diff --git a/modules/apps/27-gmp/types/packet_test.go b/modules/apps/27-gmp/types/packet_test.go new file mode 100644 index 00000000000..5abb5fec54b --- /dev/null +++ b/modules/apps/27-gmp/types/packet_test.go @@ -0,0 +1,330 @@ +package types_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + 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" +) + +func TestGMPPacketData_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + packetData types.GMPPacketData + expErr error + }{ + { + "success: valid packet", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", []byte("salt"), []byte("payload"), "memo"), + nil, + }, + { + "success: empty receiver is allowed", + types.NewGMPPacketData("cosmos1sender", "", []byte("salt"), []byte("payload"), "memo"), + nil, + }, + { + "success: empty salt is allowed", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", []byte{}, []byte("payload"), "memo"), + nil, + }, + { + "success: empty payload is allowed", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", []byte("salt"), []byte{}, "memo"), + nil, + }, + { + "success: empty memo is allowed", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", []byte("salt"), []byte("payload"), ""), + nil, + }, + { + "failure: empty sender", + types.NewGMPPacketData("", "cosmos1receiver", []byte("salt"), []byte("payload"), "memo"), + ibcerrors.ErrInvalidAddress, + }, + { + "failure: whitespace-only sender", + types.NewGMPPacketData(" ", "cosmos1receiver", []byte("salt"), []byte("payload"), "memo"), + ibcerrors.ErrInvalidAddress, + }, + { + "failure: receiver too long", + types.NewGMPPacketData("cosmos1sender", ibctesting.GenerateString(types.MaximumReceiverLength+1), []byte("salt"), []byte("payload"), "memo"), + ibcerrors.ErrInvalidAddress, + }, + { + "failure: payload too long", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", []byte("salt"), make([]byte, types.MaximumPayloadLength+1), "memo"), + types.ErrInvalidPayload, + }, + { + "failure: salt too long", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", make([]byte, types.MaximumSaltLength+1), []byte("payload"), "memo"), + types.ErrInvalidSalt, + }, + { + "failure: memo too long", + types.NewGMPPacketData("cosmos1sender", "cosmos1receiver", []byte("salt"), []byte("payload"), ibctesting.GenerateString(types.MaximumMemoLength+1)), + types.ErrInvalidMemo, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.packetData.ValidateBasic() + if tc.expErr == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expErr) + } + }) + } +} + +func TestMarshalUnmarshalPacketData(t *testing.T) { + packetData := &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("randomsalt"), + Payload: []byte("test payload"), + Memo: "test memo", + } + + testCases := []struct { + name string + encoding string + expErr error + }{ + { + "success: JSON encoding", + types.EncodingJSON, + nil, + }, + { + "success: Protobuf encoding", + types.EncodingProtobuf, + nil, + }, + { + "success: ABI encoding", + types.EncodingABI, + nil, + }, + { + "failure: invalid encoding", + "invalid-encoding", + types.ErrInvalidEncoding, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + bz, err := types.MarshalPacketData(packetData, types.Version, tc.encoding) + if tc.expErr != nil { + require.ErrorIs(t, err, tc.expErr) + return + } + require.NoError(t, err) + require.NotEmpty(t, bz) + + // Unmarshal + decoded, err := types.UnmarshalPacketData(bz, types.Version, tc.encoding) + require.NoError(t, err) + + // Compare + require.Equal(t, packetData.Sender, decoded.Sender) + require.Equal(t, packetData.Receiver, decoded.Receiver) + require.Equal(t, packetData.Salt, decoded.Salt) + require.Equal(t, packetData.Payload, decoded.Payload) + require.Equal(t, packetData.Memo, decoded.Memo) + }) + } +} + +func TestUnmarshalPacketData_InvalidEncoding(t *testing.T) { + _, err := types.UnmarshalPacketData([]byte("data"), types.Version, "invalid-encoding") + require.ErrorIs(t, err, types.ErrInvalidEncoding) +} + +func TestMsgSendCall_ValidateBasic(t *testing.T) { + validSender := "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du" + + testCases := []struct { + name string + msg *types.MsgSendCall + expErr error + }{ + { + "success: valid message", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + []byte("payload"), + []byte("salt"), + 1000000000, + types.EncodingABI, + "memo", + ), + nil, + }, + { + "success: empty encoding defaults to valid", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + []byte("payload"), + []byte("salt"), + 1000000000, + "", + "memo", + ), + nil, + }, + { + "success: empty receiver is allowed", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "", + []byte("payload"), + []byte("salt"), + 1000000000, + types.EncodingABI, + "", + ), + nil, + }, + { + "failure: invalid source client ID - too short", + types.NewMsgSendCall( + "abc", + validSender, + "receiver", + []byte("payload"), + []byte("salt"), + 1000000000, + types.EncodingABI, + "memo", + ), + host.ErrInvalidID, + }, + { + "failure: invalid sender address", + types.NewMsgSendCall( + "07-tendermint-0", + "not-a-bech32-address", + "receiver", + []byte("payload"), + []byte("salt"), + 1000000000, + types.EncodingABI, + "memo", + ), + ibcerrors.ErrInvalidAddress, + }, + { + "failure: zero timeout timestamp", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + []byte("payload"), + []byte("salt"), + 0, + types.EncodingABI, + "memo", + ), + types.ErrInvalidTimeoutTimestamp, + }, + { + "failure: invalid encoding", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + []byte("payload"), + []byte("salt"), + 1000000000, + "invalid-encoding", + "memo", + ), + types.ErrInvalidEncoding, + }, + { + "failure: receiver too long", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + strings.Repeat("a", types.MaximumReceiverLength+1), + []byte("payload"), + []byte("salt"), + 1000000000, + types.EncodingABI, + "memo", + ), + ibcerrors.ErrInvalidAddress, + }, + { + "failure: payload too long", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + make([]byte, types.MaximumPayloadLength+1), + []byte("salt"), + 1000000000, + types.EncodingABI, + "memo", + ), + types.ErrInvalidPayload, + }, + { + "failure: salt too long", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + []byte("payload"), + make([]byte, types.MaximumSaltLength+1), + 1000000000, + types.EncodingABI, + "memo", + ), + types.ErrInvalidSalt, + }, + { + "failure: memo too long", + types.NewMsgSendCall( + "07-tendermint-0", + validSender, + "receiver", + []byte("payload"), + []byte("salt"), + 1000000000, + types.EncodingABI, + strings.Repeat("m", types.MaximumMemoLength+1), + ), + types.ErrInvalidMemo, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.expErr == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expErr) + } + }) + } +} diff --git a/modules/apps/27-gmp/types/solidity_abi_test.go b/modules/apps/27-gmp/types/solidity_abi_test.go new file mode 100644 index 00000000000..82e36fd53c4 --- /dev/null +++ b/modules/apps/27-gmp/types/solidity_abi_test.go @@ -0,0 +1,205 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" +) + +func TestEncodeDecodeABIGMPPacketData(t *testing.T) { + testCases := []struct { + name string + packetData *types.GMPPacketData + }{ + { + "success: all fields populated", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("randomsalt"), + Payload: []byte("some payload data"), + Memo: "test memo", + }, + }, + { + "success: empty salt", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte{}, + Payload: []byte("payload"), + Memo: "memo", + }, + }, + { + "success: empty payload", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("salt"), + Payload: []byte{}, + Memo: "memo", + }, + }, + { + "success: empty memo", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("salt"), + Payload: []byte("payload"), + Memo: "", + }, + }, + { + "success: empty receiver", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "", + Salt: []byte("salt"), + Payload: []byte("payload"), + Memo: "memo", + }, + }, + { + "success: all optional fields empty", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "", + Salt: []byte{}, + Payload: []byte{}, + Memo: "", + }, + }, + { + "success: large payload", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("salt"), + Payload: make([]byte, 1024), // 1KB payload + Memo: "memo", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Encode + encoded, err := types.EncodeABIGMPPacketData(tc.packetData) + require.NoError(t, err, "encoding should succeed") + require.NotEmpty(t, encoded, "encoded data should not be empty") + + // Decode + decoded, err := types.DecodeABIGMPPacketData(encoded) + require.NoError(t, err, "decoding should succeed") + + // Compare + require.Equal(t, tc.packetData.Sender, decoded.Sender, "sender mismatch") + require.Equal(t, tc.packetData.Receiver, decoded.Receiver, "receiver mismatch") + require.Equal(t, tc.packetData.Salt, decoded.Salt, "salt mismatch") + require.Equal(t, tc.packetData.Payload, decoded.Payload, "payload mismatch") + require.Equal(t, tc.packetData.Memo, decoded.Memo, "memo mismatch") + }) + } +} + +func TestDecodeABIGMPPacketData_Invalid(t *testing.T) { + testCases := []struct { + name string + data []byte + }{ + { + "empty data", + []byte{}, + }, + { + "invalid abi data", + []byte("not valid abi encoded data"), + }, + { + "truncated data", + []byte{0x00, 0x01, 0x02, 0x03}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := types.DecodeABIGMPPacketData(tc.data) + require.Error(t, err, "decoding invalid data should fail") + require.ErrorIs(t, err, types.ErrAbiDecoding) + }) + } +} + +func TestEncodeDecodeABIAcknowledgement(t *testing.T) { + testCases := []struct { + name string + ack *types.Acknowledgement + }{ + { + "success: non-empty result", + &types.Acknowledgement{ + Result: []byte("success result data"), + }, + }, + { + "success: empty result", + &types.Acknowledgement{ + Result: []byte{}, + }, + }, + { + "success: large result", + &types.Acknowledgement{ + Result: make([]byte, 1024), // 1KB result + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Encode + encoded, err := types.EncodeABIAcknowledgement(tc.ack) + require.NoError(t, err, "encoding should succeed") + require.NotEmpty(t, encoded, "encoded data should not be empty") + + // Decode + decoded, err := types.DecodeABIAcknowledgement(encoded) + require.NoError(t, err, "decoding should succeed") + + // Compare + require.Equal(t, tc.ack.Result, decoded.Result, "result mismatch") + }) + } +} + +func TestDecodeABIAcknowledgement_Invalid(t *testing.T) { + testCases := []struct { + name string + data []byte + }{ + { + "empty data", + []byte{}, + }, + { + "invalid abi data", + []byte("not valid abi encoded data"), + }, + { + "truncated data", + []byte{0x00, 0x01, 0x02, 0x03}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := types.DecodeABIAcknowledgement(tc.data) + require.Error(t, err, "decoding invalid data should fail") + require.ErrorIs(t, err, types.ErrAbiDecoding) + }) + } +} diff --git a/simapp/app.go b/simapp/app.go index 46f1dcccd82..27699fb6685 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -100,6 +100,9 @@ import ( icahostkeeper "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/keeper" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/types" + gmp "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp" + gmpkeeper "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/keeper" + gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" packetforward "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware" packetforwardkeeper "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" packetforwardtypes "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" @@ -176,6 +179,7 @@ type SimApp struct { CircuitKeeper circuitkeeper.Keeper PFMKeeper *packetforwardkeeper.Keeper RateLimitKeeper *ratelimitkeeper.Keeper + GMPKeeper *gmpkeeper.Keeper // the module manager ModuleManager *module.Manager @@ -262,7 +266,7 @@ func NewSimApp( minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, govtypes.StoreKey, ibcexported.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey, evidencetypes.StoreKey, ibctransfertypes.StoreKey, icacontrollertypes.StoreKey, icahosttypes.StoreKey, - authzkeeper.StoreKey, consensusparamtypes.StoreKey, circuittypes.StoreKey, packetforwardtypes.StoreKey, ratelimittypes.StoreKey, + authzkeeper.StoreKey, consensusparamtypes.StoreKey, circuittypes.StoreKey, packetforwardtypes.StoreKey, ratelimittypes.StoreKey, gmptypes.StoreKey, ) // register streaming services @@ -366,6 +370,15 @@ func NewSimApp( govAuthority, ) + // GMP Keeper + app.GMPKeeper = gmpkeeper.NewKeeper( + appCodec, + runtime.NewKVStoreService(keys[gmptypes.StoreKey]), + app.AccountKeeper, + app.MsgServiceRouter(), + govAuthority, + ) + // Transfer Keeper app.TransferKeeper = ibctransferkeeper.NewKeeper( appCodec, @@ -427,6 +440,9 @@ func NewSimApp( // register the transfer v2 module. ibcRouterV2.AddRoute(ibctransfertypes.PortID, transferv2.NewIBCModule(app.TransferKeeper)) + // register the gmp module. + ibcRouterV2.AddRoute(gmptypes.PortID, gmp.NewIBCModule(app.GMPKeeper)) + // Set the IBC Routers app.IBCKeeper.SetRouter(ibcRouter) app.IBCKeeper.SetRouterV2(ibcRouterV2) @@ -478,6 +494,7 @@ func NewSimApp( ibc.NewAppModule(app.IBCKeeper), transfer.NewAppModule(app.TransferKeeper), ica.NewAppModule(app.ICAControllerKeeper, app.ICAHostKeeper), + gmp.NewAppModule(app.GMPKeeper), packetforward.NewAppModule(app.PFMKeeper), ratelimiting.NewAppModule(app.RateLimitKeeper), @@ -545,7 +562,7 @@ func NewSimApp( banktypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, ibcexported.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, authz.ModuleName, ibctransfertypes.ModuleName, - packetforwardtypes.ModuleName, icatypes.ModuleName, feegrant.ModuleName, upgradetypes.ModuleName, + packetforwardtypes.ModuleName, icatypes.ModuleName, gmptypes.ModuleName, feegrant.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, consensusparamtypes.ModuleName, circuittypes.ModuleName, ratelimittypes.ModuleName, } app.ModuleManager.SetOrderInitGenesis(genesisModuleOrder...) From 883879bf3606d296d14b1bdfd5fec102a59801b5 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 16:02:08 +0000 Subject: [PATCH 02/22] lint --- e2e/tests/gmp/base_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/tests/gmp/base_test.go b/e2e/tests/gmp/base_test.go index edd9b00df71..cb6e913ce54 100644 --- a/e2e/tests/gmp/base_test.go +++ b/e2e/tests/gmp/base_test.go @@ -113,7 +113,7 @@ func (s *GMPTestSuite) TestMsgSendCall_BankTransfer() { accountID := gmptypes.NewAccountIdentifier(clientIDB, senderWallet.FormattedAddress(), []byte(testSalt)) addr, err := gmptypes.BuildAddressPredictable(&accountID) s.Require().NoError(err) - gmpAccountAddr = sdk.AccAddress(addr).String() + gmpAccountAddr = addr.String() msgSend := &banktypes.MsgSend{ FromAddress: rlyWallet.FormattedAddress(), @@ -236,7 +236,7 @@ func (s *GMPTestSuite) registerCounterparty(ctx context.Context, chain ibc.Chain s.AssertTxSuccess(txResp) } -func (s *GMPTestSuite) hashPath(key []byte) []byte { +func (*GMPTestSuite) hashPath(key []byte) []byte { return crypto.Keccak256(key) } From 17c5a755de1f2515b1f2276f623d54cdd4dd15c7 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 16:12:04 +0000 Subject: [PATCH 03/22] add codec test --- modules/apps/27-gmp/types/codec_test.go | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 modules/apps/27-gmp/types/codec_test.go diff --git a/modules/apps/27-gmp/types/codec_test.go b/modules/apps/27-gmp/types/codec_test.go new file mode 100644 index 00000000000..2d2c9018657 --- /dev/null +++ b/modules/apps/27-gmp/types/codec_test.go @@ -0,0 +1,48 @@ +package types_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + + gmp "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp" + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" +) + +func TestCodecTypeRegistration(t *testing.T) { + testCases := []struct { + name string + typeURL string + expErr error + }{ + { + "success: MsgSendCall", + sdk.MsgTypeURL(&types.MsgSendCall{}), + nil, + }, + { + "type not registered on codec", + "ibc.invalid.MsgTypeURL", + errors.New("unable to resolve type URL"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + encodingCfg := moduletestutil.MakeTestEncodingConfig(gmp.AppModule{}) + msg, err := encodingCfg.Codec.InterfaceRegistry().Resolve(tc.typeURL) + + if tc.expErr == nil { + require.NotNil(t, msg) + require.NoError(t, err) + } else { + require.Nil(t, msg) + require.ErrorContains(t, err, tc.expErr.Error()) + } + }) + } +} From 269e346bac9459a88c8842318626c9c72657ad1c Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 17:14:38 +0000 Subject: [PATCH 04/22] add genesis test --- modules/apps/27-gmp/keeper/genesis_test.go | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 modules/apps/27-gmp/keeper/genesis_test.go diff --git a/modules/apps/27-gmp/keeper/genesis_test.go b/modules/apps/27-gmp/keeper/genesis_test.go new file mode 100644 index 00000000000..fa591cb15fd --- /dev/null +++ b/modules/apps/27-gmp/keeper/genesis_test.go @@ -0,0 +1,91 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +func (s *KeeperTestSuite) TestInitGenesis() { + var genesisState *types.GenesisState + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() {}, + nil, + }, + { + "success: empty genesis", + func() { + genesisState = types.DefaultGenesisState() + }, + nil, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + sender := s.chainB.SenderAccount.GetAddress().String() + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + + genesisState = &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: sdk.AccAddress(addr).String(), + AccountId: accountID, + }, + }, + } + + tc.malleate() + + err = s.chainA.GetSimApp().GMPKeeper.InitGenesis(s.chainA.GetContext(), genesisState) + + if tc.expErr == nil { + s.Require().NoError(err) + + if len(genesisState.Ics27Accounts) > 0 { + account := genesisState.Ics27Accounts[0] + storedAddr, err := s.chainA.GetSimApp().GMPKeeper.GetOrComputeICS27Address( + s.chainA.GetContext(), + &account.AccountId, + ) + s.Require().NoError(err) + s.Require().Equal(account.AccountAddress, storedAddr) + } + } else { + s.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (s *KeeperTestSuite) TestExportGenesis() { + s.SetupTest() + + sender := s.chainB.SenderAccount.GetAddress().String() + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + gmpAccountAddr := sdk.AccAddress(addr).String() + + s.createGMPAccount(gmpAccountAddr) + + genesisState, err := s.chainA.GetSimApp().GMPKeeper.ExportGenesis(s.chainA.GetContext()) + s.Require().NoError(err) + s.Require().Len(genesisState.Ics27Accounts, 1) + s.Require().Equal(gmpAccountAddr, genesisState.Ics27Accounts[0].AccountAddress) + s.Require().Equal(ibctesting.FirstClientID, genesisState.Ics27Accounts[0].AccountId.ClientId) + s.Require().Equal(sender, genesisState.Ics27Accounts[0].AccountId.Sender) + s.Require().Equal([]byte(testSalt), genesisState.Ics27Accounts[0].AccountId.Salt) +} From f72bf1c6a0dd4b27c43bed6509f80d42f9857b26 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 20:12:33 +0000 Subject: [PATCH 05/22] ignore e2e codecov --- codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yml b/codecov.yml index 90643b9dd06..ad2207fafcf 100644 --- a/codecov.yml +++ b/codecov.yml @@ -29,3 +29,4 @@ ignore: - "scripts" - "contrib" - "cmd" +- "e2e" From 459e29fdc2264bf9aadc4d157858197121eae3ca Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 21:34:03 +0000 Subject: [PATCH 06/22] add tests --- modules/apps/27-gmp/ibc_module_test.go | 120 ++++++++++++++++++++- modules/apps/27-gmp/keeper/genesis_test.go | 7 ++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/modules/apps/27-gmp/ibc_module_test.go b/modules/apps/27-gmp/ibc_module_test.go index 95a1452d676..6bc1b9246f1 100644 --- a/modules/apps/27-gmp/ibc_module_test.go +++ b/modules/apps/27-gmp/ibc_module_test.go @@ -3,9 +3,11 @@ package gmp_test import ( "testing" + "github.com/cosmos/gogoproto/proto" testifysuite "github.com/stretchr/testify/suite" sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" gmp "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp" "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" @@ -19,6 +21,7 @@ type IBCModuleTestSuite struct { coordinator *ibctesting.Coordinator chainA *ibctesting.TestChain + chainB *ibctesting.TestChain } const ( @@ -31,8 +34,9 @@ func TestIBCModuleTestSuite(t *testing.T) { } func (s *IBCModuleTestSuite) SetupTest() { - s.coordinator = ibctesting.NewCoordinator(s.T(), 1) + s.coordinator = ibctesting.NewCoordinator(s.T(), 2) s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) + s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) } func (s *IBCModuleTestSuite) TestOnSendPacket() { @@ -126,3 +130,117 @@ func (s *IBCModuleTestSuite) TestOnSendPacket() { }) } } + +func (s *IBCModuleTestSuite) TestOnRecvPacket() { + const testSalt = "test-salt" + + var ( + module *gmp.IBCModule + payload channeltypesv2.Payload + gmpAccountAddr sdk.AccAddress + ) + + testCases := []struct { + name string + malleate func() + expStatus channeltypesv2.PacketStatus + }{ + { + "success", + func() { + s.fundAccount(gmpAccountAddr, sdk.NewCoins(ibctesting.TestCoin)) + }, + channeltypesv2.PacketStatus_Success, + }, + { + "failure: invalid source port", + func() { + payload.SourcePort = "invalid-port" + }, + channeltypesv2.PacketStatus_Failure, + }, + { + "failure: invalid destination port", + func() { + payload.DestinationPort = "invalid-port" + }, + channeltypesv2.PacketStatus_Failure, + }, + { + "failure: invalid version", + func() { + payload.Version = "invalid-version" + }, + channeltypesv2.PacketStatus_Failure, + }, + { + "failure: invalid packet data", + func() { + payload.Value = []byte("invalid") + }, + channeltypesv2.PacketStatus_Failure, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + module = gmp.NewIBCModule(s.chainA.GetSimApp().GMPKeeper) + sender := s.chainB.SenderAccount.GetAddress().String() + recipient := s.chainA.SenderAccount.GetAddress() + + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + gmpAccountAddr = addr + + msgPayload := s.serializeMsgs(s.newMsgSend(gmpAccountAddr, recipient)) + packetData := types.NewGMPPacketData(sender, "", []byte(testSalt), msgPayload, "") + dataBz, err := types.MarshalPacketData(&packetData, types.Version, types.EncodingProtobuf) + s.Require().NoError(err) + + payload = channeltypesv2.NewPayload(types.PortID, types.PortID, types.Version, types.EncodingProtobuf, dataBz) + + tc.malleate() + + result := module.OnRecvPacket( + s.chainA.GetContext(), + validClientID, + validClientID, + 1, + payload, + s.chainA.SenderAccount.GetAddress(), + ) + + s.Require().Equal(tc.expStatus, result.Status) + if tc.expStatus == channeltypesv2.PacketStatus_Success { + s.Require().NotEmpty(result.Acknowledgement) + } + }) + } +} + +func (s *IBCModuleTestSuite) fundAccount(addr sdk.AccAddress, coins sdk.Coins) { + err := s.chainA.GetSimApp().BankKeeper.SendCoins( + s.chainA.GetContext(), + s.chainA.SenderAccount.GetAddress(), + addr, + coins, + ) + s.Require().NoError(err) +} + +func (s *IBCModuleTestSuite) newMsgSend(from, to sdk.AccAddress) *banktypes.MsgSend { + return &banktypes.MsgSend{ + FromAddress: from.String(), + ToAddress: to.String(), + Amount: sdk.NewCoins(ibctesting.TestCoin), + } +} + +func (s *IBCModuleTestSuite) serializeMsgs(msgs ...proto.Message) []byte { + payload, err := types.SerializeCosmosTx(s.chainA.GetSimApp().AppCodec(), msgs) + s.Require().NoError(err) + return payload +} diff --git a/modules/apps/27-gmp/keeper/genesis_test.go b/modules/apps/27-gmp/keeper/genesis_test.go index fa591cb15fd..a1010a56ec6 100644 --- a/modules/apps/27-gmp/keeper/genesis_test.go +++ b/modules/apps/27-gmp/keeper/genesis_test.go @@ -70,6 +70,13 @@ func (s *KeeperTestSuite) TestInitGenesis() { } } +func (s *KeeperTestSuite) TestGetAuthority() { + s.SetupTest() + + authority := s.chainA.GetSimApp().GMPKeeper.GetAuthority() + s.Require().NotEmpty(authority) +} + func (s *KeeperTestSuite) TestExportGenesis() { s.SetupTest() From 95343731ea820e33250d7530e2691061127c809a Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 21:36:55 +0000 Subject: [PATCH 07/22] add module tests --- modules/apps/27-gmp/module_test.go | 146 +++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 modules/apps/27-gmp/module_test.go diff --git a/modules/apps/27-gmp/module_test.go b/modules/apps/27-gmp/module_test.go new file mode 100644 index 00000000000..2568c73c5a7 --- /dev/null +++ b/modules/apps/27-gmp/module_test.go @@ -0,0 +1,146 @@ +package gmp_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + testifysuite "github.com/stretchr/testify/suite" + + gmp "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp" + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +type AppModuleTestSuite struct { + testifysuite.Suite + + coordinator *ibctesting.Coordinator + chainA *ibctesting.TestChain +} + +func TestAppModuleTestSuite(t *testing.T) { + testifysuite.Run(t, new(AppModuleTestSuite)) +} + +func (s *AppModuleTestSuite) SetupTest() { + s.coordinator = ibctesting.NewCoordinator(s.T(), 1) + s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func (s *AppModuleTestSuite) TestValidateGenesis() { + testCases := []struct { + name string + genState *types.GenesisState + expErr bool + }{ + { + "success: default genesis", + types.DefaultGenesisState(), + false, + }, + { + "success: valid genesis with account", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: s.chainA.SenderAccount.GetAddress().String(), + AccountId: types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: s.chainA.SenderAccount.GetAddress().String(), + Salt: []byte("salt"), + }, + }, + }, + }, + false, + }, + { + "failure: invalid account address", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: "invalid", + AccountId: types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: s.chainA.SenderAccount.GetAddress().String(), + Salt: []byte("salt"), + }, + }, + }, + }, + true, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + module := gmp.NewAppModule(s.chainA.GetSimApp().GMPKeeper) + cdc := s.chainA.GetSimApp().AppCodec() + bz := cdc.MustMarshalJSON(tc.genState) + + err := module.ValidateGenesis(cdc, nil, bz) + if tc.expErr { + s.Require().Error(err) + } else { + s.Require().NoError(err) + } + }) + } +} + +func (s *AppModuleTestSuite) TestValidateGenesisInvalidJSON() { + module := gmp.NewAppModule(s.chainA.GetSimApp().GMPKeeper) + cdc := s.chainA.GetSimApp().AppCodec() + + err := module.ValidateGenesis(cdc, nil, json.RawMessage("invalid")) + s.Require().Error(err) + s.Require().Contains(err.Error(), "failed to unmarshal") +} + +func (s *AppModuleTestSuite) TestAutoCLIOptions() { + module := gmp.NewAppModule(s.chainA.GetSimApp().GMPKeeper) + opts := module.AutoCLIOptions() + + s.Require().NotNil(opts) + s.Require().NotNil(opts.Query) + s.Require().NotNil(opts.Tx) +} + +func (s *AppModuleTestSuite) TestExportGenesis() { + module := gmp.NewAppModule(s.chainA.GetSimApp().GMPKeeper) + cdc := s.chainA.GetSimApp().AppCodec() + + bz := module.ExportGenesis(s.chainA.GetContext(), cdc) + s.Require().NotEmpty(bz) + + var gs types.GenesisState + err := cdc.UnmarshalJSON(bz, &gs) + s.Require().NoError(err) +} + +func TestAppModuleName(t *testing.T) { + module := gmp.AppModule{} + require.Equal(t, types.ModuleName, module.Name()) +} + +func TestAppModuleConsensusVersion(t *testing.T) { + module := gmp.AppModule{} + require.Equal(t, uint64(1), module.ConsensusVersion()) +} + +func TestAppModuleDefaultGenesis(t *testing.T) { + module := gmp.AppModule{} + + coordinator := ibctesting.NewCoordinator(t, 1) + chain := coordinator.GetChain(ibctesting.GetChainID(1)) + cdc := chain.GetSimApp().AppCodec() + + bz := module.DefaultGenesis(cdc) + require.NotEmpty(t, bz) + + var gs types.GenesisState + err := cdc.UnmarshalJSON(bz, &gs) + require.NoError(t, err) + require.Empty(t, gs.Ics27Accounts) +} From 4b838bcf438dc52d0a0e9e0af5d3ad91c06419c3 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 21:44:25 +0000 Subject: [PATCH 08/22] +test --- modules/apps/27-gmp/module_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/apps/27-gmp/module_test.go b/modules/apps/27-gmp/module_test.go index 2568c73c5a7..d3808690e76 100644 --- a/modules/apps/27-gmp/module_test.go +++ b/modules/apps/27-gmp/module_test.go @@ -124,6 +124,17 @@ func TestAppModuleName(t *testing.T) { require.Equal(t, types.ModuleName, module.Name()) } +func TestNewAppModuleBasic(t *testing.T) { + coordinator := ibctesting.NewCoordinator(t, 1) + chain := coordinator.GetChain(ibctesting.GetChainID(1)) + + module := gmp.NewAppModule(chain.GetSimApp().GMPKeeper) + basic := gmp.NewAppModuleBasic(module) + + require.NotNil(t, basic) + require.Equal(t, types.ModuleName, basic.Name()) +} + func TestAppModuleConsensusVersion(t *testing.T) { module := gmp.AppModule{} require.Equal(t, uint64(1), module.ConsensusVersion()) From a51252cfe2606f504dd3b7337323024788814a28 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 21:46:30 +0000 Subject: [PATCH 09/22] +test --- modules/apps/27-gmp/ibc_module_test.go | 29 +++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/apps/27-gmp/ibc_module_test.go b/modules/apps/27-gmp/ibc_module_test.go index 6bc1b9246f1..d35697ff487 100644 --- a/modules/apps/27-gmp/ibc_module_test.go +++ b/modules/apps/27-gmp/ibc_module_test.go @@ -138,6 +138,8 @@ func (s *IBCModuleTestSuite) TestOnRecvPacket() { module *gmp.IBCModule payload channeltypesv2.Payload gmpAccountAddr sdk.AccAddress + sender string + msgPayload []byte ) testCases := []struct { @@ -174,12 +176,33 @@ func (s *IBCModuleTestSuite) TestOnRecvPacket() { channeltypesv2.PacketStatus_Failure, }, { - "failure: invalid packet data", + "failure: invalid packet data - unmarshal error", func() { payload.Value = []byte("invalid") }, channeltypesv2.PacketStatus_Failure, }, + { + "failure: ValidateBasic error - empty sender", + func() { + packetData := types.NewGMPPacketData("", "", []byte(testSalt), msgPayload, "") + dataBz, err := types.MarshalPacketData(&packetData, types.Version, types.EncodingProtobuf) + s.Require().NoError(err) + payload.Value = dataBz + }, + channeltypesv2.PacketStatus_Failure, + }, + { + "failure: keeper OnRecvPacket error - unauthorized signer", + func() { + unauthorizedPayload := s.serializeMsgs(s.newMsgSend(s.chainA.SenderAccount.GetAddress(), s.chainB.SenderAccount.GetAddress())) + packetData := types.NewGMPPacketData(sender, "", []byte(testSalt), unauthorizedPayload, "") + dataBz, err := types.MarshalPacketData(&packetData, types.Version, types.EncodingProtobuf) + s.Require().NoError(err) + payload.Value = dataBz + }, + channeltypesv2.PacketStatus_Failure, + }, } for _, tc := range testCases { @@ -187,7 +210,7 @@ func (s *IBCModuleTestSuite) TestOnRecvPacket() { s.SetupTest() module = gmp.NewIBCModule(s.chainA.GetSimApp().GMPKeeper) - sender := s.chainB.SenderAccount.GetAddress().String() + sender = s.chainB.SenderAccount.GetAddress().String() recipient := s.chainA.SenderAccount.GetAddress() accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) @@ -195,7 +218,7 @@ func (s *IBCModuleTestSuite) TestOnRecvPacket() { s.Require().NoError(err) gmpAccountAddr = addr - msgPayload := s.serializeMsgs(s.newMsgSend(gmpAccountAddr, recipient)) + msgPayload = s.serializeMsgs(s.newMsgSend(gmpAccountAddr, recipient)) packetData := types.NewGMPPacketData(sender, "", []byte(testSalt), msgPayload, "") dataBz, err := types.MarshalPacketData(&packetData, types.Version, types.EncodingProtobuf) s.Require().NoError(err) From daeb4941e4eea11bafb1f6d59ed35a148ab534f3 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 21:51:02 +0000 Subject: [PATCH 10/22] +tests --- modules/apps/27-gmp/ibc_module_test.go | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/modules/apps/27-gmp/ibc_module_test.go b/modules/apps/27-gmp/ibc_module_test.go index d35697ff487..45efa11ccb1 100644 --- a/modules/apps/27-gmp/ibc_module_test.go +++ b/modules/apps/27-gmp/ibc_module_test.go @@ -93,6 +93,23 @@ func (s *IBCModuleTestSuite) TestOnSendPacket() { }, ibcerrors.ErrUnauthorized, }, + { + "failure: unmarshal packet data error", + func() { + payload.Value = []byte("invalid") + }, + ibcerrors.ErrInvalidType, + }, + { + "failure: ValidateBasic error - empty sender", + func() { + packetData := types.NewGMPPacketData("", "", []byte("salt"), []byte("payload"), "") + dataBz, err := types.MarshalPacketData(&packetData, types.Version, types.EncodingProtobuf) + s.Require().NoError(err) + payload.Value = dataBz + }, + ibcerrors.ErrInvalidAddress, + }, } for _, tc := range testCases { @@ -244,6 +261,43 @@ func (s *IBCModuleTestSuite) TestOnRecvPacket() { } } +func (s *IBCModuleTestSuite) TestOnTimeoutPacket() { + s.SetupTest() + + module := gmp.NewIBCModule(s.chainA.GetSimApp().GMPKeeper) + payload := channeltypesv2.Payload{} + + err := module.OnTimeoutPacket( + s.chainA.GetContext(), + validClientID, + validClientID, + 1, + payload, + s.chainA.SenderAccount.GetAddress(), + ) + + s.Require().NoError(err) +} + +func (s *IBCModuleTestSuite) TestOnAcknowledgementPacket() { + s.SetupTest() + + module := gmp.NewIBCModule(s.chainA.GetSimApp().GMPKeeper) + payload := channeltypesv2.Payload{} + + err := module.OnAcknowledgementPacket( + s.chainA.GetContext(), + validClientID, + validClientID, + 1, + []byte("ack"), + payload, + s.chainA.SenderAccount.GetAddress(), + ) + + s.Require().NoError(err) +} + func (s *IBCModuleTestSuite) fundAccount(addr sdk.AccAddress, coins sdk.Coins) { err := s.chainA.GetSimApp().BankKeeper.SendCoins( s.chainA.GetContext(), From cf187c83db96a6490a83e1dae72cadc72f086a9c Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 22:02:33 +0000 Subject: [PATCH 11/22] +tests --- .../apps/27-gmp/keeper/query_server_test.go | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/modules/apps/27-gmp/keeper/query_server_test.go b/modules/apps/27-gmp/keeper/query_server_test.go index 54acc0e4cd7..a6e94f54dea 100644 --- a/modules/apps/27-gmp/keeper/query_server_test.go +++ b/modules/apps/27-gmp/keeper/query_server_test.go @@ -132,6 +132,84 @@ func (s *KeeperTestSuite) TestQueryAccountIdentifier() { } } +func (s *KeeperTestSuite) TestGetOrComputeICS27Address() { + testCases := []struct { + name string + accountID *types.AccountIdentifier + expErr error + }{ + { + "success: existing account", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "", // will be set in test + Salt: []byte(testSalt), + }, + nil, + }, + { + "success: computes new address", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "", // will be set in test + Salt: []byte("new-salt"), + }, + nil, + }, + { + "failure: invalid client ID", + &types.AccountIdentifier{ + ClientId: "invalid", + Sender: "", // will be set in test + Salt: []byte(testSalt), + }, + ibcerrors.ErrInvalidAddress, + }, + { + "failure: empty sender", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "", + Salt: []byte(testSalt), + }, + ibcerrors.ErrInvalidAddress, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + sender := s.chainB.SenderAccount.GetAddress().String() + + // Set sender if not testing empty sender case + if tc.accountID.Sender == "" && tc.expErr != ibcerrors.ErrInvalidAddress { + tc.accountID.Sender = sender + } + + // Create existing account for first test case + if tc.name == "success: existing account" { + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + s.createGMPAccount(addr.String()) + } + + addr, err := s.chainA.GetSimApp().GMPKeeper.GetOrComputeICS27Address( + s.chainA.GetContext(), + tc.accountID, + ) + + if tc.expErr == nil { + s.Require().NoError(err) + s.Require().NotEmpty(addr) + } else { + s.Require().Error(err) + } + }) + } +} + func (s *KeeperTestSuite) createGMPAccount(gmpAccountAddr string) { sender := s.chainB.SenderAccount.GetAddress().String() recipient := s.chainA.SenderAccount.GetAddress() From 513861d00aac09c3700942e5a04948ca32697c70 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 22:06:02 +0000 Subject: [PATCH 12/22] +tests --- modules/apps/27-gmp/keeper/msg_server_test.go | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/modules/apps/27-gmp/keeper/msg_server_test.go b/modules/apps/27-gmp/keeper/msg_server_test.go index 836c75b5bc0..30d10d920cb 100644 --- a/modules/apps/27-gmp/keeper/msg_server_test.go +++ b/modules/apps/27-gmp/keeper/msg_server_test.go @@ -84,3 +84,80 @@ func (s *KeeperTestSuite) TestSendCall() { }) } } + +func (s *KeeperTestSuite) TestSendCallErrors() { + var msg *types.MsgSendCall + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "failure: invalid sender address", + func() { + msg.Sender = "invalid" + }, + nil, // bech32 error not wrapped + }, + { + "failure: empty sender address", + func() { + msg.Sender = "" + }, + nil, // bech32 error not wrapped + }, + { + "failure: invalid encoding", + func() { + msg.Encoding = "invalid-encoding" + }, + types.ErrInvalidEncoding, + }, + { + "failure: invalid source client - counterparty not found", + func() { + msg.SourceClient = "invalid" + }, + nil, // counterparty not found error + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + path := ibctesting.NewPath(s.chainA, s.chainB) + path.SetupV2() + + sender := s.chainA.SenderAccount.GetAddress() + recipient := s.chainB.SenderAccount.GetAddress() + payload := s.serializeMsgs(&banktypes.MsgSend{ + FromAddress: sender.String(), + ToAddress: recipient.String(), + Amount: sdk.NewCoins(ibctesting.TestCoin), + }) + + msg = types.NewMsgSendCall( + path.EndpointA.ClientID, + sender.String(), + "", + payload, + []byte(testSalt), + uint64(s.chainA.GetContext().BlockTime().Add(time.Hour).Unix()), + types.EncodingProtobuf, + "", + ) + + tc.malleate() + + resp, err := s.chainA.GetSimApp().GMPKeeper.SendCall(s.chainA.GetContext(), msg) + + s.Require().Error(err) + s.Require().Nil(resp) + if tc.expErr != nil { + s.Require().ErrorIs(err, tc.expErr) + } + }) + } +} From fc74e9f7d0b418e55b7f6d0ab483dd728503d104 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 22:44:27 +0000 Subject: [PATCH 13/22] +tests and bugfix --- modules/apps/27-gmp/keeper/genesis_test.go | 15 ++ .../apps/27-gmp/keeper/query_server_test.go | 45 ++++++ modules/apps/27-gmp/keeper/relay_test.go | 25 ++- modules/apps/27-gmp/module_test.go | 48 ++++++ modules/apps/27-gmp/types/account_test.go | 95 ++++++++++++ modules/apps/27-gmp/types/ack.go | 4 +- modules/apps/27-gmp/types/ack_test.go | 146 ++++++++++++++++++ 7 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 modules/apps/27-gmp/types/ack_test.go diff --git a/modules/apps/27-gmp/keeper/genesis_test.go b/modules/apps/27-gmp/keeper/genesis_test.go index a1010a56ec6..9c8a8f655c7 100644 --- a/modules/apps/27-gmp/keeper/genesis_test.go +++ b/modules/apps/27-gmp/keeper/genesis_test.go @@ -4,6 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" ibctesting "github.com/cosmos/ibc-go/v10/testing" ) @@ -27,6 +28,20 @@ func (s *KeeperTestSuite) TestInitGenesis() { }, nil, }, + { + "failure: invalid account address", + func() { + genesisState.Ics27Accounts[0].AccountAddress = "invalid" + }, + ibcerrors.ErrInvalidAddress, + }, + { + "failure: invalid sender address", + func() { + genesisState.Ics27Accounts[0].AccountId.Sender = "invalid" + }, + ibcerrors.ErrInvalidAddress, + }, } for _, tc := range testCases { diff --git a/modules/apps/27-gmp/keeper/query_server_test.go b/modules/apps/27-gmp/keeper/query_server_test.go index a6e94f54dea..bd0914b7527 100644 --- a/modules/apps/27-gmp/keeper/query_server_test.go +++ b/modules/apps/27-gmp/keeper/query_server_test.go @@ -132,6 +132,51 @@ func (s *KeeperTestSuite) TestQueryAccountIdentifier() { } } +func (s *KeeperTestSuite) TestGetAccount() { + testCases := []struct { + name string + malleate func(addr sdk.AccAddress) + expErr error + }{ + { + "success", + func(addr sdk.AccAddress) { + s.createGMPAccount(addr.String()) + }, + nil, + }, + { + "failure: account not found", + func(addr sdk.AccAddress) {}, + collections.ErrNotFound, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + + sender := s.chainB.SenderAccount.GetAddress().String() + accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) + addr, err := types.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + + tc.malleate(addr) + + account, err := s.chainA.GetSimApp().GMPKeeper.GetAccount(s.chainA.GetContext(), addr) + + if tc.expErr == nil { + s.Require().NoError(err) + s.Require().NotNil(account) + s.Require().Equal(addr.String(), account.Address) + } else { + s.Require().ErrorIs(err, tc.expErr) + s.Require().Nil(account) + } + }) + } +} + func (s *KeeperTestSuite) TestGetOrComputeICS27Address() { testCases := []struct { name string diff --git a/modules/apps/27-gmp/keeper/relay_test.go b/modules/apps/27-gmp/keeper/relay_test.go index 308579b7c90..4b49848efa7 100644 --- a/modules/apps/27-gmp/keeper/relay_test.go +++ b/modules/apps/27-gmp/keeper/relay_test.go @@ -9,10 +9,12 @@ import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/keeper" "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + 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" ) @@ -119,6 +121,7 @@ func (s *KeeperTestSuite) TestOnRecvPacket() { gmpAccountAddr sdk.AccAddress sender string recipient sdk.AccAddress + destClient string ) testCases := []struct { @@ -168,6 +171,25 @@ func (s *KeeperTestSuite) TestOnRecvPacket() { }, types.ErrInvalidPayload, }, + { + "failure: msg ValidateBasic error - invalid to address", + func() { + invalidMsg := &banktypes.MsgSend{ + FromAddress: gmpAccountAddr.String(), + ToAddress: "invalid", + Amount: sdk.NewCoins(ibctesting.TestCoin), + } + packetData.Payload = s.serializeMsgs(invalidMsg) + }, + sdkerrors.ErrInvalidAddress, + }, + { + "failure: getOrCreateICS27Account error - invalid dest client", + func() { + destClient = "x" // too short, fails ClientIdentifierValidator + }, + host.ErrInvalidID, + }, } for _, tc := range testCases { @@ -177,6 +199,7 @@ func (s *KeeperTestSuite) TestOnRecvPacket() { gmpKeeper = s.chainA.GetSimApp().GMPKeeper sender = s.chainB.SenderAccount.GetAddress().String() recipient = s.chainA.SenderAccount.GetAddress() + destClient = ibctesting.FirstClientID accountID := types.NewAccountIdentifier(ibctesting.FirstClientID, sender, []byte(testSalt)) addr, err := types.BuildAddressPredictable(&accountID) @@ -192,7 +215,7 @@ func (s *KeeperTestSuite) TestOnRecvPacket() { s.chainA.GetContext(), packetData, types.PortID, ibctesting.FirstClientID, - types.PortID, ibctesting.FirstClientID, + types.PortID, destClient, ) expPass := tc.expErr == nil diff --git a/modules/apps/27-gmp/module_test.go b/modules/apps/27-gmp/module_test.go index d3808690e76..49919c20363 100644 --- a/modules/apps/27-gmp/module_test.go +++ b/modules/apps/27-gmp/module_test.go @@ -71,6 +71,54 @@ func (s *AppModuleTestSuite) TestValidateGenesis() { }, true, }, + { + "failure: invalid sender address", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: s.chainA.SenderAccount.GetAddress().String(), + AccountId: types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "invalid", + Salt: []byte("salt"), + }, + }, + }, + }, + true, + }, + { + "failure: invalid client ID", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: s.chainA.SenderAccount.GetAddress().String(), + AccountId: types.AccountIdentifier{ + ClientId: "x", + Sender: s.chainA.SenderAccount.GetAddress().String(), + Salt: []byte("salt"), + }, + }, + }, + }, + true, + }, + { + "failure: salt exceeds max length", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: s.chainA.SenderAccount.GetAddress().String(), + AccountId: types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: s.chainA.SenderAccount.GetAddress().String(), + Salt: make([]byte, types.MaximumSaltLength+1), + }, + }, + }, + }, + true, + }, } for _, tc := range testCases { diff --git a/modules/apps/27-gmp/types/account_test.go b/modules/apps/27-gmp/types/account_test.go index e0334b2a175..e706620f618 100644 --- a/modules/apps/27-gmp/types/account_test.go +++ b/modules/apps/27-gmp/types/account_test.go @@ -3,8 +3,18 @@ package types_test import ( "testing" + "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/require" + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + "github.com/cosmos/cosmos-sdk/x/bank" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + + gmp "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp" "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" host "github.com/cosmos/ibc-go/v10/modules/core/24-host" ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" @@ -185,3 +195,88 @@ func TestNewICS27Account(t *testing.T) { require.Equal(t, addr, account.Address) require.Equal(t, accountID, account.AccountId) } + +func TestSerializeCosmosTx(t *testing.T) { + encodingCfg := moduletestutil.MakeTestEncodingConfig(gmp.AppModule{}, bank.AppModule{}) + cdc := encodingCfg.Codec + + msg := &banktypes.MsgSend{ + FromAddress: "cosmos1sender", + ToAddress: "cosmos1recipient", + Amount: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(100))), + } + + testCases := []struct { + name string + msgs []proto.Message + expErr bool + }{ + { + "success: single message", + []proto.Message{msg}, + false, + }, + { + "success: multiple messages", + []proto.Message{msg, msg}, + false, + }, + { + "success: empty messages", + []proto.Message{}, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bz, err := types.SerializeCosmosTx(cdc, tc.msgs) + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotNil(t, bz) + } + }) + } +} + +func TestDeserializeCosmosTx(t *testing.T) { + encodingCfg := moduletestutil.MakeTestEncodingConfig(gmp.AppModule{}, bank.AppModule{}) + cdc := encodingCfg.Codec + + msg := &banktypes.MsgSend{ + FromAddress: "cosmos1sender", + ToAddress: "cosmos1recipient", + Amount: sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(100))), + } + + validPayload, err := types.SerializeCosmosTx(cdc, []proto.Message{msg}) + require.NoError(t, err) + + t.Run("success: valid payload", func(t *testing.T) { + msgs, err := types.DeserializeCosmosTx(cdc, validPayload) + require.NoError(t, err) + require.NotNil(t, msgs) + require.Len(t, msgs, 1) + }) + + t.Run("failure: invalid data", func(t *testing.T) { + msgs, err := types.DeserializeCosmosTx(cdc, []byte("invalid data")) + require.ErrorIs(t, err, ibcerrors.ErrInvalidType) + require.Nil(t, msgs) + }) +} + +// mockCodec is a mock codec that implements codec.Codec but is not a ProtoCodec +type mockCodec struct { + codec.Codec +} + +func TestDeserializeCosmosTx_InvalidCodec(t *testing.T) { + // Test that the function rejects non-ProtoCodec codecs + mock := &mockCodec{} + msgs, err := types.DeserializeCosmosTx(mock, []byte("data")) + require.ErrorIs(t, err, types.ErrInvalidCodec) + require.Nil(t, msgs) +} diff --git a/modules/apps/27-gmp/types/ack.go b/modules/apps/27-gmp/types/ack.go index 6f67c6da196..b0de0bcd731 100644 --- a/modules/apps/27-gmp/types/ack.go +++ b/modules/apps/27-gmp/types/ack.go @@ -46,10 +46,10 @@ func UnmarshalAcknowledgement(bz []byte, ics27Version string, encoding string) ( panic("unsupported ics27 version") } - var data *Acknowledgement + data := &Acknowledgement{} switch encoding { case EncodingJSON: - if err := json.Unmarshal(bz, &data); err != nil { + if err := json.Unmarshal(bz, data); err != nil { return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "failed to unmarshal json packet data: %s", err) } case EncodingProtobuf: diff --git a/modules/apps/27-gmp/types/ack_test.go b/modules/apps/27-gmp/types/ack_test.go new file mode 100644 index 00000000000..979da6f1d64 --- /dev/null +++ b/modules/apps/27-gmp/types/ack_test.go @@ -0,0 +1,146 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibcerrors "github.com/cosmos/ibc-go/v10/modules/core/errors" +) + +func TestNewAcknowledgement(t *testing.T) { + result := []byte("test result") + ack := types.NewAcknowledgement(result) + + require.Equal(t, result, ack.Result) +} + +func TestAcknowledgement_ValidateBasic(t *testing.T) { + ack := types.NewAcknowledgement([]byte("test result")) + err := ack.ValidateBasic() + require.NoError(t, err) +} + +func TestMarshalUnmarshalAcknowledgement(t *testing.T) { + ack := &types.Acknowledgement{ + Result: []byte("test result"), + } + + testCases := []struct { + name string + encoding string + expErr error + }{ + { + "success: JSON encoding", + types.EncodingJSON, + nil, + }, + { + "success: Protobuf encoding", + types.EncodingProtobuf, + nil, + }, + { + "success: ABI encoding", + types.EncodingABI, + nil, + }, + { + "failure: invalid encoding", + "invalid-encoding", + types.ErrInvalidEncoding, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + bz, err := types.MarshalAcknowledgement(ack, types.Version, tc.encoding) + if tc.expErr != nil { + require.ErrorIs(t, err, tc.expErr) + return + } + require.NoError(t, err) + require.NotEmpty(t, bz) + + // Unmarshal + decoded, err := types.UnmarshalAcknowledgement(bz, types.Version, tc.encoding) + require.NoError(t, err) + + // Compare + require.Equal(t, ack.Result, decoded.Result) + }) + } +} + +func TestUnmarshalAcknowledgement_InvalidEncoding(t *testing.T) { + _, err := types.UnmarshalAcknowledgement([]byte("data"), types.Version, "invalid-encoding") + require.ErrorIs(t, err, types.ErrInvalidEncoding) +} + +func TestUnmarshalAcknowledgement_InvalidData(t *testing.T) { + testCases := []struct { + name string + data []byte + encoding string + expErr error + }{ + { + "failure: invalid JSON data", + []byte("not valid json"), + types.EncodingJSON, + ibcerrors.ErrInvalidType, + }, + { + "failure: invalid Protobuf data", + []byte("not valid protobuf"), + types.EncodingProtobuf, + ibcerrors.ErrInvalidType, + }, + { + "failure: invalid ABI data", + []byte("not valid abi"), + types.EncodingABI, + ibcerrors.ErrInvalidType, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := types.UnmarshalAcknowledgement(tc.data, types.Version, tc.encoding) + require.ErrorIs(t, err, tc.expErr) + }) + } +} + +func TestMarshalAcknowledgement_EmptyResult(t *testing.T) { + ack := &types.Acknowledgement{ + Result: []byte{}, + } + + testCases := []struct { + name string + encoding string + expectEmptyData bool // Protobuf produces empty data for empty struct + }{ + {"JSON encoding", types.EncodingJSON, false}, + {"Protobuf encoding", types.EncodingProtobuf, true}, + {"ABI encoding", types.EncodingABI, false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bz, err := types.MarshalAcknowledgement(ack, types.Version, tc.encoding) + require.NoError(t, err) + if !tc.expectEmptyData { + require.NotEmpty(t, bz) + } + + decoded, err := types.UnmarshalAcknowledgement(bz, types.Version, tc.encoding) + require.NoError(t, err) + require.Empty(t, decoded.Result) + }) + } +} From 18244c63e36553cadc645e1799d1901b49150da9 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Wed, 10 Dec 2025 23:00:39 +0000 Subject: [PATCH 14/22] more tests --- modules/apps/27-gmp/types/packet_test.go | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/modules/apps/27-gmp/types/packet_test.go b/modules/apps/27-gmp/types/packet_test.go index 5abb5fec54b..9f8df710b16 100644 --- a/modules/apps/27-gmp/types/packet_test.go +++ b/modules/apps/27-gmp/types/packet_test.go @@ -153,6 +153,41 @@ func TestUnmarshalPacketData_InvalidEncoding(t *testing.T) { require.ErrorIs(t, err, types.ErrInvalidEncoding) } +func TestUnmarshalPacketData_InvalidData(t *testing.T) { + testCases := []struct { + name string + data []byte + encoding string + expErr error + }{ + { + "failure: invalid JSON data", + []byte("not valid json"), + types.EncodingJSON, + ibcerrors.ErrInvalidType, + }, + { + "failure: invalid Protobuf data", + []byte("not valid protobuf"), + types.EncodingProtobuf, + ibcerrors.ErrInvalidType, + }, + { + "failure: invalid ABI data", + []byte("not valid abi"), + types.EncodingABI, + ibcerrors.ErrInvalidType, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := types.UnmarshalPacketData(tc.data, types.Version, tc.encoding) + require.ErrorIs(t, err, tc.expErr) + }) + } +} + func TestMsgSendCall_ValidateBasic(t *testing.T) { validSender := "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du" From 0eace1a5fcf3eb673d922b80140b3731d7acd154 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 00:38:53 +0000 Subject: [PATCH 15/22] add query e2e tests --- e2e/tests/gmp/base_test.go | 114 +++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/e2e/tests/gmp/base_test.go b/e2e/tests/gmp/base_test.go index cb6e913ce54..93fb60205a8 100644 --- a/e2e/tests/gmp/base_test.go +++ b/e2e/tests/gmp/base_test.go @@ -261,3 +261,117 @@ func (s *GMPTestSuite) createAttestationProof(path, commitment []byte) []byte { s.Require().NoError(err) return proofBz } + +func (s *GMPTestSuite) TestQueryAccountAddress() { + ctx := context.TODO() + chain := s.GetAllChains()[0] + + senderWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chain), "failed to wait for blocks") + + req := &gmptypes.QueryAccountAddressRequest{ + ClientId: ibctesting.FirstClientID, + Sender: senderWallet.FormattedAddress(), + Salt: "", + } + + resp, err := query.GRPCQuery[gmptypes.QueryAccountAddressResponse](ctx, chain, req) + s.Require().NoError(err) + s.Require().NotEmpty(resp.AccountAddress) + + accountID := gmptypes.NewAccountIdentifier(ibctesting.FirstClientID, senderWallet.FormattedAddress(), []byte{}) + expectedAddr, err := gmptypes.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + s.Require().Equal(expectedAddr.String(), resp.AccountAddress) +} + +func (s *GMPTestSuite) TestQueryAccountIdentifier() { + t := s.T() + ctx := context.TODO() + chain := s.GetAllChains()[0] + + rlyWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + senderWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + recipientWallet := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + + var ( + clientIDA, clientIDB string + gmpAccountAddr string + ) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chain), "failed to wait for blocks") + + proofTimestamp := uint64(time.Now().UnixNano()) + + t.Run("setup clients and counterparties", func(t *testing.T) { + clientIDA = s.createAttestationsClient(ctx, chain, rlyWallet, proofTimestamp) + clientIDB = s.createAttestationsClient(ctx, chain, rlyWallet, proofTimestamp) + s.registerCounterparty(ctx, chain, rlyWallet, clientIDA, clientIDB) + s.registerCounterparty(ctx, chain, rlyWallet, clientIDB, clientIDA) + }) + + t.Run("create GMP account via packet", func(t *testing.T) { + accountID := gmptypes.NewAccountIdentifier(clientIDB, senderWallet.FormattedAddress(), []byte(testSalt)) + addr, err := gmptypes.BuildAddressPredictable(&accountID) + s.Require().NoError(err) + gmpAccountAddr = addr.String() + + msgSend := &banktypes.MsgSend{ + FromAddress: rlyWallet.FormattedAddress(), + ToAddress: gmpAccountAddr, + Amount: sdk.NewCoins(sdk.NewCoin(chain.Config().Denom, sdkmath.NewInt(testvalues.StartingTokenAmount))), + } + txResp := s.BroadcastMessages(ctx, chain, rlyWallet, msgSend) + s.AssertTxSuccess(txResp) + + payload, err := gmptypes.SerializeCosmosTx(testsuite.Codec(), []proto.Message{ + &banktypes.MsgSend{ + FromAddress: gmpAccountAddr, + ToAddress: recipientWallet.FormattedAddress(), + Amount: sdk.NewCoins(sdk.NewCoin(chain.Config().Denom, sdkmath.NewInt(testvalues.IBCTransferAmount))), + }, + }) + s.Require().NoError(err) + + msgSendCall := gmptypes.NewMsgSendCall( + clientIDA, + senderWallet.FormattedAddress(), + "", + payload, + []byte(testSalt), + uint64(time.Now().Add(10*time.Minute).Unix()), + gmptypes.EncodingProtobuf, + "", + ) + + txResp = s.BroadcastMessages(ctx, chain, senderWallet, msgSendCall) + s.AssertTxSuccess(txResp) + + packet, err := ibctesting.ParseV2PacketFromEvents(txResp.Events) + s.Require().NoError(err) + + commitment := channeltypesv2.CommitPacket(packet) + path := s.hashPath(hostv2.PacketCommitmentKey(packet.SourceClient, packet.Sequence)) + proof := s.createAttestationProof(path, commitment) + + msgRecvPacket := channeltypesv2.NewMsgRecvPacket( + packet, proof, clienttypes.NewHeight(0, proofHeight), rlyWallet.FormattedAddress(), + ) + + txResp = s.BroadcastMessages(ctx, chain, rlyWallet, msgRecvPacket) + s.AssertTxSuccess(txResp) + }) + + t.Run("query account identifier", func(t *testing.T) { + req := &gmptypes.QueryAccountIdentifierRequest{ + AccountAddress: gmpAccountAddr, + } + + resp, err := query.GRPCQuery[gmptypes.QueryAccountIdentifierResponse](ctx, chain, req) + s.Require().NoError(err) + s.Require().Equal(clientIDB, resp.AccountId.ClientId) + s.Require().Equal(senderWallet.FormattedAddress(), resp.AccountId.Sender) + s.Require().Equal([]byte(testSalt), resp.AccountId.Salt) + }) +} From 2063590867ae22ab82efc023c3c7bdcb6b0d89fc Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 01:53:31 +0000 Subject: [PATCH 16/22] + test --- modules/apps/27-gmp/keeper/msg_server_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/apps/27-gmp/keeper/msg_server_test.go b/modules/apps/27-gmp/keeper/msg_server_test.go index 30d10d920cb..98631dbbf53 100644 --- a/modules/apps/27-gmp/keeper/msg_server_test.go +++ b/modules/apps/27-gmp/keeper/msg_server_test.go @@ -121,6 +121,13 @@ func (s *KeeperTestSuite) TestSendCallErrors() { }, nil, // counterparty not found error }, + { + "failure: payload too long", + func() { + msg.Payload = make([]byte, types.MaximumPayloadLength+1) + }, + types.ErrInvalidPayload, + }, } for _, tc := range testCases { From 87dbccf76777895afe533eb494911c71ccfc6e78 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 11:36:16 +0000 Subject: [PATCH 17/22] +genesis tests --- modules/apps/27-gmp/types/genesis_test.go | 124 ++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 modules/apps/27-gmp/types/genesis_test.go diff --git a/modules/apps/27-gmp/types/genesis_test.go b/modules/apps/27-gmp/types/genesis_test.go new file mode 100644 index 00000000000..e84ba10e07f --- /dev/null +++ b/modules/apps/27-gmp/types/genesis_test.go @@ -0,0 +1,124 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +func TestDefaultGenesisState(t *testing.T) { + gs := types.DefaultGenesisState() + require.NotNil(t, gs) + require.Empty(t, gs.Ics27Accounts) +} + +func TestGenesisState_Validate(t *testing.T) { + validAddress := ibctesting.TestAccAddress + validClientID := ibctesting.FirstClientID + + testCases := []struct { + name string + genState *types.GenesisState + expErr bool + }{ + { + "success: default genesis", + types.DefaultGenesisState(), + false, + }, + { + "success: valid genesis with account", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: validAddress, + AccountId: types.AccountIdentifier{ + ClientId: validClientID, + Sender: validAddress, + Salt: []byte("salt"), + }, + }, + }, + }, + false, + }, + { + "failure: invalid account address", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: "invalid", + AccountId: types.AccountIdentifier{ + ClientId: validClientID, + Sender: validAddress, + Salt: []byte("salt"), + }, + }, + }, + }, + true, + }, + { + "failure: invalid sender address", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: validAddress, + AccountId: types.AccountIdentifier{ + ClientId: validClientID, + Sender: "invalid", + Salt: []byte("salt"), + }, + }, + }, + }, + true, + }, + { + "failure: invalid client ID", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: validAddress, + AccountId: types.AccountIdentifier{ + ClientId: "x", + Sender: validAddress, + Salt: []byte("salt"), + }, + }, + }, + }, + true, + }, + { + "failure: salt exceeds max length", + &types.GenesisState{ + Ics27Accounts: []types.RegisteredICS27Account{ + { + AccountAddress: validAddress, + AccountId: types.AccountIdentifier{ + ClientId: validClientID, + Sender: validAddress, + Salt: make([]byte, types.MaximumSaltLength+1), + }, + }, + }, + }, + true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.genState.Validate() + if tc.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} From 6c71185e665d572477bd98a9e5ec23a355ba0e4e Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 12:21:37 +0000 Subject: [PATCH 18/22] update --- modules/apps/27-gmp/types/account_test.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/modules/apps/27-gmp/types/account_test.go b/modules/apps/27-gmp/types/account_test.go index e706620f618..2ba0b6fdb55 100644 --- a/modules/apps/27-gmp/types/account_test.go +++ b/modules/apps/27-gmp/types/account_test.go @@ -18,6 +18,7 @@ import ( "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" 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" ) func TestBuildAddressPredictable(t *testing.T) { @@ -29,7 +30,7 @@ func TestBuildAddressPredictable(t *testing.T) { { "success: valid account identifier", &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("randomsalt"), }, @@ -38,7 +39,7 @@ func TestBuildAddressPredictable(t *testing.T) { { "success: empty salt is allowed", &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte{}, }, @@ -47,7 +48,7 @@ func TestBuildAddressPredictable(t *testing.T) { { "success: nil salt is allowed", &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: nil, }, @@ -74,7 +75,7 @@ func TestBuildAddressPredictable(t *testing.T) { { "failure: empty sender", &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "", Salt: []byte("salt"), }, @@ -83,7 +84,7 @@ func TestBuildAddressPredictable(t *testing.T) { { "failure: whitespace-only sender", &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: " ", Salt: []byte("salt"), }, @@ -108,7 +109,7 @@ func TestBuildAddressPredictable(t *testing.T) { func TestBuildAddressPredictable_Determinism(t *testing.T) { accountID := &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("randomsalt"), } @@ -131,7 +132,7 @@ func TestBuildAddressPredictable_Determinism(t *testing.T) { func TestBuildAddressPredictable_Uniqueness(t *testing.T) { // Base account identifier baseAccountID := &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt1"), } @@ -141,7 +142,7 @@ func TestBuildAddressPredictable_Uniqueness(t *testing.T) { // Different salt should produce different address differentSaltID := &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt2"), } @@ -151,7 +152,7 @@ func TestBuildAddressPredictable_Uniqueness(t *testing.T) { // Different sender should produce different address differentSenderID := &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1different", Salt: []byte("salt1"), } @@ -171,7 +172,7 @@ func TestBuildAddressPredictable_Uniqueness(t *testing.T) { } func TestNewAccountIdentifier(t *testing.T) { - clientID := "07-tendermint-0" + clientID := ibctesting.FirstClientID sender := "cosmos1sender" salt := []byte("salt") @@ -185,7 +186,7 @@ func TestNewAccountIdentifier(t *testing.T) { func TestNewICS27Account(t *testing.T) { addr := "cosmos1address" accountID := &types.AccountIdentifier{ - ClientId: "07-tendermint-0", + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt"), } From 563fed81a8cd339fe63f5ad6073a9c68ade532c4 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 12:25:46 +0000 Subject: [PATCH 19/22] update --- modules/apps/27-gmp/keeper/query_server_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/apps/27-gmp/keeper/query_server_test.go b/modules/apps/27-gmp/keeper/query_server_test.go index bd0914b7527..5892745c2b0 100644 --- a/modules/apps/27-gmp/keeper/query_server_test.go +++ b/modules/apps/27-gmp/keeper/query_server_test.go @@ -73,6 +73,7 @@ func (s *KeeperTestSuite) TestQueryAccountIdentifier() { var ( req *types.QueryAccountIdentifierRequest gmpAccountAddr string + expAccountId *types.AccountIdentifier ) testCases := []struct { @@ -83,6 +84,12 @@ func (s *KeeperTestSuite) TestQueryAccountIdentifier() { { "success", func() { + sender := s.chainB.SenderAccount.GetAddress().String() + expAccountId = &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: sender, + Salt: []byte(testSalt), + } s.createGMPAccount(gmpAccountAddr) }, nil, @@ -119,12 +126,11 @@ func (s *KeeperTestSuite) TestQueryAccountIdentifier() { resp, err := s.chainA.GetSimApp().GMPKeeper.AccountIdentifier(s.chainA.GetContext(), req) - expPass := tc.expErr == nil - if expPass { + if tc.expErr == nil { s.Require().NoError(err) - s.Require().Equal(ibctesting.FirstClientID, resp.AccountId.ClientId) - s.Require().Equal(sender, resp.AccountId.Sender) - s.Require().Equal([]byte(testSalt), resp.AccountId.Salt) + s.Require().Equal(expAccountId.ClientId, resp.AccountId.ClientId) + s.Require().Equal(expAccountId.Sender, resp.AccountId.Sender) + s.Require().Equal(expAccountId.Salt, resp.AccountId.Salt) } else { s.Require().ErrorIs(err, tc.expErr) } From 5711c678144ffd5ba577941dde9cff32f32c955d Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 12:27:42 +0000 Subject: [PATCH 20/22] loop --- modules/apps/27-gmp/types/account_test.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/modules/apps/27-gmp/types/account_test.go b/modules/apps/27-gmp/types/account_test.go index 2ba0b6fdb55..2422ff5e9b5 100644 --- a/modules/apps/27-gmp/types/account_test.go +++ b/modules/apps/27-gmp/types/account_test.go @@ -114,19 +114,14 @@ func TestBuildAddressPredictable_Determinism(t *testing.T) { Salt: []byte("randomsalt"), } - // Generate address multiple times - addr1, err := types.BuildAddressPredictable(accountID) + firstAddr, err := types.BuildAddressPredictable(accountID) require.NoError(t, err) - addr2, err := types.BuildAddressPredictable(accountID) - require.NoError(t, err) - - addr3, err := types.BuildAddressPredictable(accountID) - require.NoError(t, err) - - // All addresses should be identical - require.Equal(t, addr1, addr2, "addresses should be deterministic") - require.Equal(t, addr2, addr3, "addresses should be deterministic") + for range 50 { + addr, err := types.BuildAddressPredictable(accountID) + require.NoError(t, err) + require.Equal(t, firstAddr, addr, "addresses should be deterministic") + } } func TestBuildAddressPredictable_Uniqueness(t *testing.T) { From 431650bb52e7e30a08321b2c0f4db879c7e1408b Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 12:59:10 +0000 Subject: [PATCH 21/22] update test --- modules/apps/27-gmp/keeper/msg_server_test.go | 82 +++++-------------- 1 file changed, 22 insertions(+), 60 deletions(-) diff --git a/modules/apps/27-gmp/keeper/msg_server_test.go b/modules/apps/27-gmp/keeper/msg_server_test.go index 98631dbbf53..658ae6b87f9 100644 --- a/modules/apps/27-gmp/keeper/msg_server_test.go +++ b/modules/apps/27-gmp/keeper/msg_server_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "errors" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -10,102 +11,57 @@ import ( ibctesting "github.com/cosmos/ibc-go/v10/testing" ) +var errAny = errors.New("any error") + func (s *KeeperTestSuite) TestSendCall() { var msg *types.MsgSendCall testCases := []struct { - name string - malleate func() - expEncoding string + name string + malleate func() + expErr error }{ { "success: empty encoding defaults to ABI", func() { msg.Encoding = "" }, - types.EncodingABI, + nil, }, { "success: protobuf encoding", func() { msg.Encoding = types.EncodingProtobuf }, - types.EncodingProtobuf, + nil, }, { "success: JSON encoding", func() { msg.Encoding = types.EncodingJSON }, - types.EncodingJSON, + nil, }, { "success: ABI encoding", func() { msg.Encoding = types.EncodingABI }, - types.EncodingABI, + nil, }, - } - - for _, tc := range testCases { - s.Run(tc.name, func() { - s.SetupTest() - - path := ibctesting.NewPath(s.chainA, s.chainB) - path.SetupV2() - - sender := s.chainA.SenderAccount.GetAddress() - recipient := s.chainB.SenderAccount.GetAddress() - payload := s.serializeMsgs(&banktypes.MsgSend{ - FromAddress: sender.String(), - ToAddress: recipient.String(), - Amount: sdk.NewCoins(ibctesting.TestCoin), - }) - - msg = types.NewMsgSendCall( - path.EndpointA.ClientID, - sender.String(), - "", - payload, - []byte(testSalt), - uint64(s.chainA.GetContext().BlockTime().Add(time.Hour).Unix()), - types.EncodingProtobuf, - "", - ) - - tc.malleate() - - resp, err := s.chainA.GetSimApp().GMPKeeper.SendCall(s.chainA.GetContext(), msg) - - s.Require().NoError(err) - s.Require().NotNil(resp) - s.Require().Equal(uint64(1), resp.Sequence) - }) - } -} - -func (s *KeeperTestSuite) TestSendCallErrors() { - var msg *types.MsgSendCall - - testCases := []struct { - name string - malleate func() - expErr error - }{ { "failure: invalid sender address", func() { msg.Sender = "invalid" }, - nil, // bech32 error not wrapped + errAny, }, { "failure: empty sender address", func() { msg.Sender = "" }, - nil, // bech32 error not wrapped + errAny, }, { "failure: invalid encoding", @@ -119,7 +75,7 @@ func (s *KeeperTestSuite) TestSendCallErrors() { func() { msg.SourceClient = "invalid" }, - nil, // counterparty not found error + errAny, }, { "failure: payload too long", @@ -160,10 +116,16 @@ func (s *KeeperTestSuite) TestSendCallErrors() { resp, err := s.chainA.GetSimApp().GMPKeeper.SendCall(s.chainA.GetContext(), msg) - s.Require().Error(err) - s.Require().Nil(resp) - if tc.expErr != nil { + if tc.expErr == nil { + s.Require().NoError(err) + s.Require().NotNil(resp) + s.Require().Equal(uint64(1), resp.Sequence) + } else if errors.Is(tc.expErr, errAny) { + s.Require().Error(err) + s.Require().Nil(resp) + } else { s.Require().ErrorIs(err, tc.expErr) + s.Require().Nil(resp) } }) } From bcc79adc3e5fe74ae00669e3872f7b3405e4efc3 Mon Sep 17 00:00:00 2001 From: Dmytro Onypko Date: Thu, 11 Dec 2025 13:24:54 +0000 Subject: [PATCH 22/22] rework tests --- modules/apps/27-gmp/types/account_test.go | 114 +++++++------- modules/apps/27-gmp/types/ack_test.go | 85 +++------- modules/apps/27-gmp/types/packet_test.go | 87 +++-------- .../apps/27-gmp/types/solidity_abi_test.go | 146 ++++++++++-------- 4 files changed, 170 insertions(+), 262 deletions(-) diff --git a/modules/apps/27-gmp/types/account_test.go b/modules/apps/27-gmp/types/account_test.go index 2422ff5e9b5..3af9256cdf2 100644 --- a/modules/apps/27-gmp/types/account_test.go +++ b/modules/apps/27-gmp/types/account_test.go @@ -105,65 +105,58 @@ func TestBuildAddressPredictable(t *testing.T) { } }) } -} - -func TestBuildAddressPredictable_Determinism(t *testing.T) { - accountID := &types.AccountIdentifier{ - ClientId: ibctesting.FirstClientID, - Sender: "cosmos1sender", - Salt: []byte("randomsalt"), - } - firstAddr, err := types.BuildAddressPredictable(accountID) - require.NoError(t, err) - - for range 50 { - addr, err := types.BuildAddressPredictable(accountID) + t.Run("determinism", func(t *testing.T) { + accountID := &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "cosmos1sender", + Salt: []byte("randomsalt"), + } + firstAddr, err := types.BuildAddressPredictable(accountID) require.NoError(t, err) - require.Equal(t, firstAddr, addr, "addresses should be deterministic") - } -} -func TestBuildAddressPredictable_Uniqueness(t *testing.T) { - // Base account identifier - baseAccountID := &types.AccountIdentifier{ - ClientId: ibctesting.FirstClientID, - Sender: "cosmos1sender", - Salt: []byte("salt1"), - } - - baseAddr, err := types.BuildAddressPredictable(baseAccountID) - require.NoError(t, err) + for range 50 { + addr, err := types.BuildAddressPredictable(accountID) + require.NoError(t, err) + require.Equal(t, firstAddr, addr) + } + }) - // Different salt should produce different address - differentSaltID := &types.AccountIdentifier{ - ClientId: ibctesting.FirstClientID, - Sender: "cosmos1sender", - Salt: []byte("salt2"), - } - differentSaltAddr, err := types.BuildAddressPredictable(differentSaltID) - require.NoError(t, err) - require.NotEqual(t, baseAddr, differentSaltAddr, "different salts should produce different addresses") + t.Run("uniqueness: different salt", func(t *testing.T) { + addr1, err := types.BuildAddressPredictable(&types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt1"), + }) + require.NoError(t, err) + addr2, err := types.BuildAddressPredictable(&types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt2"), + }) + require.NoError(t, err) + require.NotEqual(t, addr1, addr2) + }) - // Different sender should produce different address - differentSenderID := &types.AccountIdentifier{ - ClientId: ibctesting.FirstClientID, - Sender: "cosmos1different", - Salt: []byte("salt1"), - } - differentSenderAddr, err := types.BuildAddressPredictable(differentSenderID) - require.NoError(t, err) - require.NotEqual(t, baseAddr, differentSenderAddr, "different senders should produce different addresses") + t.Run("uniqueness: different sender", func(t *testing.T) { + addr1, err := types.BuildAddressPredictable(&types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt"), + }) + require.NoError(t, err) + addr2, err := types.BuildAddressPredictable(&types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, Sender: "cosmos1different", Salt: []byte("salt"), + }) + require.NoError(t, err) + require.NotEqual(t, addr1, addr2) + }) - // Different client ID should produce different address - differentClientID := &types.AccountIdentifier{ - ClientId: "07-tendermint-1", - Sender: "cosmos1sender", - Salt: []byte("salt1"), - } - differentClientAddr, err := types.BuildAddressPredictable(differentClientID) - require.NoError(t, err) - require.NotEqual(t, baseAddr, differentClientAddr, "different client IDs should produce different addresses") + t.Run("uniqueness: different client ID", func(t *testing.T) { + addr1, err := types.BuildAddressPredictable(&types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, Sender: "cosmos1sender", Salt: []byte("salt"), + }) + require.NoError(t, err) + addr2, err := types.BuildAddressPredictable(&types.AccountIdentifier{ + ClientId: "07-tendermint-1", Sender: "cosmos1sender", Salt: []byte("salt"), + }) + require.NoError(t, err) + require.NotEqual(t, addr1, addr2) + }) } func TestNewAccountIdentifier(t *testing.T) { @@ -262,17 +255,16 @@ func TestDeserializeCosmosTx(t *testing.T) { require.ErrorIs(t, err, ibcerrors.ErrInvalidType) require.Nil(t, msgs) }) + + t.Run("failure: invalid codec", func(t *testing.T) { + mock := &mockCodec{} + msgs, err := types.DeserializeCosmosTx(mock, []byte("data")) + require.ErrorIs(t, err, types.ErrInvalidCodec) + require.Nil(t, msgs) + }) } // mockCodec is a mock codec that implements codec.Codec but is not a ProtoCodec type mockCodec struct { codec.Codec } - -func TestDeserializeCosmosTx_InvalidCodec(t *testing.T) { - // Test that the function rejects non-ProtoCodec codecs - mock := &mockCodec{} - msgs, err := types.DeserializeCosmosTx(mock, []byte("data")) - require.ErrorIs(t, err, types.ErrInvalidCodec) - require.Nil(t, msgs) -} diff --git a/modules/apps/27-gmp/types/ack_test.go b/modules/apps/27-gmp/types/ack_test.go index 979da6f1d64..692b504cdb2 100644 --- a/modules/apps/27-gmp/types/ack_test.go +++ b/modules/apps/27-gmp/types/ack_test.go @@ -28,35 +28,29 @@ func TestMarshalUnmarshalAcknowledgement(t *testing.T) { } testCases := []struct { - name string - encoding string - expErr error + name string + encoding string + invalidData []byte + expErr error }{ - { - "success: JSON encoding", - types.EncodingJSON, - nil, - }, - { - "success: Protobuf encoding", - types.EncodingProtobuf, - nil, - }, - { - "success: ABI encoding", - types.EncodingABI, - nil, - }, - { - "failure: invalid encoding", - "invalid-encoding", - types.ErrInvalidEncoding, - }, + {"success: JSON encoding", types.EncodingJSON, nil, nil}, + {"success: Protobuf encoding", types.EncodingProtobuf, nil, nil}, + {"success: ABI encoding", types.EncodingABI, nil, nil}, + {"failure: invalid encoding on marshal", "invalid-encoding", nil, types.ErrInvalidEncoding}, + {"failure: invalid encoding on unmarshal", "invalid-encoding", []byte("data"), types.ErrInvalidEncoding}, + {"failure: invalid JSON data", types.EncodingJSON, []byte("not valid json"), ibcerrors.ErrInvalidType}, + {"failure: invalid Protobuf data", types.EncodingProtobuf, []byte("not valid protobuf"), ibcerrors.ErrInvalidType}, + {"failure: invalid ABI data", types.EncodingABI, []byte("not valid abi"), ibcerrors.ErrInvalidType}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - // Marshal + if tc.invalidData != nil { + _, err := types.UnmarshalAcknowledgement(tc.invalidData, types.Version, tc.encoding) + require.ErrorIs(t, err, tc.expErr) + return + } + bz, err := types.MarshalAcknowledgement(ack, types.Version, tc.encoding) if tc.expErr != nil { require.ErrorIs(t, err, tc.expErr) @@ -65,56 +59,13 @@ func TestMarshalUnmarshalAcknowledgement(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, bz) - // Unmarshal decoded, err := types.UnmarshalAcknowledgement(bz, types.Version, tc.encoding) require.NoError(t, err) - - // Compare require.Equal(t, ack.Result, decoded.Result) }) } } -func TestUnmarshalAcknowledgement_InvalidEncoding(t *testing.T) { - _, err := types.UnmarshalAcknowledgement([]byte("data"), types.Version, "invalid-encoding") - require.ErrorIs(t, err, types.ErrInvalidEncoding) -} - -func TestUnmarshalAcknowledgement_InvalidData(t *testing.T) { - testCases := []struct { - name string - data []byte - encoding string - expErr error - }{ - { - "failure: invalid JSON data", - []byte("not valid json"), - types.EncodingJSON, - ibcerrors.ErrInvalidType, - }, - { - "failure: invalid Protobuf data", - []byte("not valid protobuf"), - types.EncodingProtobuf, - ibcerrors.ErrInvalidType, - }, - { - "failure: invalid ABI data", - []byte("not valid abi"), - types.EncodingABI, - ibcerrors.ErrInvalidType, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := types.UnmarshalAcknowledgement(tc.data, types.Version, tc.encoding) - require.ErrorIs(t, err, tc.expErr) - }) - } -} - func TestMarshalAcknowledgement_EmptyResult(t *testing.T) { ack := &types.Acknowledgement{ Result: []byte{}, diff --git a/modules/apps/27-gmp/types/packet_test.go b/modules/apps/27-gmp/types/packet_test.go index 9f8df710b16..1a89eead4be 100644 --- a/modules/apps/27-gmp/types/packet_test.go +++ b/modules/apps/27-gmp/types/packet_test.go @@ -1,6 +1,7 @@ package types_test import ( + "errors" "strings" "testing" @@ -12,6 +13,8 @@ import ( ibctesting "github.com/cosmos/ibc-go/v10/testing" ) +var errAny = errors.New("any error") + func TestGMPPacketData_ValidateBasic(t *testing.T) { testCases := []struct { name string @@ -97,35 +100,29 @@ func TestMarshalUnmarshalPacketData(t *testing.T) { } testCases := []struct { - name string - encoding string - expErr error + name string + encoding string + invalidData []byte + expErr error }{ - { - "success: JSON encoding", - types.EncodingJSON, - nil, - }, - { - "success: Protobuf encoding", - types.EncodingProtobuf, - nil, - }, - { - "success: ABI encoding", - types.EncodingABI, - nil, - }, - { - "failure: invalid encoding", - "invalid-encoding", - types.ErrInvalidEncoding, - }, + {"success: JSON encoding", types.EncodingJSON, nil, nil}, + {"success: Protobuf encoding", types.EncodingProtobuf, nil, nil}, + {"success: ABI encoding", types.EncodingABI, nil, nil}, + {"failure: invalid encoding on marshal", "invalid-encoding", nil, types.ErrInvalidEncoding}, + {"failure: invalid encoding on unmarshal", "invalid-encoding", []byte("data"), types.ErrInvalidEncoding}, + {"failure: invalid JSON data", types.EncodingJSON, []byte("not valid json"), ibcerrors.ErrInvalidType}, + {"failure: invalid Protobuf data", types.EncodingProtobuf, []byte("not valid protobuf"), ibcerrors.ErrInvalidType}, + {"failure: invalid ABI data", types.EncodingABI, []byte("not valid abi"), ibcerrors.ErrInvalidType}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - // Marshal + if tc.invalidData != nil { + _, err := types.UnmarshalPacketData(tc.invalidData, types.Version, tc.encoding) + require.ErrorIs(t, err, tc.expErr) + return + } + bz, err := types.MarshalPacketData(packetData, types.Version, tc.encoding) if tc.expErr != nil { require.ErrorIs(t, err, tc.expErr) @@ -134,11 +131,9 @@ func TestMarshalUnmarshalPacketData(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, bz) - // Unmarshal decoded, err := types.UnmarshalPacketData(bz, types.Version, tc.encoding) require.NoError(t, err) - // Compare require.Equal(t, packetData.Sender, decoded.Sender) require.Equal(t, packetData.Receiver, decoded.Receiver) require.Equal(t, packetData.Salt, decoded.Salt) @@ -148,46 +143,6 @@ func TestMarshalUnmarshalPacketData(t *testing.T) { } } -func TestUnmarshalPacketData_InvalidEncoding(t *testing.T) { - _, err := types.UnmarshalPacketData([]byte("data"), types.Version, "invalid-encoding") - require.ErrorIs(t, err, types.ErrInvalidEncoding) -} - -func TestUnmarshalPacketData_InvalidData(t *testing.T) { - testCases := []struct { - name string - data []byte - encoding string - expErr error - }{ - { - "failure: invalid JSON data", - []byte("not valid json"), - types.EncodingJSON, - ibcerrors.ErrInvalidType, - }, - { - "failure: invalid Protobuf data", - []byte("not valid protobuf"), - types.EncodingProtobuf, - ibcerrors.ErrInvalidType, - }, - { - "failure: invalid ABI data", - []byte("not valid abi"), - types.EncodingABI, - ibcerrors.ErrInvalidType, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := types.UnmarshalPacketData(tc.data, types.Version, tc.encoding) - require.ErrorIs(t, err, tc.expErr) - }) - } -} - func TestMsgSendCall_ValidateBasic(t *testing.T) { validSender := "cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du" diff --git a/modules/apps/27-gmp/types/solidity_abi_test.go b/modules/apps/27-gmp/types/solidity_abi_test.go index 82e36fd53c4..4a34c828409 100644 --- a/modules/apps/27-gmp/types/solidity_abi_test.go +++ b/modules/apps/27-gmp/types/solidity_abi_test.go @@ -10,8 +10,10 @@ import ( func TestEncodeDecodeABIGMPPacketData(t *testing.T) { testCases := []struct { - name string - packetData *types.GMPPacketData + name string + packetData *types.GMPPacketData + invalidData []byte + expErr error }{ { "success: all fields populated", @@ -22,6 +24,8 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: []byte("some payload data"), Memo: "test memo", }, + nil, + nil, }, { "success: empty salt", @@ -32,6 +36,8 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: []byte("payload"), Memo: "memo", }, + nil, + nil, }, { "success: empty payload", @@ -42,6 +48,8 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: []byte{}, Memo: "memo", }, + nil, + nil, }, { "success: empty memo", @@ -52,6 +60,8 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: []byte("payload"), Memo: "", }, + nil, + nil, }, { "success: empty receiver", @@ -62,6 +72,8 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: []byte("payload"), Memo: "memo", }, + nil, + nil, }, { "success: all optional fields empty", @@ -72,6 +84,8 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: []byte{}, Memo: "", }, + nil, + nil, }, { "success: large payload", @@ -82,124 +96,120 @@ func TestEncodeDecodeABIGMPPacketData(t *testing.T) { Payload: make([]byte, 1024), // 1KB payload Memo: "memo", }, + nil, + nil, }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Encode - encoded, err := types.EncodeABIGMPPacketData(tc.packetData) - require.NoError(t, err, "encoding should succeed") - require.NotEmpty(t, encoded, "encoded data should not be empty") - - // Decode - decoded, err := types.DecodeABIGMPPacketData(encoded) - require.NoError(t, err, "decoding should succeed") - - // Compare - require.Equal(t, tc.packetData.Sender, decoded.Sender, "sender mismatch") - require.Equal(t, tc.packetData.Receiver, decoded.Receiver, "receiver mismatch") - require.Equal(t, tc.packetData.Salt, decoded.Salt, "salt mismatch") - require.Equal(t, tc.packetData.Payload, decoded.Payload, "payload mismatch") - require.Equal(t, tc.packetData.Memo, decoded.Memo, "memo mismatch") - }) - } -} - -func TestDecodeABIGMPPacketData_Invalid(t *testing.T) { - testCases := []struct { - name string - data []byte - }{ { - "empty data", + "failure: empty data", + nil, []byte{}, + types.ErrAbiDecoding, }, { - "invalid abi data", + "failure: invalid abi data", + nil, []byte("not valid abi encoded data"), + types.ErrAbiDecoding, }, { - "truncated data", + "failure: truncated data", + nil, []byte{0x00, 0x01, 0x02, 0x03}, + types.ErrAbiDecoding, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := types.DecodeABIGMPPacketData(tc.data) - require.Error(t, err, "decoding invalid data should fail") - require.ErrorIs(t, err, types.ErrAbiDecoding) + if tc.invalidData != nil { + _, err := types.DecodeABIGMPPacketData(tc.invalidData) + require.ErrorIs(t, err, tc.expErr) + return + } + + encoded, err := types.EncodeABIGMPPacketData(tc.packetData) + require.NoError(t, err) + require.NotEmpty(t, encoded) + + decoded, err := types.DecodeABIGMPPacketData(encoded) + require.NoError(t, err) + + require.Equal(t, tc.packetData.Sender, decoded.Sender) + require.Equal(t, tc.packetData.Receiver, decoded.Receiver) + require.Equal(t, tc.packetData.Salt, decoded.Salt) + require.Equal(t, tc.packetData.Payload, decoded.Payload) + require.Equal(t, tc.packetData.Memo, decoded.Memo) }) } } func TestEncodeDecodeABIAcknowledgement(t *testing.T) { testCases := []struct { - name string - ack *types.Acknowledgement + name string + ack *types.Acknowledgement + invalidData []byte + expErr error }{ { "success: non-empty result", &types.Acknowledgement{ Result: []byte("success result data"), }, + nil, + nil, }, { "success: empty result", &types.Acknowledgement{ Result: []byte{}, }, + nil, + nil, }, { "success: large result", &types.Acknowledgement{ Result: make([]byte, 1024), // 1KB result }, + nil, + nil, }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Encode - encoded, err := types.EncodeABIAcknowledgement(tc.ack) - require.NoError(t, err, "encoding should succeed") - require.NotEmpty(t, encoded, "encoded data should not be empty") - - // Decode - decoded, err := types.DecodeABIAcknowledgement(encoded) - require.NoError(t, err, "decoding should succeed") - - // Compare - require.Equal(t, tc.ack.Result, decoded.Result, "result mismatch") - }) - } -} - -func TestDecodeABIAcknowledgement_Invalid(t *testing.T) { - testCases := []struct { - name string - data []byte - }{ { - "empty data", + "failure: empty data", + nil, []byte{}, + types.ErrAbiDecoding, }, { - "invalid abi data", + "failure: invalid abi data", + nil, []byte("not valid abi encoded data"), + types.ErrAbiDecoding, }, { - "truncated data", + "failure: truncated data", + nil, []byte{0x00, 0x01, 0x02, 0x03}, + types.ErrAbiDecoding, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - _, err := types.DecodeABIAcknowledgement(tc.data) - require.Error(t, err, "decoding invalid data should fail") - require.ErrorIs(t, err, types.ErrAbiDecoding) + if tc.invalidData != nil { + _, err := types.DecodeABIAcknowledgement(tc.invalidData) + require.ErrorIs(t, err, tc.expErr) + return + } + + encoded, err := types.EncodeABIAcknowledgement(tc.ack) + require.NoError(t, err) + require.NotEmpty(t, encoded) + + decoded, err := types.DecodeABIAcknowledgement(encoded) + require.NoError(t, err) + + require.Equal(t, tc.ack.Result, decoded.Result) }) } }