Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ Ref: https://keepachangelog.com/en/1.0.0/

# Changelog

## [Unreleased]
## [v10.5.0](https://github.com/cosmos/ibc-go/releases/tag/v10.5.0) - 2025-12-18

### Improvements

* [\#8734](https://github.com/cosmos/ibc-go/pull/8734) Add extra validation for ProtoJSON unmarshalling in ICS-27 ICA.

## [v10.4.0](https://github.com/cosmos/ibc-go/releases/tag/v10.4.0) - 2025-10-10

### Improvements

* [\#8573](https://github.com/cosmos/ibc-go/pull/8573) Support custom address codecs in transfer.

Expand Down
4 changes: 4 additions & 0 deletions docs/docs/02-apps/02-interchain-accounts/07-tx-encoding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::
150 changes: 131 additions & 19 deletions modules/apps/27-interchain-accounts/host/keeper/relay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ func (suite *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, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
suite.Require().NoError(err)
Expand All @@ -581,8 +581,9 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
{
"@type": "/cosmos.gov.v1.MsgVote",
"voter": "` + icaAddress + `",
"proposal_id": 1,
"option": 1
"proposal_id": "1",
"option": "VOTE_OPTION_YES",
"metadata": ""
}
]
}`)
Expand All @@ -601,7 +602,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
nil,
},
{
"interchain account successfully executes banktypes.MsgSend",
"success: interchain account successfully executes banktypes.MsgSend",
func(icaAddress string) {
msgBytes := []byte(`{
"messages": [
Expand All @@ -626,7 +627,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
nil,
},
{
"interchain account successfully executes govtypesv1.MsgSubmitProposal",
"success: interchain account successfully executes govtypesv1.MsgSubmitProposal",
func(icaAddress string) {
msgBytes := []byte(`{
"messages": [
Expand Down Expand Up @@ -655,7 +656,7 @@ func (suite *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, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
suite.Require().NoError(err)
Expand All @@ -670,8 +671,9 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
{
"@type": "/cosmos.gov.v1.MsgVote",
"voter": "` + icaAddress + `",
"proposal_id": 1,
"option": 1
"proposal_id": "1",
"option": "VOTE_OPTION_YES",
"metadata": ""
}
]
}`)
Expand All @@ -688,7 +690,7 @@ func (suite *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": [
Expand All @@ -704,15 +706,16 @@ func (suite *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": ""
}
]
}`)
Expand All @@ -729,7 +732,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
nil,
},
{
"interchain account successfully executes transfertypes.MsgTransfer",
"success: interchain account successfully executes transfertypes.MsgTransfer",
func(icaAddress string) {
transferPath := ibctesting.NewTransferPath(suite.chainB, suite.chainC)

Expand All @@ -744,9 +747,11 @@ func (suite *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": ""

}
]
}`)
Expand All @@ -763,7 +768,7 @@ func (suite *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
Expand All @@ -779,7 +784,7 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
ibcerrors.ErrInvalidType,
},
{
"message type not allowed banktypes.MsgSend",
"failure: message type not allowed banktypes.MsgSend",
func(icaAddress string) {
msgBytes := []byte(`{
"messages": [
Expand All @@ -804,7 +809,7 @@ func (suite *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": [
Expand All @@ -828,6 +833,113 @@ func (suite *KeeperTestSuite) TestJSONOnRecvPacket() {
},
ibcerrors.ErrInvalidType,
},
{
"failure: missing optional metadata field",
func(icaAddress string) {
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
suite.Require().NoError(err)

err = suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal)
suite.Require().NoError(err)
err = suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal)
suite.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{"*"})
suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
},
ibcerrors.ErrInvalidType,
},
{
"failure: alternative int representation of enum field",
func(icaAddress string) {
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
suite.Require().NoError(err)

err = suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal)
suite.Require().NoError(err)
err = suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal)
suite.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{"*"})
suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
},
ibcerrors.ErrInvalidType,
},
{
"failure: alternative integer field representation",
func(icaAddress string) {
proposal, err := govtypesv1.NewProposal([]sdk.Msg{getTestProposalMessage()}, govtypesv1.DefaultStartingProposalID, suite.chainA.GetContext().BlockTime(), suite.chainA.GetContext().BlockTime(), "test proposal", "title", "Description", sdk.AccAddress(interchainAccountAddr), false)
suite.Require().NoError(err)

err = suite.chainB.GetSimApp().GovKeeper.SetProposal(suite.chainB.GetContext(), proposal)
suite.Require().NoError(err)
err = suite.chainB.GetSimApp().GovKeeper.ActivateVotingPeriod(suite.chainB.GetContext(), proposal)
suite.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{"*"})
suite.chainB.GetSimApp().ICAHostKeeper.SetParams(suite.chainB.GetContext(), params)
},
ibcerrors.ErrInvalidType,
},
}

for _, ordering := range []channeltypes.Order{channeltypes.UNORDERED, channeltypes.ORDERED} {
Expand Down
23 changes: 23 additions & 0 deletions modules/apps/27-interchain-accounts/types/codec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package types

import (
"encoding/json"
"reflect"

"github.com/cosmos/gogoproto/proto"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -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 objects do not match, original: %s, reconstructed: %s", string(data), string(reconstructedData))
}
default:
return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding)
}
Expand All @@ -109,3 +121,14 @@ 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
}
Loading