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" diff --git a/e2e/tests/gmp/base_test.go b/e2e/tests/gmp/base_test.go new file mode 100644 index 00000000000..93fb60205a8 --- /dev/null +++ b/e2e/tests/gmp/base_test.go @@ -0,0 +1,377 @@ +//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 = 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 (*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 +} + +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) + }) +} 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..45efa11ccb1 --- /dev/null +++ b/modules/apps/27-gmp/ibc_module_test.go @@ -0,0 +1,323 @@ +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" + 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 + chainB *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(), 2) + s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) + s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) +} + +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, + }, + { + "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 { + 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) + } + }) + } +} + +func (s *IBCModuleTestSuite) TestOnRecvPacket() { + const testSalt = "test-salt" + + var ( + module *gmp.IBCModule + payload channeltypesv2.Payload + gmpAccountAddr sdk.AccAddress + sender string + msgPayload []byte + ) + + 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 - 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 { + 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) 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(), + 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/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/genesis_test.go b/modules/apps/27-gmp/keeper/genesis_test.go new file mode 100644 index 00000000000..9c8a8f655c7 --- /dev/null +++ b/modules/apps/27-gmp/keeper/genesis_test.go @@ -0,0 +1,113 @@ +package keeper_test + +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" +) + +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, + }, + { + "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 { + 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) TestGetAuthority() { + s.SetupTest() + + authority := s.chainA.GetSimApp().GMPKeeper.GetAuthority() + s.Require().NotEmpty(authority) +} + +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) +} 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..658ae6b87f9 --- /dev/null +++ b/modules/apps/27-gmp/keeper/msg_server_test.go @@ -0,0 +1,132 @@ +package keeper_test + +import ( + "errors" + "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" +) + +var errAny = errors.New("any error") + +func (s *KeeperTestSuite) TestSendCall() { + var msg *types.MsgSendCall + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: empty encoding defaults to ABI", + func() { + msg.Encoding = "" + }, + nil, + }, + { + "success: protobuf encoding", + func() { + msg.Encoding = types.EncodingProtobuf + }, + nil, + }, + { + "success: JSON encoding", + func() { + msg.Encoding = types.EncodingJSON + }, + nil, + }, + { + "success: ABI encoding", + func() { + msg.Encoding = types.EncodingABI + }, + nil, + }, + { + "failure: invalid sender address", + func() { + msg.Sender = "invalid" + }, + errAny, + }, + { + "failure: empty sender address", + func() { + msg.Sender = "" + }, + errAny, + }, + { + "failure: invalid encoding", + func() { + msg.Encoding = "invalid-encoding" + }, + types.ErrInvalidEncoding, + }, + { + "failure: invalid source client - counterparty not found", + func() { + msg.SourceClient = "invalid" + }, + errAny, + }, + { + "failure: payload too long", + func() { + msg.Payload = make([]byte, types.MaximumPayloadLength+1) + }, + types.ErrInvalidPayload, + }, + } + + 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) + + 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) + } + }) + } +} 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..5892745c2b0 --- /dev/null +++ b/modules/apps/27-gmp/keeper/query_server_test.go @@ -0,0 +1,281 @@ +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 + expAccountId *types.AccountIdentifier + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() { + sender := s.chainB.SenderAccount.GetAddress().String() + expAccountId = &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: sender, + Salt: []byte(testSalt), + } + 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) + + if tc.expErr == nil { + s.Require().NoError(err) + 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) + } + }) + } +} + +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 + 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() + + 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..4b49848efa7 --- /dev/null +++ b/modules/apps/27-gmp/keeper/relay_test.go @@ -0,0 +1,259 @@ +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" + 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" +) + +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 + destClient string + ) + + 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, + }, + { + "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 { + s.Run(tc.name, func() { + s.SetupTest() + + 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) + 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, destClient, + ) + + 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/module_test.go b/modules/apps/27-gmp/module_test.go new file mode 100644 index 00000000000..49919c20363 --- /dev/null +++ b/modules/apps/27-gmp/module_test.go @@ -0,0 +1,205 @@ +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, + }, + { + "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 { + 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 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()) +} + +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) +} 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..3af9256cdf2 --- /dev/null +++ b/modules/apps/27-gmp/types/account_test.go @@ -0,0 +1,270 @@ +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" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +func TestBuildAddressPredictable(t *testing.T) { + testCases := []struct { + name string + accountID *types.AccountIdentifier + expErr error + }{ + { + "success: valid account identifier", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "cosmos1sender", + Salt: []byte("randomsalt"), + }, + nil, + }, + { + "success: empty salt is allowed", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + Sender: "cosmos1sender", + Salt: []byte{}, + }, + nil, + }, + { + "success: nil salt is allowed", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + 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: ibctesting.FirstClientID, + Sender: "", + Salt: []byte("salt"), + }, + ibcerrors.ErrInvalidAddress, + }, + { + "failure: whitespace-only sender", + &types.AccountIdentifier{ + ClientId: ibctesting.FirstClientID, + 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) + } + }) + } + + 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) + + for range 50 { + addr, err := types.BuildAddressPredictable(accountID) + require.NoError(t, err) + require.Equal(t, firstAddr, addr) + } + }) + + 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) + }) + + 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) + }) + + 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) { + clientID := ibctesting.FirstClientID + 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: ibctesting.FirstClientID, + Sender: "cosmos1sender", + Salt: []byte("salt"), + } + + account := types.NewICS27Account(addr, accountID) + + 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) + }) + + 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 +} 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..692b504cdb2 --- /dev/null +++ b/modules/apps/27-gmp/types/ack_test.go @@ -0,0 +1,97 @@ +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 + invalidData []byte + expErr error + }{ + {"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) { + 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) + return + } + require.NoError(t, err) + require.NotEmpty(t, bz) + + decoded, err := types.UnmarshalAcknowledgement(bz, types.Version, tc.encoding) + require.NoError(t, err) + require.Equal(t, ack.Result, decoded.Result) + }) + } +} + +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) + }) + } +} 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()) + } + }) + } +} 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) + } + }) + } +} 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..1a89eead4be --- /dev/null +++ b/modules/apps/27-gmp/types/packet_test.go @@ -0,0 +1,320 @@ +package types_test + +import ( + "errors" + "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" +) + +var errAny = errors.New("any error") + +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 + invalidData []byte + expErr error + }{ + {"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) { + 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) + return + } + require.NoError(t, err) + require.NotEmpty(t, bz) + + decoded, err := types.UnmarshalPacketData(bz, types.Version, tc.encoding) + require.NoError(t, err) + + 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 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..4a34c828409 --- /dev/null +++ b/modules/apps/27-gmp/types/solidity_abi_test.go @@ -0,0 +1,215 @@ +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 + invalidData []byte + expErr error + }{ + { + "success: all fields populated", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("randomsalt"), + Payload: []byte("some payload data"), + Memo: "test memo", + }, + nil, + nil, + }, + { + "success: empty salt", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte{}, + Payload: []byte("payload"), + Memo: "memo", + }, + nil, + nil, + }, + { + "success: empty payload", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("salt"), + Payload: []byte{}, + Memo: "memo", + }, + nil, + nil, + }, + { + "success: empty memo", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("salt"), + Payload: []byte("payload"), + Memo: "", + }, + nil, + nil, + }, + { + "success: empty receiver", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "", + Salt: []byte("salt"), + Payload: []byte("payload"), + Memo: "memo", + }, + nil, + nil, + }, + { + "success: all optional fields empty", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "", + Salt: []byte{}, + Payload: []byte{}, + Memo: "", + }, + nil, + nil, + }, + { + "success: large payload", + &types.GMPPacketData{ + Sender: "cosmos1sender", + Receiver: "cosmos1receiver", + Salt: []byte("salt"), + Payload: make([]byte, 1024), // 1KB payload + Memo: "memo", + }, + nil, + nil, + }, + { + "failure: empty data", + nil, + []byte{}, + types.ErrAbiDecoding, + }, + { + "failure: invalid abi data", + nil, + []byte("not valid abi encoded data"), + types.ErrAbiDecoding, + }, + { + "failure: truncated data", + nil, + []byte{0x00, 0x01, 0x02, 0x03}, + types.ErrAbiDecoding, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + 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 + 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, + }, + { + "failure: empty data", + nil, + []byte{}, + types.ErrAbiDecoding, + }, + { + "failure: invalid abi data", + nil, + []byte("not valid abi encoded data"), + types.ErrAbiDecoding, + }, + { + "failure: truncated data", + nil, + []byte{0x00, 0x01, 0x02, 0x03}, + types.ErrAbiDecoding, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + 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) + }) + } +} 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...)