diff --git a/modules/apps/27-interchain-accounts/controller/ibc_middleware.go b/modules/apps/27-interchain-accounts/controller/ibc_middleware.go index 1338b3f252f..2c9bcead23a 100644 --- a/modules/apps/27-interchain-accounts/controller/ibc_middleware.go +++ b/modules/apps/27-interchain-accounts/controller/ibc_middleware.go @@ -17,7 +17,10 @@ import ( ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" ) -var _ porttypes.Middleware = (*IBCMiddleware)(nil) +var ( + _ porttypes.Middleware = (*IBCMiddleware)(nil) + _ porttypes.PacketDataUnmarshaler = (*IBCMiddleware)(nil) +) // IBCMiddleware implements the ICS26 callbacks for the fee middleware given the // ICA controller keeper and the underlying application. @@ -253,3 +256,15 @@ func (im IBCMiddleware) WriteAcknowledgement( func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { return im.keeper.GetAppVersion(ctx, portID, channelID) } + +// UnmarshalPacketData attempts to unmarshal the provided packet data bytes +// into an InterchainAccountPacketData. This function implements the optional +// PacketDataUnmarshaler interface required for ADR 008 support. +func (im IBCMiddleware) UnmarshalPacketData(bz []byte) (interface{}, error) { + var packetData icatypes.InterchainAccountPacketData + if err := icatypes.ModuleCdc.UnmarshalJSON(bz, &packetData); err != nil { + return nil, err + } + + return packetData, nil +} diff --git a/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go b/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go index cf049e3ae50..071d6695868 100644 --- a/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go +++ b/modules/apps/27-interchain-accounts/controller/ibc_middleware_test.go @@ -918,3 +918,26 @@ func (suite *InterchainAccountsTestSuite) TestClosedChannelReopensWithMsgServer( err = path.EndpointB.ChanOpenConfirm() suite.Require().NoError(err) } + +func (suite *InterchainAccountsTestSuite) TestPacketDataUnmarshalerInterface() { + path := NewICAPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + err := SetupICAPath(path, TestOwnerAddress) + suite.Require().NoError(err) + + expPacketData := icatypes.InterchainAccountPacketData{ + Type: icatypes.EXECUTE_TX, + Data: []byte("data"), + Memo: "", + } + + packetData, err := controller.IBCMiddleware{}.UnmarshalPacketData(expPacketData.GetBytes()) + suite.Require().NoError(err) + suite.Require().Equal(expPacketData, packetData) + + // test invalid packet data + invalidPacketData := []byte("invalid packet data") + packetData, err = controller.IBCMiddleware{}.UnmarshalPacketData(invalidPacketData) + suite.Require().Error(err) + suite.Require().Nil(packetData) +} diff --git a/modules/apps/29-fee/ibc_middleware.go b/modules/apps/29-fee/ibc_middleware.go index 5b260ad7be9..1390d816386 100644 --- a/modules/apps/29-fee/ibc_middleware.go +++ b/modules/apps/29-fee/ibc_middleware.go @@ -16,7 +16,10 @@ import ( "github.com/cosmos/ibc-go/v7/modules/core/exported" ) -var _ porttypes.Middleware = (*IBCMiddleware)(nil) +var ( + _ porttypes.Middleware = (*IBCMiddleware)(nil) + _ porttypes.PacketDataUnmarshaler = (*IBCMiddleware)(nil) +) // IBCMiddleware implements the ICS26 callbacks for the fee middleware given the // fee keeper and the underlying application. @@ -348,3 +351,15 @@ func (im IBCMiddleware) WriteAcknowledgement( func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { return im.keeper.GetAppVersion(ctx, portID, channelID) } + +// UnmarshalPacketData attempts to use the underlying app to unmarshal the packet data. +// If the underlying app does not support the PacketDataUnmarshaler interface, an error is returned. +// This function implements the optional PacketDataUnmarshaler interface required for ADR 008 support. +func (im IBCMiddleware) UnmarshalPacketData(bz []byte) (interface{}, error) { + unmarshaler, ok := im.app.(porttypes.PacketDataUnmarshaler) + if !ok { + return nil, errorsmod.Wrapf(types.ErrUnsupportedAction, "underlying app does not implement %T", (*porttypes.PacketDataUnmarshaler)(nil)) + } + + return unmarshaler.UnmarshalPacketData(bz) +} diff --git a/modules/apps/29-fee/ibc_middleware_test.go b/modules/apps/29-fee/ibc_middleware_test.go index ee1d0ed228a..a36f9c6a381 100644 --- a/modules/apps/29-fee/ibc_middleware_test.go +++ b/modules/apps/29-fee/ibc_middleware_test.go @@ -3,15 +3,18 @@ package fee_test import ( "fmt" + errorsmod "cosmossdk.io/errors" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" fee "github.com/cosmos/ibc-go/v7/modules/apps/29-fee" + feekeeper "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/keeper" "github.com/cosmos/ibc-go/v7/modules/apps/29-fee/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" 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" "github.com/cosmos/ibc-go/v7/modules/core/exported" ibctesting "github.com/cosmos/ibc-go/v7/testing" @@ -1080,3 +1083,27 @@ func (suite *FeeTestSuite) TestGetAppVersion() { }) } } + +func (suite *FeeTestSuite) TestPacketDataUnmarshalerInterface() { + module, _, err := suite.chainA.App.GetIBCKeeper().PortKeeper.LookupModuleByPort(suite.chainA.GetContext(), ibctesting.MockFeePort) + suite.Require().NoError(err) + + cbs, ok := suite.chainA.App.GetIBCKeeper().Router.GetRoute(module) + suite.Require().True(ok) + + feeModule, ok := cbs.(porttypes.PacketDataUnmarshaler) + suite.Require().True(ok) + + packetData, err := feeModule.UnmarshalPacketData(ibcmock.MockPacketData) + suite.Require().NoError(err) + suite.Require().Equal(ibcmock.MockPacketData, packetData) +} + +func (suite *FeeTestSuite) TestPacketDataUnmarshalerInterfaceError() { + // test the case when the underlying application cannot be casted to a PacketDataUnmarshaler + mockFeeMiddleware := fee.NewIBCMiddleware(nil, feekeeper.Keeper{}) + + _, err := mockFeeMiddleware.UnmarshalPacketData(ibcmock.MockPacketData) + expError := errorsmod.Wrapf(types.ErrUnsupportedAction, "underlying app does not implement %T", (*porttypes.PacketDataUnmarshaler)(nil)) + suite.Require().ErrorIs(err, expError) +} diff --git a/modules/apps/29-fee/types/errors.go b/modules/apps/29-fee/types/errors.go index 26167c71016..22dd35000a2 100644 --- a/modules/apps/29-fee/types/errors.go +++ b/modules/apps/29-fee/types/errors.go @@ -16,4 +16,5 @@ var ( ErrFeeNotEnabled = errorsmod.Register(ModuleName, 9, "fee module is not enabled for this channel. If this error occurs after channel setup, fee module may not be enabled") ErrRelayerNotFoundForAsyncAck = errorsmod.Register(ModuleName, 10, "relayer address must be stored for async WriteAcknowledgement") ErrFeeModuleLocked = errorsmod.Register(ModuleName, 11, "the fee module is currently locked, a severe bug has been detected") + ErrUnsupportedAction = errorsmod.Register(ModuleName, 12, "unsupported action") ) diff --git a/modules/apps/transfer/ibc_module.go b/modules/apps/transfer/ibc_module.go index 613315bacc3..29f76b3d19b 100644 --- a/modules/apps/transfer/ibc_module.go +++ b/modules/apps/transfer/ibc_module.go @@ -19,6 +19,11 @@ import ( ibcexported "github.com/cosmos/ibc-go/v7/modules/core/exported" ) +var ( + _ porttypes.IBCModule = (*IBCModule)(nil) + _ porttypes.PacketDataUnmarshaler = (*IBCModule)(nil) +) + // IBCModule implements the ICS26 interface for transfer given the transfer keeper. type IBCModule struct { keeper keeper.Keeper @@ -301,3 +306,15 @@ func (im IBCModule) OnTimeoutPacket( return nil } + +// UnmarshalPacketData attempts to unmarshal the provided packet data bytes +// into a FungibleTokenPacketData. This function implements the optional +// PacketDataUnmarshaler interface required for ADR 008 support. +func (im IBCModule) UnmarshalPacketData(bz []byte) (interface{}, error) { + var packetData types.FungibleTokenPacketData + if err := types.ModuleCdc.UnmarshalJSON(bz, &packetData); err != nil { + return nil, err + } + + return packetData, nil +} diff --git a/modules/apps/transfer/ibc_module_test.go b/modules/apps/transfer/ibc_module_test.go index deed17c37c6..d8e532632a4 100644 --- a/modules/apps/transfer/ibc_module_test.go +++ b/modules/apps/transfer/ibc_module_test.go @@ -3,6 +3,9 @@ package transfer_test import ( "math" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" "github.com/cosmos/ibc-go/v7/modules/apps/transfer" "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" @@ -238,3 +241,69 @@ func (suite *TransferTestSuite) TestOnChanOpenAck() { }) } } + +func (suite *TransferTestSuite) TestPacketDataUnmarshalerInterface() { + var ( + sender = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()).String() + receiver = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()).String() + + data []byte + expPacketData types.FungibleTokenPacketData + ) + + testCases := []struct { + name string + malleate func() + expPass bool + }{ + { + "success: valid packet data with memo", + func() { + expPacketData = types.FungibleTokenPacketData{ + Denom: ibctesting.TestCoin.Denom, + Amount: ibctesting.TestCoin.Amount.String(), + Sender: sender, + Receiver: receiver, + Memo: "some memo", + } + data = expPacketData.GetBytes() + }, + true, + }, + { + "success: valid packet data without memo", + func() { + expPacketData = types.FungibleTokenPacketData{ + Denom: ibctesting.TestCoin.Denom, + Amount: ibctesting.TestCoin.Amount.String(), + Sender: sender, + Receiver: receiver, + Memo: "", + } + data = expPacketData.GetBytes() + }, + true, + }, + { + "failure: invalid packet data", + func() { + data = []byte("invalid packet data") + }, + false, + }, + } + + for _, tc := range testCases { + tc.malleate() + + packetData, err := transfer.IBCModule{}.UnmarshalPacketData(data) + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(expPacketData, packetData) + } else { + suite.Require().Error(err) + suite.Require().Nil(packetData) + } + } +} diff --git a/modules/core/05-port/types/module.go b/modules/core/05-port/types/module.go index 33465bf6f98..a8306f73064 100644 --- a/modules/core/05-port/types/module.go +++ b/modules/core/05-port/types/module.go @@ -138,3 +138,10 @@ type Middleware interface { IBCModule ICS4Wrapper } + +// PacketDataUnmarshaler defines an optional interface which allows a middleware to +// request the packet data to be unmarshaled by the base application. +type PacketDataUnmarshaler interface { + // UnmarshalPacketData unmarshals the packet data into a concrete type + UnmarshalPacketData([]byte) (interface{}, error) +} diff --git a/testing/mock/ibc_module.go b/testing/mock/ibc_module.go index 1cbbfe47fa7..f2834e435a6 100644 --- a/testing/mock/ibc_module.go +++ b/testing/mock/ibc_module.go @@ -3,6 +3,7 @@ package mock import ( "bytes" "fmt" + "reflect" "strconv" "strings" @@ -10,10 +11,16 @@ import ( capabilitytypes "github.com/cosmos/ibc-go/modules/capability/types" 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" "github.com/cosmos/ibc-go/v7/modules/core/exported" ) +var ( + _ porttypes.IBCModule = (*IBCModule)(nil) + _ porttypes.PacketDataUnmarshaler = (*IBCModule)(nil) +) + // applicationCallbackError is a custom error type that will be unique for testing purposes. type applicationCallbackError struct{} @@ -169,6 +176,15 @@ func (im IBCModule) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, return nil } +// UnmarshalPacketData returns the MockPacketData. This function implements the optional +// PacketDataUnmarshaler interface required for ADR 008 support. +func (im IBCModule) UnmarshalPacketData(bz []byte) (interface{}, error) { + if reflect.DeepEqual(bz, MockPacketData) { + return MockPacketData, nil + } + return nil, MockApplicationCallbackError +} + // GetMockRecvCanaryCapabilityName generates a capability name for testing OnRecvPacket functionality. func GetMockRecvCanaryCapabilityName(packet channeltypes.Packet) string { return fmt.Sprintf("%s%s%s%s", MockRecvCanaryCapabilityName, packet.GetDestPort(), packet.GetDestChannel(), strconv.Itoa(int(packet.GetSequence())))