From 37ffee47f296b05872696e002a974fad783fd40a Mon Sep 17 00:00:00 2001 From: srdtrk Date: Tue, 16 Dec 2025 23:28:59 +0400 Subject: [PATCH 1/5] imp: add extra validation --- .../27-interchain-accounts/types/codec.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/apps/27-interchain-accounts/types/codec.go b/modules/apps/27-interchain-accounts/types/codec.go index f08bd3df50e..59447e2d792 100644 --- a/modules/apps/27-interchain-accounts/types/codec.go +++ b/modules/apps/27-interchain-accounts/types/codec.go @@ -1,6 +1,9 @@ package types import ( + "encoding/json" + "reflect" + "github.com/cosmos/gogoproto/proto" errorsmod "cosmossdk.io/errors" @@ -92,6 +95,15 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M if err := cdc.UnmarshalJSON(data, &cosmosTx); err != nil { return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot unmarshal CosmosTx with proto3 json: %v", err) } + reconstructedData, err := cdc.MarshalJSON(&cosmosTx) + if err != nil { + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot remarshal CosmosTx with proto3 json: %v", err) + } + if isEqual, err := equalJSON(data, reconstructedData); err != nil { + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot compare original and reconstructed JSON: %v", err) + } else if !isEqual { + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "original and reconstructed JSON do not match") + } default: return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding) } @@ -109,3 +121,10 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M return msgs, nil } + +func equalJSON(a, b []byte) (bool, error) { + var x, y any + if err := json.Unmarshal(a, &x); err != nil { return false, err } + if err := json.Unmarshal(b, &y); err != nil { return false, err } + return reflect.DeepEqual(x, y), nil +} From 022e13ad490446a59282a0caec08f890e54fe160 Mon Sep 17 00:00:00 2001 From: srdtrk Date: Tue, 16 Dec 2025 23:34:18 +0400 Subject: [PATCH 2/5] chore: lint --- modules/apps/27-interchain-accounts/types/codec.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/apps/27-interchain-accounts/types/codec.go b/modules/apps/27-interchain-accounts/types/codec.go index 59447e2d792..bea6b95aa52 100644 --- a/modules/apps/27-interchain-accounts/types/codec.go +++ b/modules/apps/27-interchain-accounts/types/codec.go @@ -123,8 +123,12 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M } func equalJSON(a, b []byte) (bool, error) { - var x, y any - if err := json.Unmarshal(a, &x); err != nil { return false, err } - if err := json.Unmarshal(b, &y); err != nil { return false, err } - return reflect.DeepEqual(x, y), nil + var x, y any + if err := json.Unmarshal(a, &x); err != nil { + return false, err + } + if err := json.Unmarshal(b, &y); err != nil { + return false, err + } + return reflect.DeepEqual(x, y), nil } From 9c11ccc39323745488cd63963f6b467dbb65bda9 Mon Sep 17 00:00:00 2001 From: srdtrk Date: Wed, 17 Dec 2025 12:17:21 +0400 Subject: [PATCH 3/5] imp: error msg --- modules/apps/27-interchain-accounts/types/codec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/apps/27-interchain-accounts/types/codec.go b/modules/apps/27-interchain-accounts/types/codec.go index bea6b95aa52..cec30d9f924 100644 --- a/modules/apps/27-interchain-accounts/types/codec.go +++ b/modules/apps/27-interchain-accounts/types/codec.go @@ -102,7 +102,7 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M if isEqual, err := equalJSON(data, reconstructedData); err != nil { return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot compare original and reconstructed JSON: %v", err) } else if !isEqual { - return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "original and reconstructed JSON do not match") + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "original and reconstructed JSON do not match, all optional fields must be explicitly set") } default: return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding) From 72b696369314e53f60864f0a5cfe4b1387405b46 Mon Sep 17 00:00:00 2001 From: srdtrk Date: Wed, 17 Dec 2025 13:31:57 +0400 Subject: [PATCH 4/5] test: fix tests --- .../host/keeper/relay_test.go | 151 +++++++++++++++--- .../27-interchain-accounts/types/codec.go | 2 +- 2 files changed, 133 insertions(+), 20 deletions(-) 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 3f29224d2b8..ab5e352d285 100644 --- a/modules/apps/27-interchain-accounts/host/keeper/relay_test.go +++ b/modules/apps/27-interchain-accounts/host/keeper/relay_test.go @@ -566,7 +566,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { expErr error }{ { - "interchain account successfully executes an arbitrary message type using the * (allow all message types) param", + "success: interchain account successfully executes an arbitrary message type using the * (allow all message types) param", func(icaAddress string) { proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, s.chainA.GetContext().BlockTime(), s.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false) s.Require().NoError(err) @@ -581,8 +581,9 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { { "@type": "/cosmos.gov.v1.MsgVote", "voter": "` + icaAddress + `", - "proposal_id": 1, - "option": 1 + "proposal_id": "1", + "option": "VOTE_OPTION_YES", + "metadata": "" } ] }`) @@ -601,7 +602,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { nil, }, { - "interchain account successfully executes banktypes.MsgSend", + "success: interchain account successfully executes banktypes.MsgSend", func(icaAddress string) { msgBytes := []byte(`{ "messages": [ @@ -626,7 +627,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { nil, }, { - "interchain account successfully executes govtypesv1.MsgSubmitProposal", + "success: interchain account successfully executes govtypesv1.MsgSubmitProposal", func(icaAddress string) { msgBytes := []byte(`{ "messages": [ @@ -655,7 +656,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { nil, }, { - "interchain account successfully executes govtypesv1.MsgVote", + "success: interchain account successfully executes govtypesv1.MsgVote", func(icaAddress string) { proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, s.chainA.GetContext().BlockTime(), s.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false) s.Require().NoError(err) @@ -670,8 +671,9 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { { "@type": "/cosmos.gov.v1.MsgVote", "voter": "` + icaAddress + `", - "proposal_id": 1, - "option": 1 + "proposal_id": "1", + "option": "VOTE_OPTION_YES", + "metadata": "" } ] }`) @@ -688,7 +690,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { nil, }, { - "interchain account successfully executes govtypesv1.MsgSubmitProposal, govtypesv1.MsgDeposit, and then govtypesv1.MsgVote sequentially", + "success: interchain account successfully executes govtypesv1.MsgSubmitProposal, govtypesv1.MsgDeposit, and then govtypesv1.MsgVote sequentially", func(icaAddress string) { msgBytes := []byte(`{ "messages": [ @@ -704,15 +706,16 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { }, { "@type": "/cosmos.gov.v1.MsgDeposit", - "proposal_id": 1, + "proposal_id": "1", "depositor": "` + icaAddress + `", "amount": [{ "denom": "stake", "amount": "10000000" }] }, { "@type": "/cosmos.gov.v1.MsgVote", "voter": "` + icaAddress + `", - "proposal_id": 1, - "option": 1 + "proposal_id": "1", + "option": "VOTE_OPTION_YES", + "metadata": "" } ] }`) @@ -729,7 +732,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { nil, }, { - "interchain account successfully executes transfertypes.MsgTransfer", + "success: interchain account successfully executes transfertypes.MsgTransfer", func(icaAddress string) { transferPath := ibctesting.NewTransferPath(s.chainB, s.chainC) @@ -744,9 +747,12 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { "token": { "denom": "stake", "amount": "100" }, "sender": "` + icaAddress + `", "receiver": "cosmos15ulrf36d4wdtrtqzkgaan9ylwuhs7k7qz753uk", - "timeout_height": { "revision_number": 1, "revision_height": 100 }, - "timeout_timestamp": 0, - "memo": "" + "timeout_height": { "revision_number": "1", "revision_height": "100" }, + "timeout_timestamp": "0", + "memo": "", + "encoding": "", + "use_aliasing": false + } ] }`) @@ -763,7 +769,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { nil, }, { - "unregistered sdk.Msg", + "failure: unregistered sdk.Msg", func(icaAddress string) { msgBytes := []byte(`{"messages":[{}]}`) byteArrayString := strings.Join(strings.Fields(fmt.Sprint(msgBytes)), ",") //nolint:staticcheck @@ -779,7 +785,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { ibcerrors.ErrInvalidType, }, { - "message type not allowed banktypes.MsgSend", + "failure: message type not allowed banktypes.MsgSend", func(icaAddress string) { msgBytes := []byte(`{ "messages": [ @@ -804,7 +810,7 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { ibcerrors.ErrUnauthorized, }, { - "unauthorised: signer address is not the interchain account associated with the controller portID", + "failure: signer address is not the interchain account associated with the controller portID", func(icaAddress string) { msgBytes := []byte(`{ "messages": [ @@ -828,6 +834,113 @@ func (s *KeeperTestSuite) TestJSONOnRecvPacket() { }, ibcerrors.ErrInvalidType, }, + { + "failure: missing optional metadata field", + func(icaAddress string) { + proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, s.chainA.GetContext().BlockTime(), s.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false) + s.Require().NoError(err) + + err = s.chainB.GetSimApp().GovKeeper.SetProposal(s.chainB.GetContext(), proposal) + s.Require().NoError(err) + err = s.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(s.chainB.GetContext(), proposal) + s.Require().NoError(err) + + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1.MsgVote", + "voter": "` + icaAddress + `", + "proposal_id": "1", + "option": "VOTE_OPTION_YES" + } + ] + }`) + // 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)), ",") //nolint:staticcheck + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{"*"}) + s.chainB.GetSimApp().ICAHostKeeper.SetParams(s.chainB.GetContext(), params) + }, + ibcerrors.ErrInvalidType, + }, + { + "failure: alternative int representation of enum field", + func(icaAddress string) { + proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, s.chainA.GetContext().BlockTime(), s.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false) + s.Require().NoError(err) + + err = s.chainB.GetSimApp().GovKeeper.SetProposal(s.chainB.GetContext(), proposal) + s.Require().NoError(err) + err = s.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(s.chainB.GetContext(), proposal) + s.Require().NoError(err) + + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1.MsgVote", + "voter": "` + icaAddress + `", + "proposal_id": "1", + "option": 1, + "metadata": "" + } + ] + }`) + // 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)), ",") //nolint:staticcheck + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{"*"}) + s.chainB.GetSimApp().ICAHostKeeper.SetParams(s.chainB.GetContext(), params) + }, + ibcerrors.ErrInvalidType, + }, + { + "failure: alternative integer field representation", + func(icaAddress string) { + proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, s.chainA.GetContext().BlockTime(), s.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false) + s.Require().NoError(err) + + err = s.chainB.GetSimApp().GovKeeper.SetProposal(s.chainB.GetContext(), proposal) + s.Require().NoError(err) + err = s.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(s.chainB.GetContext(), proposal) + s.Require().NoError(err) + + msgBytes := []byte(`{ + "messages": [ + { + "@type": "/cosmos.gov.v1.MsgVote", + "voter": "` + icaAddress + `", + "proposal_id": 1, + "option": "VOTE_OPTION_YES", + "metadata": "" + } + ] + }`) + // 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)), ",") //nolint:staticcheck + + packetData = []byte(`{ + "type": 1, + "data":` + byteArrayString + ` + }`) + + params := types.NewParams(true, []string{"*"}) + s.chainB.GetSimApp().ICAHostKeeper.SetParams(s.chainB.GetContext(), params) + }, + ibcerrors.ErrInvalidType, + }, } for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} { diff --git a/modules/apps/27-interchain-accounts/types/codec.go b/modules/apps/27-interchain-accounts/types/codec.go index cec30d9f924..d61f524c988 100644 --- a/modules/apps/27-interchain-accounts/types/codec.go +++ b/modules/apps/27-interchain-accounts/types/codec.go @@ -102,7 +102,7 @@ func DeserializeCosmosTx(cdc codec.Codec, data []byte, encoding string) ([]sdk.M if isEqual, err := equalJSON(data, reconstructedData); err != nil { return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "cannot compare original and reconstructed JSON: %v", err) } else if !isEqual { - return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "original and reconstructed JSON do not match, all optional fields must be explicitly set") + return nil, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "original and reconstructed JSON objects do not match, original: %s, reconstructed: %s", string(data), string(reconstructedData)) } default: return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding) From f4397fd036a3cf5dff20913b3e21c125ec2602da Mon Sep 17 00:00:00 2001 From: srdtrk Date: Wed, 17 Dec 2025 18:30:16 +0400 Subject: [PATCH 5/5] docs: document the validation change --- docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md b/docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md index 5be03af2f8e..8a72b4cfe30 100644 --- a/docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md +++ b/docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md @@ -56,3 +56,7 @@ The proto3 JSON encoding presents an alternative encoding technique for `CosmosT ``` Here, the `"messages"` array is populated with transactions. Each transaction is represented as a JSON object with the `@type` field denoting the transaction type and the remaining fields representing the transaction's attributes. + +:::warning +When utilizing proto3 JSON encoding, we have extra validations to ensure the integrity of the encoded data. Specifically, after decoding the `CosmosTx` from JSON, we re-encode it back to JSON and compare it with the original input. If non-formatting discrepancies arise, an error is returned. This validation step ensures that the JSON representation remains consistent throughout the encoding and decoding processes. This additional check means that all optional fields must be explicitly set in the JSON input, all enums and integers must be represented as strings. +:::