diff --git a/modules/apps/27-interchain-accounts/controller/keeper/keeper.go b/modules/apps/27-interchain-accounts/controller/keeper/keeper.go index 9aa172a6f15..2c98ce3f9b1 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/keeper.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/keeper.go @@ -27,7 +27,7 @@ import ( // Keeper defines the IBC interchain accounts controller keeper type Keeper struct { storeKey storetypes.StoreKey - cdc codec.BinaryCodec + cdc codec.Codec legacySubspace paramtypes.Subspace ics4Wrapper porttypes.ICS4Wrapper channelKeeper icatypes.ChannelKeeper @@ -44,7 +44,7 @@ type Keeper struct { // NewKeeper creates a new interchain accounts controller Keeper instance func NewKeeper( - cdc codec.BinaryCodec, key storetypes.StoreKey, legacySubspace paramtypes.Subspace, + cdc codec.Codec, key storetypes.StoreKey, legacySubspace paramtypes.Subspace, ics4Wrapper porttypes.ICS4Wrapper, channelKeeper icatypes.ChannelKeeper, portKeeper icatypes.PortKeeper, scopedKeeper exported.ScopedKeeper, msgRouter icatypes.MessageRouter, authority string, ) Keeper { diff --git a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go index 7960af5b01b..08d3fd4ce5c 100644 --- a/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go +++ b/modules/apps/27-interchain-accounts/controller/keeper/msg_server_test.go @@ -171,7 +171,7 @@ func (suite *KeeperTestSuite) TestSubmitTx() { Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.Codec, []proto.Message{icaMsg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{icaMsg}, icatypes.EncodingProtobuf) suite.Require().NoError(err) packetData := icatypes.InterchainAccountPacketData{ diff --git a/modules/apps/27-interchain-accounts/host/client/cli/tx_test.go b/modules/apps/27-interchain-accounts/host/client/cli/tx_test.go index 63422405696..f9c4c0a4d71 100644 --- a/modules/apps/27-interchain-accounts/host/client/cli/tx_test.go +++ b/modules/apps/27-interchain-accounts/host/client/cli/tx_test.go @@ -125,6 +125,7 @@ func TestGeneratePacketData(t *testing.T) { require.Equal(t, tc.memo, packetData.Memo) data := packetData.Data + // cli tx commands always use protobuf encoding messages, err := icatypes.DeserializeCosmosTx(cdc, data, icatypes.EncodingProtobuf) require.NoError(t, err) diff --git a/modules/apps/27-interchain-accounts/host/ibc_module_test.go b/modules/apps/27-interchain-accounts/host/ibc_module_test.go index 3ad31f13012..35687bbdc1b 100644 --- a/modules/apps/27-interchain-accounts/host/ibc_module_test.go +++ b/modules/apps/27-interchain-accounts/host/ibc_module_test.go @@ -446,7 +446,7 @@ func (suite *InterchainAccountsTestSuite) TestOnRecvPacket() { ToAddress: suite.chainB.SenderAccount.GetAddress().String(), Amount: amount, } - data, err := icatypes.SerializeCosmosTx(suite.chainA.Codec, []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ diff --git a/modules/apps/27-interchain-accounts/host/keeper/export_test.go b/modules/apps/27-interchain-accounts/host/keeper/export_test.go new file mode 100644 index 00000000000..03c13e894d5 --- /dev/null +++ b/modules/apps/27-interchain-accounts/host/keeper/export_test.go @@ -0,0 +1,16 @@ +package keeper + +/* + This file is to allow for unexported functions to be accessible to the testing package. +*/ + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" +) + +// GetAppMetadata is a wrapper around getAppMetadata to allow the function to be directly called in tests. +func (k Keeper) GetAppMetadata(ctx sdk.Context, portID, channelID string) (icatypes.Metadata, error) { + return k.getAppMetadata(ctx, portID, channelID) +} diff --git a/modules/apps/27-interchain-accounts/host/keeper/genesis_test.go b/modules/apps/27-interchain-accounts/host/keeper/genesis_test.go index 8e9893be333..3a24e8555dd 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/genesis_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/genesis_test.go @@ -109,7 +109,7 @@ func (suite *KeeperTestSuite) TestGenesisParams() { func (suite *KeeperTestSuite) TestExportGenesis() { suite.SetupTest() - path := NewICAPath(suite.chainA, suite.chainB) + path := NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) diff --git a/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go b/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go index 034550a7d50..c6984fe3181 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/handshake_test.go @@ -276,7 +276,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenTry() { suite.Run(tc.name, func() { suite.SetupTest() // reset - path = NewICAPath(suite.chainA, suite.chainB) + path = NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := RegisterInterchainAccount(path.EndpointA, TestOwnerAddress) @@ -356,7 +356,7 @@ func (suite *KeeperTestSuite) TestOnChanOpenConfirm() { suite.Run(tc.name, func() { suite.SetupTest() // reset - path = NewICAPath(suite.chainA, suite.chainB) + path = NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := RegisterInterchainAccount(path.EndpointA, TestOwnerAddress) @@ -399,7 +399,7 @@ func (suite *KeeperTestSuite) TestOnChanCloseConfirm() { suite.Run(tc.name, func() { suite.SetupTest() // reset - path = NewICAPath(suite.chainA, suite.chainB) + path = NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) diff --git a/modules/apps/27-interchain-accounts/host/keeper/keeper.go b/modules/apps/27-interchain-accounts/host/keeper/keeper.go index 9234117c4fe..467d88b9792 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/keeper.go +++ b/modules/apps/27-interchain-accounts/host/keeper/keeper.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + errorsmod "cosmossdk.io/errors" + "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -18,6 +20,7 @@ import ( channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" host "github.com/cosmos/ibc-go/v7/modules/core/24-host" + ibcerrors "github.com/cosmos/ibc-go/v7/modules/core/errors" "github.com/cosmos/ibc-go/v7/modules/core/exported" ) @@ -106,6 +109,22 @@ func (k Keeper) GetAppVersion(ctx sdk.Context, portID, channelID string) (string return k.ics4Wrapper.GetAppVersion(ctx, portID, channelID) } +// GetAppMetadata retrieves the interchain accounts channel metadata from the store associated with the provided portID and channelID +func (k Keeper) getAppMetadata(ctx sdk.Context, portID, channelID string) (icatypes.Metadata, error) { + appVersion, found := k.GetAppVersion(ctx, portID, channelID) + if !found { + return icatypes.Metadata{}, errorsmod.Wrapf(ibcerrors.ErrNotFound, "app version not found for port %s and channel %s", portID, channelID) + } + + var metadata icatypes.Metadata + if err := icatypes.ModuleCdc.UnmarshalJSON([]byte(appVersion), &metadata); err != nil { + // UnmarshalJSON errors are indeterminate and therefore are not wrapped and included in failed acks + return icatypes.Metadata{}, errorsmod.Wrapf(icatypes.ErrUnknownDataType, "cannot unmarshal ICS-27 interchain accounts metadata") + } + + return metadata, nil +} + // GetActiveChannelID retrieves the active channelID from the store keyed by the provided connectionID and portID func (k Keeper) GetActiveChannelID(ctx sdk.Context, connectionID, portID string) (string, bool) { store := ctx.KVStore(k.storeKey) diff --git a/modules/apps/27-interchain-accounts/host/keeper/keeper_test.go b/modules/apps/27-interchain-accounts/host/keeper/keeper_test.go index 71854cbbe16..b8b91e83df8 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/keeper_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/keeper_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "testing" "github.com/stretchr/testify/suite" @@ -9,6 +10,7 @@ import ( "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + ibcerrors "github.com/cosmos/ibc-go/v7/modules/core/errors" ibctesting "github.com/cosmos/ibc-go/v7/testing" ) @@ -27,6 +29,15 @@ var ( Encoding: icatypes.EncodingProtobuf, TxType: icatypes.TxTypeSDKMultiMsg, })) + + // TestVersionWithJSONEncoding defines a reusable interchainaccounts version string that uses JSON encoding for testing purposes + TestVersionWithJSONEncoding = string(icatypes.ModuleCdc.MustMarshalJSON(&icatypes.Metadata{ + Version: icatypes.Version, + ControllerConnectionId: ibctesting.FirstConnectionID, + HostConnectionId: ibctesting.FirstConnectionID, + Encoding: icatypes.EncodingProto3JSON, + TxType: icatypes.TxTypeSDKMultiMsg, + })) ) type KeeperTestSuite struct { @@ -47,14 +58,25 @@ func (suite *KeeperTestSuite) SetupTest() { suite.chainC = suite.coordinator.GetChain(ibctesting.GetChainID(3)) } -func NewICAPath(chainA, chainB *ibctesting.TestChain) *ibctesting.Path { +func NewICAPath(chainA, chainB *ibctesting.TestChain, encoding string) *ibctesting.Path { path := ibctesting.NewPath(chainA, chainB) + + var version string + switch encoding { + case icatypes.EncodingProtobuf: + version = TestVersion + case icatypes.EncodingProto3JSON: + version = TestVersionWithJSONEncoding + default: + panic(fmt.Sprintf("unsupported encoding type: %s", encoding)) + } + path.EndpointA.ChannelConfig.PortID = icatypes.HostPortID path.EndpointB.ChannelConfig.PortID = icatypes.HostPortID path.EndpointA.ChannelConfig.Order = channeltypes.ORDERED path.EndpointB.ChannelConfig.Order = channeltypes.ORDERED - path.EndpointA.ChannelConfig.Version = TestVersion - path.EndpointB.ChannelConfig.Version = TestVersion + path.EndpointA.ChannelConfig.Version = version + path.EndpointB.ChannelConfig.Version = version return path } @@ -85,7 +107,7 @@ func RegisterInterchainAccount(endpoint *ibctesting.Endpoint, owner string) erro channelSequence := endpoint.Chain.App.GetIBCKeeper().ChannelKeeper.GetNextChannelSequence(endpoint.Chain.GetContext()) - if err := endpoint.Chain.GetSimApp().ICAControllerKeeper.RegisterInterchainAccount(endpoint.Chain.GetContext(), endpoint.ConnectionID, owner, TestVersion); err != nil { + if err := endpoint.Chain.GetSimApp().ICAControllerKeeper.RegisterInterchainAccount(endpoint.Chain.GetContext(), endpoint.ConnectionID, owner, endpoint.ChannelConfig.Version); err != nil { return err } @@ -106,7 +128,7 @@ func TestKeeperTestSuite(t *testing.T) { func (suite *KeeperTestSuite) TestGetInterchainAccountAddress() { suite.SetupTest() - path := NewICAPath(suite.chainA, suite.chainB) + path := NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) @@ -131,7 +153,7 @@ func (suite *KeeperTestSuite) TestGetAllActiveChannels() { suite.SetupTest() - path := NewICAPath(suite.chainA, suite.chainB) + path := NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) @@ -165,7 +187,7 @@ func (suite *KeeperTestSuite) TestGetAllInterchainAccounts() { suite.SetupTest() - path := NewICAPath(suite.chainA, suite.chainB) + path := NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) @@ -197,7 +219,7 @@ func (suite *KeeperTestSuite) TestGetAllInterchainAccounts() { func (suite *KeeperTestSuite) TestIsActiveChannel() { suite.SetupTest() - path := NewICAPath(suite.chainA, suite.chainB) + path := NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProtobuf) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) @@ -220,6 +242,17 @@ func (suite *KeeperTestSuite) TestSetInterchainAccountAddress() { suite.Require().Equal(expectedAccAddr, retrievedAddr) } +func (suite *KeeperTestSuite) TestMetadataNotFound() { + var ( + invalidPortID = "invalid-port" + invalidChannelID = "invalid-channel" + ) + + _, err := suite.chainB.GetSimApp().ICAHostKeeper.GetAppMetadata(suite.chainB.GetContext(), invalidPortID, invalidChannelID) + suite.Require().ErrorIs(err, ibcerrors.ErrNotFound) + suite.Require().Contains(err.Error(), fmt.Sprintf("app version not found for port %s and channel %s", invalidPortID, invalidChannelID)) +} + func (suite *KeeperTestSuite) TestParams() { expParams := types.DefaultParams() diff --git a/modules/apps/27-interchain-accounts/host/keeper/relay.go b/modules/apps/27-interchain-accounts/host/keeper/relay.go index b49df0a38b2..74dd7c6f832 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/relay.go +++ b/modules/apps/27-interchain-accounts/host/keeper/relay.go @@ -24,9 +24,14 @@ func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet) ([]byt return nil, errorsmod.Wrapf(icatypes.ErrUnknownDataType, "cannot unmarshal ICS-27 interchain account packet data") } + metadata, err := k.getAppMetadata(ctx, packet.DestinationPort, packet.DestinationChannel) + if err != nil { + return nil, err + } + switch data.Type { case icatypes.EXECUTE_TX: - msgs, err := icatypes.DeserializeCosmosTx(k.cdc, data.Data, icatypes.EncodingProtobuf) + msgs, err := icatypes.DeserializeCosmosTx(k.cdc, data.Data, metadata.Encoding) if err != nil { return nil, errorsmod.Wrapf(err, "failed to deserialize interchain account transaction") } diff --git a/modules/apps/27-interchain-accounts/host/keeper/relay_test.go b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go index d829259ecce..d76fe3e5cae 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/relay_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go @@ -1,6 +1,9 @@ package keeper_test import ( + "fmt" + "strings" + "github.com/cosmos/gogoproto/proto" sdkmath "cosmossdk.io/math" @@ -16,12 +19,12 @@ import ( "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" - clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" ibctesting "github.com/cosmos/ibc-go/v7/testing" ) func (suite *KeeperTestSuite) TestOnRecvPacket() { + testedEncodings := []string{icatypes.EncodingProtobuf, icatypes.EncodingProto3JSON} var ( path *ibctesting.Path packetData []byte @@ -29,12 +32,12 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { testCases := []struct { msg string - malleate func() + malleate func(encoding string) expPass bool }{ { "interchain account successfully executes an arbitrary message type using the * (allow all message types) param", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -59,7 +62,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Option: govtypes.OptionYes, } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -76,7 +79,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes banktypes.MsgSend", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -86,7 +89,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -103,7 +106,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes stakingtypes.MsgDelegate", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -114,7 +117,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Amount: sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000)), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -131,7 +134,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes stakingtypes.MsgDelegate and stakingtypes.MsgUndelegate sequentially", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -148,7 +151,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Amount: sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000)), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msgDelegate, msgUndelegate}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msgDelegate, msgUndelegate}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -165,7 +168,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes govtypes.MsgSubmitProposal", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -183,7 +186,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Proposer: interchainAccountAddr, } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -200,7 +203,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes govtypes.MsgVote", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -225,7 +228,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Option: govtypes.OptionYes, } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -242,7 +245,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes disttypes.MsgFundCommunityPool", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -251,7 +254,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Depositor: interchainAccountAddr, } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -268,7 +271,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes disttypes.MsgSetWithdrawAddress", - func() { + func(encoding string) { interchainAccountAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, path.EndpointA.ChannelConfig.PortID) suite.Require().True(found) @@ -277,7 +280,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { WithdrawAddress: suite.chainB.SenderAccount.GetAddress().String(), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -294,7 +297,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "interchain account successfully executes transfertypes.MsgTransfer", - func() { + func(encoding string) { transferPath := ibctesting.NewPath(suite.chainB, suite.chainC) transferPath.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort transferPath.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort @@ -312,11 +315,11 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { Token: sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100)), Sender: interchainAccountAddr, Receiver: suite.chainA.SenderAccount.GetAddress().String(), - TimeoutHeight: clienttypes.NewHeight(1, 100), + TimeoutHeight: suite.chainB.GetTimeoutHeight(), TimeoutTimestamp: uint64(0), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -333,10 +336,10 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "unregistered sdk.Msg", - func() { + func(encoding string) { msg := &banktypes.MsgSendResponse{} - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -353,14 +356,14 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "cannot unmarshal interchain account packet data", - func() { + func(encoding string) { packetData = []byte{} }, false, }, { "cannot deserialize interchain account packet data messages", - func() { + func(encoding string) { data := []byte("invalid packet data") icaPacketData := icatypes.InterchainAccountPacketData{ @@ -374,8 +377,8 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "invalid packet type - UNSPECIFIED", - func() { - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{&banktypes.MsgSend{}}, icatypes.EncodingProtobuf) + func(encoding string) { + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{&banktypes.MsgSend{}}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -389,10 +392,10 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "unauthorised: interchain account not found for controller port ID", - func() { + func(encoding string) { path.EndpointA.ChannelConfig.PortID = "invalid-port-id" - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{&banktypes.MsgSend{}}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{&banktypes.MsgSend{}}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -406,14 +409,14 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "unauthorised: message type not allowed", // NOTE: do not update params to explicitly force the error - func() { + func(encoding string) { msg := &banktypes.MsgSend{ FromAddress: suite.chainB.SenderAccount.GetAddress().String(), ToAddress: suite.chainB.SenderAccount.GetAddress().String(), Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -427,14 +430,14 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, { "unauthorised: signer address is not the interchain account associated with the controller portID", - func() { + func(encoding string) { msg := &banktypes.MsgSend{ FromAddress: suite.chainB.SenderAccount.GetAddress().String(), // unexpected signer ToAddress: suite.chainB.SenderAccount.GetAddress().String(), Amount: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100))), } - data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, icatypes.EncodingProtobuf) + data, err := icatypes.SerializeCosmosTx(suite.chainA.GetSimApp().AppCodec(), []proto.Message{msg}, encoding) suite.Require().NoError(err) icaPacketData := icatypes.InterchainAccountPacketData{ @@ -451,13 +454,363 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { }, } + for _, encoding := range testedEncodings { + for _, tc := range testCases { + tc := tc + + suite.Run(tc.msg, func() { + suite.SetupTest() // reset + + path = NewICAPath(suite.chainA, suite.chainB, encoding) + suite.coordinator.SetupConnections(path) + + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + portID, err := icatypes.NewControllerPortID(TestOwnerAddress) + suite.Require().NoError(err) + + // Get the address of the interchain account stored in state during handshake step + storedAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, portID) + suite.Require().True(found) + + icaAddr, err := sdk.AccAddressFromBech32(storedAddr) + suite.Require().NoError(err) + + // Check if account is created + interchainAccount := suite.chainB.GetSimApp().AccountKeeper.GetAccount(suite.chainB.GetContext(), icaAddr) + suite.Require().Equal(interchainAccount.GetAddress().String(), storedAddr) + + suite.fundICAWallet(suite.chainB.GetContext(), path.EndpointA.ChannelConfig.PortID, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(10000)))) + + tc.malleate(encoding) // malleate mutates test data + + packet := channeltypes.NewPacket( + packetData, + suite.chainA.SenderAccount.GetSequence(), + path.EndpointA.ChannelConfig.PortID, + path.EndpointA.ChannelID, + path.EndpointB.ChannelConfig.PortID, + path.EndpointB.ChannelID, + suite.chainB.GetTimeoutHeight(), + 0, + ) + + txResponse, err := suite.chainB.GetSimApp().ICAHostKeeper.OnRecvPacket(suite.chainB.GetContext(), packet) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().NotNil(txResponse) + } else { + suite.Require().Error(err) + suite.Require().Nil(txResponse) + } + }) + } + } +} + +func (suite *KeeperTestSuite) TestJSONOnRecvPacket() { + var ( + path *ibctesting.Path + packetData []byte + ) + interchainAccountAddr := "cosmos15ulrf36d4wdtrtqzkgaan9ylwuhs7k7qz753uk" + + testCases := []struct { + msg string + malleate func(icaAddress string) + expPass bool + }{ + { + "interchain account successfully executes an arbitrary message type using the * (allow all message types) param", + func(icaAddress string) { + // Populate the gov keeper in advance with an active proposal + testProposal := &govtypes.TextProposal{ + Title: "IBC Gov Proposal", + Description: "tokens for all!", + } + + proposalMsg, err := govv1.NewLegacyContent(testProposal, interchainAccountAddr) + suite.Require().NoError(err) + + proposal, err := govv1.NewProposal([]sdk.Msg{proposalMsg}, govtypes.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr)) + suite.Require().NoError(err) + + suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal) + suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal) + + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1beta1.MsgVote", + "voter": "` + icaAddress + `", + "proposal_id": 1, + "option": 1 + } + ] + }`) + // this is the way cosmwasm encodes byte arrays by default + // golang doesn't use this encoding by default, but it can still deserialize: + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{"*"}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + true, + }, + { + "interchain account successfully executes banktypes.MsgSend", + func(icaAddress string) { + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + icaAddress + `", + "to_address": "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs", + "amount": [{ "denom": "stake", "amount": "100" }] + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*banktypes.MsgSend)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + true, + }, + { + "interchain account successfully executes govtypes.MsgSubmitProposal", + func(icaAddress string) { + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1beta1.MsgSubmitProposal", + "content": { + "@type": "/cosmos.gov.v1beta1.TextProposal", + "title": "IBC Gov Proposal", + "description": "tokens for all!" + }, + "initial_deposit": [{ "denom": "stake", "amount": "5000" }], + "proposer": "` + icaAddress + `" + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*govtypes.MsgSubmitProposal)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + true, + }, + { + "interchain account successfully executes govtypes.MsgVote", + func(icaAddress string) { + // Populate the gov keeper in advance with an active proposal + testProposal := &govtypes.TextProposal{ + Title: "IBC Gov Proposal", + Description: "tokens for all!", + } + + proposalMsg, err := govv1.NewLegacyContent(testProposal, interchainAccountAddr) + suite.Require().NoError(err) + + proposal, err := govv1.NewProposal([]sdk.Msg{proposalMsg}, govtypes.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "description", sdk.AccAddress(interchainAccountAddr)) + suite.Require().NoError(err) + + suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal) + suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal) + + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1beta1.MsgVote", + "voter": "` + icaAddress + `", + "proposal_id": 1, + "option": 1 + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*govtypes.MsgVote)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + true, + }, + { + "interchain account successfully executes govtypes.MsgSubmitProposal, govtypes.MsgDeposit, and then govtypes.MsgVote sequentially", + func(icaAddress string) { + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1beta1.MsgSubmitProposal", + "content": { + "@type": "/cosmos.gov.v1beta1.TextProposal", + "title": "IBC Gov Proposal", + "description": "tokens for all!" + }, + "initial_deposit": [{ "denom": "stake", "amount": "5000" }], + "proposer": "` + icaAddress + `" + }, + { + "@type": "/cosmos.gov.v1beta1.MsgDeposit", + "proposal_id": 1, + "depositor": "` + icaAddress + `", + "amount": [{ "denom": "stake", "amount": "10000000" }] + }, + { + "@type": "/cosmos.gov.v1beta1.MsgVote", + "voter": "` + icaAddress + `", + "proposal_id": 1, + "option": 1 + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*govtypes.MsgSubmitProposal)(nil)), sdk.MsgTypeURL((*govtypes.MsgDeposit)(nil)), sdk.MsgTypeURL((*govtypes.MsgVote)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + true, + }, + { + "interchain account successfully executes transfertypes.MsgTransfer", + func(icaAddress string) { + transferPath := ibctesting.NewPath(suite.chainB, suite.chainC) + transferPath.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + transferPath.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + transferPath.EndpointA.ChannelConfig.Version = transfertypes.Version + transferPath.EndpointB.ChannelConfig.Version = transfertypes.Version + + suite.coordinator.Setup(transferPath) + + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/ibc.applications.transfer.v1.MsgTransfer", + "source_port": "transfer", + "source_channel": "channel-1", + "token": { "denom": "stake", "amount": "100" }, + "sender": "` + icaAddress + `", + "receiver": "cosmos15ulrf36d4wdtrtqzkgaan9ylwuhs7k7qz753uk", + "timeout_height": { "revision_number": 1, "revision_height": 100 }, + "timeout_timestamp": 0 + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*transfertypes.MsgTransfer)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + true, + }, + { + "unregistered sdk.Msg", + func(icaAddress string) { + msgBytes := []byte(`{"messages":[{}]}`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{"*"}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + false, + }, + { + "message type not allowed banktypes.MsgSend", + func(icaAddress string) { + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + icaAddress + `", + "to_address": "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs", + "amount": [{ "denom": "stake", "amount": "100" }] + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*transfertypes.MsgTransfer)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + false, + }, + { + "unauthorised: signer address is not the interchain account associated with the controller portID", + func(icaAddress string) { + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + ibctesting.InvalidID + `", + "to_address": "cosmos17dtl0mjt3t77kpuhg2edqzjpszulwhgzuj9ljs", + "amount": [{ "denom": "stake", "amount": "100" }] + } + ] + }`) + byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{sdk.MsgTypeURL((*banktypes.MsgSend)(nil))}) + suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params) + }, + false, + }, + } + for _, tc := range testCases { tc := tc suite.Run(tc.msg, func() { suite.SetupTest() // reset - path = NewICAPath(suite.chainA, suite.chainB) + path = NewICAPath(suite.chainA, suite.chainB, icatypes.EncodingProto3JSON) suite.coordinator.SetupConnections(path) err := SetupICAPath(path, TestOwnerAddress) @@ -467,19 +820,12 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { suite.Require().NoError(err) // Get the address of the interchain account stored in state during handshake step - storedAddr, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, portID) + icaAddress, found := suite.chainB.GetSimApp().ICAHostKeeper.GetInterchainAccountAddress(suite.chainB.GetContext(), ibctesting.FirstConnectionID, portID) suite.Require().True(found) - icaAddr, err := sdk.AccAddressFromBech32(storedAddr) - suite.Require().NoError(err) - - // Check if account is created - interchainAccount := suite.chainB.GetSimApp().AccountKeeper.GetAccount(suite.chainB.GetContext(), icaAddr) - suite.Require().Equal(interchainAccount.GetAddress().String(), storedAddr) - - suite.fundICAWallet(suite.chainB.GetContext(), path.EndpointA.ChannelConfig.PortID, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(10000)))) + suite.fundICAWallet(suite.chainB.GetContext(), path.EndpointA.ChannelConfig.PortID, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000)))) - tc.malleate() // malleate mutates test data + tc.malleate(icaAddress) // malleate mutates test data packet := channeltypes.NewPacket( packetData, @@ -488,7 +834,7 @@ func (suite *KeeperTestSuite) TestOnRecvPacket() { path.EndpointA.ChannelID, path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, - clienttypes.NewHeight(1, 100), + suite.chainB.GetTimeoutHeight(), 0, ) diff --git a/modules/apps/27-interchain-accounts/types/codec.go b/modules/apps/27-interchain-accounts/types/codec.go index d8ba2d05433..68d19fb0db3 100644 --- a/modules/apps/27-interchain-accounts/types/codec.go +++ b/modules/apps/27-interchain-accounts/types/codec.go @@ -26,14 +26,18 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { } // SerializeCosmosTx serializes a slice of sdk.Msg's using the CosmosTx type. The sdk.Msg's are -// packed into Any's and inserted into the Messages field of a CosmosTx. The proto marshaled CosmosTx -// bytes are returned. Only the ProtoCodec is supported for serializing messages. -func SerializeCosmosTx(cdc codec.BinaryCodec, msgs []proto.Message, encoding string) (bz []byte, err error) { - // only ProtoCodec is supported +// packed into Any's and inserted into the Messages field of a CosmosTx. The CosmosTx is marshaled +// depending on the encoding type passed in. The marshaled bytes are returned. Only the ProtoCodec +// is supported for serializing messages. Both protobuf and proto3 JSON are supported. +func SerializeCosmosTx(cdc codec.Codec, msgs []proto.Message, encoding string) ([]byte, error) { + // this is a defensive check to ensure only the ProtoCodec is used for message serialization if _, ok := cdc.(*codec.ProtoCodec); !ok { - return nil, errorsmod.Wrap(ErrInvalidCodec, "only ProtoCodec is supported for receiving messages on the host chain") + return nil, errorsmod.Wrap(ErrInvalidCodec, "only the ProtoCodec may be used for receiving messages on the host chain") } + var bz []byte + var err error + msgAnys := make([]*codectypes.Any, len(msgs)) for i, msg := range msgs { @@ -47,38 +51,57 @@ func SerializeCosmosTx(cdc codec.BinaryCodec, msgs []proto.Message, encoding str Messages: msgAnys, } - bz, err = cdc.Marshal(cosmosTx) - if err != nil { - return nil, err + switch encoding { + case EncodingProtobuf: + bz, err = cdc.Marshal(cosmosTx) + if err != nil { + return nil, errorsmod.Wrapf(err, "cannot marshal CosmosTx with protobuf") + } + case EncodingProto3JSON: + bz, err = cdc.MarshalJSON(cosmosTx) + if err != nil { + return nil, errorsmod.Wrapf(ErrUnknownDataType, "cannot marshal CosmosTx with proto3 json") + } + default: + return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding) } return bz, nil } -// DeserializeCosmosTx unmarshals and unpacks a slice of transaction bytes -// into a slice of sdk.Msg's. Only the ProtoCodec is supported for message -// deserialization. -func DeserializeCosmosTx(cdc codec.BinaryCodec, data []byte, encoding string) ([]sdk.Msg, error) { - // only ProtoCodec is supported +// DeserializeCosmosTx unmarshals and unpacks a slice of transaction bytes into a slice of sdk.Msg's. +// The transaction bytes are unmarshaled depending on the encoding type passed in. The sdk.Msg's are +// unpacked from Any's and returned. Only the ProtoCodec is supported for serializing messages. Both +// protobuf and proto3 JSON are supported. +func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.Msg, error) { + // this is a defensive check to ensure only the ProtoCodec is used for message deserialization if _, ok := cdc.(*codec.ProtoCodec); !ok { - return nil, errorsmod.Wrap(ErrInvalidCodec, "only ProtoCodec is supported for receiving messages on the host chain") + return nil, errorsmod.Wrap(ErrInvalidCodec, "only the ProtoCodec may be used for receiving messages on the host chain") } var cosmosTx CosmosTx - if err := cdc.Unmarshal(data, &cosmosTx); err != nil { - return nil, err + + switch encoding { + case EncodingProtobuf: + if err := cdc.Unmarshal(data, &cosmosTx); err != nil { + return nil, errorsmod.Wrapf(err, "cannot unmarshal CosmosTx with protobuf") + } + case EncodingProto3JSON: + if err := cdc.UnmarshalJSON(data, &cosmosTx); err != nil { + return nil, errorsmod.Wrapf(ErrUnknownDataType, "cannot unmarshal CosmosTx with proto3 json") + } + default: + return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding) } msgs := make([]sdk.Msg, len(cosmosTx.Messages)) for i, protoAny := range cosmosTx.Messages { var msg sdk.Msg - err := cdc.UnpackAny(protoAny, &msg) if err != nil { return nil, err } - msgs[i] = msg } diff --git a/modules/apps/27-interchain-accounts/types/codec_test.go b/modules/apps/27-interchain-accounts/types/codec_test.go index 886fb21a77c..e120c393c16 100644 --- a/modules/apps/27-interchain-accounts/types/codec_test.go +++ b/modules/apps/27-interchain-accounts/types/codec_test.go @@ -6,9 +6,12 @@ import ( sdkmath "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/ibc-go/v7/modules/apps/27-interchain-accounts/types" "github.com/cosmos/ibc-go/v7/testing/simapp" @@ -40,14 +43,316 @@ func (mockSdkMsg) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{} } +// TestSerializeAndDeserializeCosmosTx tests the SerializeCosmosTx and DeserializeCosmosTx functions +// for all supported encoding types. +// +// expPass set to false means that: +// - the test case is expected to fail on deserialization for protobuf encoding. +// - the test case is expected to fail on serialization for proto3 json encoding. func (suite *TypesTestSuite) TestSerializeAndDeserializeCosmosTx() { + testedEncodings := []string{types.EncodingProtobuf, types.EncodingProto3JSON} + // each test case will have a corresponding expected errors in case of failures: + expSerializeErrorStrings := make([]string, len(testedEncodings)) + expDeserializeErrorStrings := make([]string, len(testedEncodings)) + + var msgs []proto.Message testCases := []struct { - name string - msgs []proto.Message - expPass bool + name string + malleate func() + expPass bool }{ { "single msg", + func() { + msgs = []proto.Message{ + &banktypes.MsgSend{ + FromAddress: TestOwnerAddress, + ToAddress: TestOwnerAddress, + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), + }, + } + }, + true, + }, + { + "multiple msgs, same types", + func() { + msgs = []proto.Message{ + &banktypes.MsgSend{ + FromAddress: TestOwnerAddress, + ToAddress: TestOwnerAddress, + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), + }, + &banktypes.MsgSend{ + FromAddress: TestOwnerAddress, + ToAddress: TestOwnerAddress, + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(200))), + }, + } + }, + true, + }, + { + "success: multiple msgs, different types", + func() { + msgs = []proto.Message{ + &banktypes.MsgSend{ + FromAddress: TestOwnerAddress, + ToAddress: TestOwnerAddress, + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), + }, + &stakingtypes.MsgDelegate{ + DelegatorAddress: TestOwnerAddress, + ValidatorAddress: TestOwnerAddress, + Amount: sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000)), + }, + } + }, + true, + }, + { + "success: msg with nested any", + func() { + testProposal := &govtypes.TextProposal{ + Title: "IBC Gov Proposal", + Description: "tokens for all!", + } + content, err := codectypes.NewAnyWithValue(testProposal) + suite.Require().NoError(err) + + msgs = []proto.Message{ + &govtypes.MsgSubmitProposal{ + Content: content, + InitialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000))), + Proposer: TestOwnerAddress, + }, + } + }, + true, + }, + { + "success: msg with nested array of any", + func() { + sendMsg := &banktypes.MsgSend{ + FromAddress: TestOwnerAddress, + ToAddress: TestOwnerAddress, + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), + } + sendAny, err := codectypes.NewAnyWithValue(sendMsg) + suite.Require().NoError(err) + + testProposal := &govtypes.TextProposal{ + Title: "IBC Gov Proposal", + Description: "tokens for all!", + } + content, err := codectypes.NewAnyWithValue(testProposal) + suite.Require().NoError(err) + legacyPropMsg := &govtypes.MsgSubmitProposal{ + Content: content, + InitialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000))), + Proposer: TestOwnerAddress, + } + legacyPropAny, err := codectypes.NewAnyWithValue(legacyPropMsg) + suite.Require().NoError(err) + + delegateMsg := &stakingtypes.MsgDelegate{ + DelegatorAddress: TestOwnerAddress, + ValidatorAddress: TestOwnerAddress, + Amount: sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000)), + } + delegateAny, err := codectypes.NewAnyWithValue(delegateMsg) + suite.Require().NoError(err) + + messages := []*codectypes.Any{sendAny, legacyPropAny, delegateAny} + + propMsg := &govtypesv1.MsgSubmitProposal{ + Messages: messages, + InitialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000))), + Proposer: TestOwnerAddress, + Metadata: "", + Title: "New IBC Gov Proposal", + Summary: "more tokens for all!", + } + + msgs = []proto.Message{propMsg} + }, + true, + }, + { + "success: empty messages", + func() { + msgs = []proto.Message{} + }, + true, + }, + { + "failure: unregistered msg type", + func() { + msgs = []proto.Message{ + &mockSdkMsg{}, + } + + expSerializeErrorStrings = []string{"NO_ERROR_EXPECTED", "cannot marshal CosmosTx with proto3 json"} + expDeserializeErrorStrings = []string{"cannot unmarshal CosmosTx with protobuf", "cannot unmarshal CosmosTx with proto3 json"} + }, + false, + }, + { + "failure: multiple unregistered msg types", + func() { + msgs = []proto.Message{ + &mockSdkMsg{}, + &mockSdkMsg{}, + &mockSdkMsg{}, + } + + expSerializeErrorStrings = []string{"NO_ERROR_EXPECTED", "cannot marshal CosmosTx with proto3 json"} + expDeserializeErrorStrings = []string{"cannot unmarshal CosmosTx with protobuf", "cannot unmarshal CosmosTx with proto3 json"} + }, + false, + }, + { + "failure: nested unregistered msg", + func() { + mockMsg := &mockSdkMsg{} + mockAny, err := codectypes.NewAnyWithValue(mockMsg) + suite.Require().NoError(err) + + msgs = []proto.Message{ + &govtypes.MsgSubmitProposal{ + Content: mockAny, + InitialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000))), + Proposer: TestOwnerAddress, + }, + } + + expSerializeErrorStrings = []string{"NO_ERROR_EXPECTED", "cannot marshal CosmosTx with proto3 json"} + expDeserializeErrorStrings = []string{"cannot unmarshal CosmosTx with protobuf", "cannot unmarshal CosmosTx with proto3 json"} + }, + false, + }, + { + "failure: nested array of unregistered msg", + func() { + mockMsg := &mockSdkMsg{} + mockAny, err := codectypes.NewAnyWithValue(mockMsg) + suite.Require().NoError(err) + + messages := []*codectypes.Any{mockAny, mockAny, mockAny} + + propMsg := &govtypesv1.MsgSubmitProposal{ + Messages: messages, + InitialDeposit: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000))), + Proposer: TestOwnerAddress, + Metadata: "", + Title: "New IBC Gov Proposal", + Summary: "more tokens for all!", + } + + msgs = []proto.Message{propMsg} + + expSerializeErrorStrings = []string{"NO_ERROR_EXPECTED", "cannot marshal CosmosTx with proto3 json"} + expDeserializeErrorStrings = []string{"cannot unmarshal CosmosTx with protobuf", "cannot unmarshal CosmosTx with proto3 json"} + }, + false, + }, + } + + for i, encoding := range testedEncodings { + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + tc.malleate() + + bz, err := types.SerializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, msgs, encoding) + if encoding == types.EncodingProto3JSON && !tc.expPass { + suite.Require().Error(err, tc.name) + suite.Require().Contains(err.Error(), expSerializeErrorStrings[1], tc.name) + } else { + suite.Require().NoError(err, tc.name) + } + + deserializedMsgs, err := types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, bz, encoding) + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + suite.Require().Contains(err.Error(), expDeserializeErrorStrings[i], tc.name) + } + + if tc.expPass { + for i, msg := range msgs { + // We're using proto.CompactTextString() for comparison instead of suite.Require().Equal() or proto.Equal() + // for two main reasons: + // + // 1. When deserializing from JSON, the `Any` type has private fields and cached values + // that do not match the original message, causing equality checks to fail. + // + // 2. proto.Equal() does not have built-in support for comparing sdk's math.Int types. + // + // Using proto.CompactTextString() mitigates these issues by focusing on serialized string representation, + // rather than internal details of the types. + suite.Require().Equal(proto.CompactTextString(msg), proto.CompactTextString(deserializedMsgs[i])) + } + } + }) + } + + // test serializing non sdk.Msg type + bz, err := types.SerializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, []proto.Message{&banktypes.MsgSendResponse{}}, encoding) + suite.Require().NoError(err) + suite.Require().NotEmpty(bz) + + // test deserializing unknown bytes + msgs, err := types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, bz, encoding) + suite.Require().Error(err) // unregistered type + suite.Require().Contains(err.Error(), expDeserializeErrorStrings[i]) + suite.Require().Empty(msgs) + + // test deserializing unknown bytes + msgs, err = types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, []byte("invalid"), encoding) + suite.Require().Error(err) + suite.Require().Contains(err.Error(), expDeserializeErrorStrings[i]) + suite.Require().Empty(msgs) + } +} + +// unregistered bytes causes amino to panic. +// test that DeserializeCosmosTx gracefully returns an error on +// unsupported amino codec. +func (suite *TypesTestSuite) TestProtoDeserializeAndSerializeCosmosTxWithAmino() { + cdc := codec.NewLegacyAmino() + marshaler := codec.NewAminoCodec(cdc) + + msgs, err := types.SerializeCosmosTx(marshaler, []proto.Message{&banktypes.MsgSend{}}, types.EncodingProtobuf) + suite.Require().ErrorIs(err, types.ErrInvalidCodec) + suite.Require().Empty(msgs) + + bz, err := types.DeserializeCosmosTx(marshaler, []byte{0x10, 0}, types.EncodingProtobuf) + suite.Require().ErrorIs(err, types.ErrInvalidCodec) + suite.Require().Empty(bz) +} + +func (suite *TypesTestSuite) TestJSONDeserializeCosmosTx() { + testCases := []struct { + name string + jsonBytes []byte + expMsgs []proto.Message + expError error + }{ + { + "success: single msg", + []byte(`{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + TestOwnerAddress + `", + "to_address": "` + TestOwnerAddress + `", + "amount": [{ "denom": "bananas", "amount": "100" }] + } + ] + }`), []proto.Message{ &banktypes.MsgSend{ FromAddress: TestOwnerAddress, @@ -55,10 +360,26 @@ func (suite *TypesTestSuite) TestSerializeAndDeserializeCosmosTx() { Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), }, }, - true, + nil, }, { - "multiple msgs, same types", + "success: multiple msgs, same types", + []byte(`{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + TestOwnerAddress + `", + "to_address": "` + TestOwnerAddress + `", + "amount": [{ "denom": "bananas", "amount": "100" }] + }, + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + TestOwnerAddress + `", + "to_address": "` + TestOwnerAddress + `", + "amount": [{ "denom": "bananas", "amount": "100" }] + } + ] + }`), []proto.Message{ &banktypes.MsgSend{ FromAddress: TestOwnerAddress, @@ -68,41 +389,66 @@ func (suite *TypesTestSuite) TestSerializeAndDeserializeCosmosTx() { &banktypes.MsgSend{ FromAddress: TestOwnerAddress, ToAddress: TestOwnerAddress, - Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(200))), + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), }, }, - true, + nil, }, { - "multiple msgs, different types", + "success: multiple msgs, different types", + []byte(`{ + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "` + TestOwnerAddress + `", + "to_address": "` + TestOwnerAddress + `", + "amount": [{ "denom": "bananas", "amount": "100" }] + }, + { + "@type": "/cosmos.staking.v1beta1.MsgDelegate", + "delegator_address": "` + TestOwnerAddress + `", + "validator_address": "` + TestOwnerAddress + `", + "amount": { "denom": "stake", "amount": "5000" } + } + ] + }`), []proto.Message{ &banktypes.MsgSend{ FromAddress: TestOwnerAddress, ToAddress: TestOwnerAddress, Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), }, - &govtypes.MsgSubmitProposal{ - InitialDeposit: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), - Proposer: TestOwnerAddress, + &stakingtypes.MsgDelegate{ + DelegatorAddress: TestOwnerAddress, + ValidatorAddress: TestOwnerAddress, + Amount: sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(5000)), }, }, - true, + nil, }, { - "unregistered msg type", + "failure: unregistered msg type", + []byte(`{"messages":[{}]}`), []proto.Message{ &mockSdkMsg{}, }, - false, + types.ErrUnknownDataType, }, { - "multiple unregistered msg types", + "failure: multiple unregistered msg types", + []byte(`{"messages":[{},{},{}]}`), []proto.Message{ &mockSdkMsg{}, &mockSdkMsg{}, &mockSdkMsg{}, }, - false, + types.ErrUnknownDataType, + }, + { + "failure: empty bytes", + []byte{}, + nil, + types.ErrUnknownDataType, }, } @@ -110,49 +456,39 @@ func (suite *TypesTestSuite) TestSerializeAndDeserializeCosmosTx() { tc := tc suite.Run(tc.name, func() { - bz, err := types.SerializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, tc.msgs, types.EncodingProtobuf) - suite.Require().NoError(err, tc.name) - - msgs, err := types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, bz, types.EncodingProtobuf) - if tc.expPass { - suite.Require().NoError(err, tc.name) + msgs, errDeserialize := types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, tc.jsonBytes, types.EncodingProto3JSON) + if tc.expError == nil { + suite.Require().NoError(errDeserialize, tc.name) + for i, msg := range msgs { + suite.Require().Equal(tc.expMsgs[i], msg) + } } else { - suite.Require().Error(err, tc.name) - } - - for i, msg := range msgs { - suite.Require().Equal(tc.msgs[i], msg) + suite.Require().ErrorIs(errDeserialize, tc.expError, tc.name) } }) } +} - // test serializing non sdk.Msg type - bz, err := types.SerializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, []proto.Message{&banktypes.MsgSendResponse{}}, types.EncodingProtobuf) - suite.Require().NoError(err) - suite.Require().NotEmpty(bz) - - // test deserializing unknown bytes - _, err = types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, bz, types.EncodingProtobuf) - suite.Require().Error(err) // unregistered type +func (suite *TypesTestSuite) TestUnsupportedEncodingType() { + msgs := []proto.Message{ + &banktypes.MsgSend{ + FromAddress: TestOwnerAddress, + ToAddress: TestOwnerAddress, + Amount: sdk.NewCoins(sdk.NewCoin("bananas", sdkmath.NewInt(100))), + }, + } - // test deserializing unknown bytes - msgs, err := types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, []byte("invalid"), types.EncodingProtobuf) - suite.Require().Error(err) - suite.Require().Empty(msgs) -} + bz, err := types.SerializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, msgs, "unsupported") + suite.Require().ErrorIs(err, types.ErrInvalidCodec) + suite.Require().Nil(bz) -// unregistered bytes causes amino to panic. -// test that DeserializeCosmosTx gracefully returns an error on -// unsupported amino codec. -func (suite *TypesTestSuite) TestDeserializeAndSerializeCosmosTxWithAmino() { - cdc := codec.NewLegacyAmino() - marshaler := codec.NewAminoCodec(cdc) + data, err := types.SerializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, msgs, types.EncodingProtobuf) + suite.Require().NoError(err) - msgs, err := types.SerializeCosmosTx(marshaler, []proto.Message{&banktypes.MsgSend{}}, types.EncodingProtobuf) - suite.Require().Error(err) - suite.Require().Empty(msgs) + _, err = types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, data, "unsupported") + suite.Require().ErrorIs(err, types.ErrInvalidCodec) - bz, err := types.DeserializeCosmosTx(marshaler, []byte{0x10, 0}, types.EncodingProtobuf) - suite.Require().Error(err) - suite.Require().Empty(bz) + // verify that protobuf encoding still works otherwise: + _, err = types.DeserializeCosmosTx(simapp.MakeTestEncodingConfig().Codec, data, types.EncodingProtobuf) + suite.Require().NoError(err) } diff --git a/modules/apps/27-interchain-accounts/types/metadata.go b/modules/apps/27-interchain-accounts/types/metadata.go index 6cf6a27688f..3df2028d9b1 100644 --- a/modules/apps/27-interchain-accounts/types/metadata.go +++ b/modules/apps/27-interchain-accounts/types/metadata.go @@ -11,6 +11,8 @@ import ( const ( // EncodingProtobuf defines the protocol buffers proto3 encoding format EncodingProtobuf = "proto3" + // EncodingProto3JSON defines the proto3 JSON encoding format + EncodingProto3JSON = "proto3json" // TxTypeSDKMultiMsg defines the multi message transaction type supported by the Cosmos SDK TxTypeSDKMultiMsg = "sdk_multi_msg" @@ -142,7 +144,7 @@ func isSupportedEncoding(encoding string) bool { // getSupportedEncoding returns a string slice of supported encoding formats func getSupportedEncoding() []string { - return []string{EncodingProtobuf} + return []string{EncodingProtobuf, EncodingProto3JSON} } // isSupportedTxType returns true if the provided transaction type is supported, otherwise false diff --git a/modules/apps/27-interchain-accounts/types/metadata_test.go b/modules/apps/27-interchain-accounts/types/metadata_test.go index c0274368e07..04ef4342e9f 100644 --- a/modules/apps/27-interchain-accounts/types/metadata_test.go +++ b/modules/apps/27-interchain-accounts/types/metadata_test.go @@ -45,7 +45,7 @@ func (suite *TypesTestSuite) TestIsPreviousMetadataEqual() { false, }, { - "unequal encoding format", + "unequal and invalid encoding format", func() { metadata.Encoding = "invalid-encoding-format" @@ -55,6 +55,17 @@ func (suite *TypesTestSuite) TestIsPreviousMetadataEqual() { }, false, }, + { + "unequal encoding format", + func() { + metadata.Encoding = types.EncodingProto3JSON + + versionBytes, err := types.ModuleCdc.MarshalJSON(&metadata) + suite.Require().NoError(err) + previousVersion = string(versionBytes) + }, + false, + }, { "unequal transaction type", func() { @@ -152,6 +163,20 @@ func (suite *TypesTestSuite) TestValidateControllerMetadata() { }, true, }, + { + "success with EncodingProto3JSON", + func() { + metadata = types.Metadata{ + Version: types.Version, + ControllerConnectionId: ibctesting.FirstConnectionID, + HostConnectionId: ibctesting.FirstConnectionID, + Address: TestOwnerAddress, + Encoding: types.EncodingProto3JSON, + TxType: types.TxTypeSDKMultiMsg, + } + }, + true, + }, { "unsupported encoding format", func() { @@ -293,6 +318,20 @@ func (suite *TypesTestSuite) TestValidateHostMetadata() { }, true, }, + { + "success with EncodingProto3JSON", + func() { + metadata = types.Metadata{ + Version: types.Version, + ControllerConnectionId: ibctesting.FirstConnectionID, + HostConnectionId: ibctesting.FirstConnectionID, + Address: TestOwnerAddress, + Encoding: types.EncodingProto3JSON, + TxType: types.TxTypeSDKMultiMsg, + } + }, + true, + }, { "unsupported encoding format", func() {