diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b0b02fcb6..5a2a87ffe8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. 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. +::: diff --git a/e2e/go.mod b/e2e/go.mod index ed025a01f55..26e88ee1a15 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -18,12 +18,12 @@ require ( cosmossdk.io/x/upgrade v0.2.0 github.com/cometbft/cometbft v0.38.19 github.com/cosmos/cosmos-sdk v0.53.4 - github.com/cosmos/gogoproto v1.7.2 - github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.0.0-00010101000000-000000000000 - github.com/cosmos/ibc-go/v10 v10.3.0 - github.com/cosmos/interchaintest/v10 v10.0.1 - github.com/docker/docker v28.5.2+incompatible - github.com/moby/moby v28.5.2+incompatible + github.com/cosmos/gogoproto v1.7.0 + github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 v10.2.0 + github.com/cosmos/ibc-go/v10 v10.4.0 + github.com/cosmos/interchaintest/v10 v10.0.0 + github.com/docker/docker v27.5.1+incompatible + github.com/moby/moby v27.5.1+incompatible github.com/pelletier/go-toml v1.9.5 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 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 f7b495c8252..ad43d756826 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 (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) @@ -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": "" } ] }`) @@ -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": [ @@ -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": [ @@ -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) @@ -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": "" } ] }`) @@ -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": [ @@ -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": "" } ] }`) @@ -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) @@ -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": "" + } ] }`) @@ -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 @@ -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": [ @@ -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": [ @@ -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} { diff --git a/modules/apps/27-interchain-accounts/types/codec.go b/modules/apps/27-interchain-accounts/types/codec.go index f08bd3df50e..d61f524c988 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 objects do not match, original: %s, reconstructed: %s", string(data), string(reconstructedData)) + } default: return nil, errorsmod.Wrapf(ErrInvalidCodec, "unsupported encoding format %s", encoding) } @@ -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 +} diff --git a/modules/light-clients/08-wasm/go.mod b/modules/light-clients/08-wasm/go.mod index 217c28bcbd0..10ec922d15a 100644 --- a/modules/light-clients/08-wasm/go.mod +++ b/modules/light-clients/08-wasm/go.mod @@ -26,7 +26,7 @@ require ( github.com/cosmos/cosmos-db v1.1.3 github.com/cosmos/cosmos-sdk v0.53.4 github.com/cosmos/gogoproto v1.7.0 - github.com/cosmos/ibc-go/v10 v10.3.0 + github.com/cosmos/ibc-go/v10 v10.4.0 github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/prysmaticlabs/prysm/v5 v5.3.0 diff --git a/modules/light-clients/08-wasm/go.sum b/modules/light-clients/08-wasm/go.sum index a4d601f1246..540349809cd 100644 --- a/modules/light-clients/08-wasm/go.sum +++ b/modules/light-clients/08-wasm/go.sum @@ -661,8 +661,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CosmWasm/wasmvm/v2 v2.2.4 h1:V3UwXJMA8TNOuQETppDQkaXAevF7gOWLYpKvrThPv7o= -github.com/CosmWasm/wasmvm/v2 v2.2.4/go.mod h1:Aj/rB2KMRM8nAdbWxkO23rnQYb5KsoPuH9ZizSi0sVg= github.com/CosmWasm/wasmvm/v3 v3.0.2 h1:+MLkOX+IdklITLqfG26PCFv5OXdZvNb8z5Wq5JFXTRM= github.com/CosmWasm/wasmvm/v3 v3.0.2/go.mod h1:oknpb1bFERvvKcY7vHRp1F/Y/z66xVrsl7n9uWkOAlM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= diff --git a/modules/light-clients/08-wasm/testing/simapp/simd/cmd/root.go b/modules/light-clients/08-wasm/testing/simapp/simd/cmd/root.go index 0f3d234bd44..c8300a92094 100644 --- a/modules/light-clients/08-wasm/testing/simapp/simd/cmd/root.go +++ b/modules/light-clients/08-wasm/testing/simapp/simd/cmd/root.go @@ -410,7 +410,7 @@ func getExpectedLibwasmVersion() string { panic("can't read build info") } for _, d := range buildInfo.Deps { - if d.Path != "github.com/CosmWasm/wasmvm/v2" { + if d.Path != "github.com/CosmWasm/wasmvm/v3" { continue } if d.Replace != nil {