diff --git a/modules/apps/callbacks/callbacks_test.go b/modules/apps/callbacks/callbacks_test.go index 4f87e7496d9..743683f2f99 100644 --- a/modules/apps/callbacks/callbacks_test.go +++ b/modules/apps/callbacks/callbacks_test.go @@ -295,7 +295,7 @@ func (s *CallbacksTestSuite) AssertHasExecutedExpectedCallbackWithFee( // GetExpectedEvent returns the expected event for a callback. func GetExpectedEvent( - ctx sdk.Context, packetDataUnmarshaler porttypes.PacketDataUnmarshaler, remainingGas uint64, data []byte, srcPortID, + ctx sdk.Context, packetDataUnmarshaler porttypes.PacketDataUnmarshaler, remainingGas uint64, data []byte, eventPortID, eventChannelID string, seq uint64, callbackType types.CallbackType, expError error, ) (abci.Event, bool) { var ( diff --git a/modules/apps/callbacks/export_test.go b/modules/apps/callbacks/export_test.go deleted file mode 100644 index 0a825ee745b..00000000000 --- a/modules/apps/callbacks/export_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package ibccallbacks - -/* - This file is to allow for unexported functions and fields to be accessible to the testing package. -*/ - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/cosmos/ibc-go/modules/apps/callbacks/types" -) - -// ProcessCallback is a wrapper around processCallback to allow the function to be directly called in tests. -func (im IBCMiddleware) ProcessCallback( - ctx sdk.Context, callbackType types.CallbackType, - callbackData types.CallbackData, callbackExecutor func(sdk.Context) error, -) error { - return im.processCallback(ctx, callbackType, callbackData, callbackExecutor) -} diff --git a/modules/apps/callbacks/ibc_middleware.go b/modules/apps/callbacks/ibc_middleware.go index 1b483487ede..4915adbe643 100644 --- a/modules/apps/callbacks/ibc_middleware.go +++ b/modules/apps/callbacks/ibc_middleware.go @@ -6,10 +6,10 @@ import ( "fmt" errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ibc-go/modules/apps/callbacks/internal" "github.com/cosmos/ibc-go/modules/apps/callbacks/types" clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" @@ -114,7 +114,7 @@ func (im IBCMiddleware) SendPacket( ) } - err = im.processCallback(sdkCtx, types.CallbackTypeSendPacket, callbackData, callbackExecutor) + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeSendPacket, callbackData, callbackExecutor) // contract keeper is allowed to reject the packet send. if err != nil { return 0, err @@ -158,7 +158,7 @@ func (im IBCMiddleware) OnAcknowledgementPacket( } // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions - err = im.processCallback(sdkCtx, types.CallbackTypeAcknowledgementPacket, callbackData, callbackExecutor) + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeAcknowledgementPacket, callbackData, callbackExecutor) types.EmitCallbackEvent( sdkCtx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(), types.CallbackTypeAcknowledgementPacket, callbackData, err, @@ -192,7 +192,7 @@ func (im IBCMiddleware) OnTimeoutPacket(ctx context.Context, channelVersion stri } // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions - err = im.processCallback(sdkCtx, types.CallbackTypeTimeoutPacket, callbackData, callbackExecutor) + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeTimeoutPacket, callbackData, callbackExecutor) types.EmitCallbackEvent( sdkCtx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(), types.CallbackTypeTimeoutPacket, callbackData, err, @@ -229,7 +229,7 @@ func (im IBCMiddleware) OnRecvPacket(ctx context.Context, channelVersion string, } // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions - err = im.processCallback(sdkCtx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor) + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor) types.EmitCallbackEvent( sdkCtx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(), types.CallbackTypeReceivePacket, callbackData, err, @@ -272,7 +272,7 @@ func (im IBCMiddleware) WriteAcknowledgement( } // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions - err = im.processCallback(sdkCtx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor) + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor) types.EmitCallbackEvent( sdkCtx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(), types.CallbackTypeReceivePacket, callbackData, err, @@ -281,55 +281,6 @@ func (im IBCMiddleware) WriteAcknowledgement( return nil } -// processCallback executes the callbackExecutor and reverts contract changes if the callbackExecutor fails. -// -// Error Precedence and Returns: -// - oogErr: Takes the highest precedence. If the callback runs out of gas, an error wrapped with types.ErrCallbackOutOfGas is returned. -// - panicErr: Takes the second-highest precedence. If a panic occurs and it is not propagated, an error wrapped with types.ErrCallbackPanic is returned. -// - callbackErr: If the callbackExecutor returns an error, it is returned as-is. -// -// panics if -// - the contractExecutor panics for any reason, and the callbackType is SendPacket, or -// - the contractExecutor runs out of gas and the relayer has not reserved gas grater than or equal to -// CommitGasLimit. -func (IBCMiddleware) processCallback( - ctx sdk.Context, callbackType types.CallbackType, - callbackData types.CallbackData, callbackExecutor func(sdk.Context) error, -) (err error) { - cachedCtx, writeFn := ctx.CacheContext() - cachedCtx = cachedCtx.WithGasMeter(storetypes.NewGasMeter(callbackData.ExecutionGasLimit)) - - defer func() { - // consume the minimum of g.consumed and g.limit - ctx.GasMeter().ConsumeGas(cachedCtx.GasMeter().GasConsumedToLimit(), fmt.Sprintf("ibc %s callback", callbackType)) - - // recover from all panics except during SendPacket callbacks - if r := recover(); r != nil { - if callbackType == types.CallbackTypeSendPacket { - panic(r) - } - err = errorsmod.Wrapf(types.ErrCallbackPanic, "ibc %s callback panicked with: %v", callbackType, r) - } - - // if the callback ran out of gas and the relayer has not reserved enough gas, then revert the state - if cachedCtx.GasMeter().IsPastLimit() { - if callbackData.AllowRetry() { - panic(storetypes.ErrorOutOfGas{Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", callbackType, callbackData.CommitGasLimit)}) - } - err = errorsmod.Wrapf(types.ErrCallbackOutOfGas, "ibc %s callback out of gas", callbackType) - } - - // allow the transaction to be committed, continuing the packet lifecycle - }() - - err = callbackExecutor(cachedCtx) - if err == nil { - writeFn() - } - - return err -} - // OnChanOpenInit defers to the underlying application func (im IBCMiddleware) OnChanOpenInit( ctx context.Context, diff --git a/modules/apps/callbacks/ibc_middleware_test.go b/modules/apps/callbacks/ibc_middleware_test.go index 00cd58bc20a..6e2ccd79722 100644 --- a/modules/apps/callbacks/ibc_middleware_test.go +++ b/modules/apps/callbacks/ibc_middleware_test.go @@ -11,6 +11,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ibccallbacks "github.com/cosmos/ibc-go/modules/apps/callbacks" + "github.com/cosmos/ibc-go/modules/apps/callbacks/internal" "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp" "github.com/cosmos/ibc-go/modules/apps/callbacks/types" icacontrollertypes "github.com/cosmos/ibc-go/v9/modules/apps/27-interchain-accounts/controller/types" @@ -210,7 +211,7 @@ func (s *CallbacksTestSuite) TestSendPacket() { s.Require().Equal(uint64(1), seq) expEvent, exists := GetExpectedEvent( - ctx, transferICS4Wrapper.(porttypes.PacketDataUnmarshaler), gasLimit, packetData.GetBytes(), s.path.EndpointA.ChannelConfig.PortID, + ctx, transferICS4Wrapper.(porttypes.PacketDataUnmarshaler), gasLimit, packetData.GetBytes(), s.path.EndpointA.ChannelConfig.PortID, s.path.EndpointA.ChannelID, seq, types.CallbackTypeSendPacket, nil, ) if exists { @@ -391,7 +392,7 @@ func (s *CallbacksTestSuite) TestOnAcknowledgementPacket() { s.Require().Equal(uint8(1), sourceStatefulCounter) expEvent, exists := GetExpectedEvent( - ctx, transferStack.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.SourcePort, + ctx, transferStack.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.SourcePort, packet.SourceChannel, packet.Sequence, types.CallbackTypeAcknowledgementPacket, nil, ) s.Require().True(exists) @@ -554,7 +555,7 @@ func (s *CallbacksTestSuite) TestOnTimeoutPacket() { s.Require().Equal(uint8(2), sourceStatefulCounter) expEvent, exists := GetExpectedEvent( - ctx, transferStack.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.SourcePort, + ctx, transferStack.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.SourcePort, packet.SourceChannel, packet.Sequence, types.CallbackTypeTimeoutPacket, nil, ) s.Require().True(exists) @@ -723,7 +724,7 @@ func (s *CallbacksTestSuite) TestOnRecvPacket() { s.Require().Equal(uint8(1), destStatefulCounter) expEvent, exists := GetExpectedEvent( - ctx, transferStack.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.SourcePort, + ctx, transferStack.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.DestinationPort, packet.DestinationChannel, packet.Sequence, types.CallbackTypeReceivePacket, nil, ) s.Require().True(exists) @@ -823,7 +824,7 @@ func (s *CallbacksTestSuite) TestWriteAcknowledgement() { s.Require().NoError(err) expEvent, exists := GetExpectedEvent( - ctx, transferICS4Wrapper.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.SourcePort, + ctx, transferICS4Wrapper.(porttypes.PacketDataUnmarshaler), gasLimit, packet.Data, packet.DestinationPort, packet.DestinationChannel, packet.Sequence, types.CallbackTypeReceivePacket, nil, ) if exists { @@ -953,13 +954,8 @@ func (s *CallbacksTestSuite) TestProcessCallback() { tc.malleate() var err error - cbs, ok := s.chainA.App.GetIBCKeeper().PortKeeper.Route(ibctesting.MockFeePort) - s.Require().True(ok) - mockCallbackStack, ok := cbs.(ibccallbacks.IBCMiddleware) - s.Require().True(ok) - processCallback := func() { - err = mockCallbackStack.ProcessCallback(ctx, callbackType, callbackData, callbackExecutor) + err = internal.ProcessCallback(ctx, callbackType, callbackData, callbackExecutor) } expPass := tc.expValue == nil diff --git a/modules/apps/callbacks/internal/process.go b/modules/apps/callbacks/internal/process.go new file mode 100644 index 00000000000..37020375ebc --- /dev/null +++ b/modules/apps/callbacks/internal/process.go @@ -0,0 +1,61 @@ +package internal + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/apps/callbacks/types" +) + +// ProcessCallback executes the callbackExecutor and reverts contract changes if the callbackExecutor fails. +// +// Error Precedence and Returns: +// - oogErr: Takes the highest precedence. If the callback runs out of gas, an error wrapped with types.ErrCallbackOutOfGas is returned. +// - panicErr: Takes the second-highest precedence. If a panic occurs and it is not propagated, an error wrapped with types.ErrCallbackPanic is returned. +// - callbackErr: If the callbackExecutor returns an error, it is returned as-is. +// +// panics if +// - the contractExecutor panics for any reason, and the callbackType is SendPacket, or +// - the contractExecutor runs out of gas and the relayer has not reserved gas grater than or equal to +// CommitGasLimit. +func ProcessCallback( + ctx sdk.Context, callbackType types.CallbackType, + callbackData types.CallbackData, callbackExecutor func(sdk.Context) error, +) (err error) { + cachedCtx, writeFn := ctx.CacheContext() + cachedCtx = cachedCtx.WithGasMeter(storetypes.NewGasMeter(callbackData.ExecutionGasLimit)) + + defer func() { + // consume the minimum of g.consumed and g.limit + ctx.GasMeter().ConsumeGas(cachedCtx.GasMeter().GasConsumedToLimit(), fmt.Sprintf("ibc %s callback", callbackType)) + + // recover from all panics except during SendPacket callbacks + if r := recover(); r != nil { + if callbackType == types.CallbackTypeSendPacket { + panic(r) + } + err = errorsmod.Wrapf(types.ErrCallbackPanic, "ibc %s callback panicked with: %v", callbackType, r) + } + + // if the callback ran out of gas and the relayer has not reserved enough gas, then revert the state + if cachedCtx.GasMeter().IsPastLimit() { + if callbackData.AllowRetry() { + panic(storetypes.ErrorOutOfGas{Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", callbackType, callbackData.CommitGasLimit)}) + } + err = errorsmod.Wrapf(types.ErrCallbackOutOfGas, "ibc %s callback out of gas", callbackType) + } + + // allow the transaction to be committed, continuing the packet lifecycle + }() + + err = callbackExecutor(cachedCtx) + if err == nil { + writeFn() + } + + return err +} diff --git a/modules/apps/callbacks/testing/simapp/app.go b/modules/apps/callbacks/testing/simapp/app.go index e50440c6565..6f04252dce4 100644 --- a/modules/apps/callbacks/testing/simapp/app.go +++ b/modules/apps/callbacks/testing/simapp/app.go @@ -79,6 +79,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" ibccallbacks "github.com/cosmos/ibc-go/modules/apps/callbacks" + ibccallbacksv2 "github.com/cosmos/ibc-go/modules/apps/callbacks/v2" ica "github.com/cosmos/ibc-go/v9/modules/apps/27-interchain-accounts" icacontroller "github.com/cosmos/ibc-go/v9/modules/apps/27-interchain-accounts/controller" icacontrollerkeeper "github.com/cosmos/ibc-go/v9/modules/apps/27-interchain-accounts/controller/keeper" @@ -93,8 +94,10 @@ import ( "github.com/cosmos/ibc-go/v9/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v9/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + transferv2 "github.com/cosmos/ibc-go/v9/modules/apps/transfer/v2" ibc "github.com/cosmos/ibc-go/v9/modules/core" porttypes "github.com/cosmos/ibc-go/v9/modules/core/05-port/types" + ibcapi "github.com/cosmos/ibc-go/v9/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v9/modules/core/exported" ibckeeper "github.com/cosmos/ibc-go/v9/modules/core/keeper" solomachine "github.com/cosmos/ibc-go/v9/modules/light-clients/06-solomachine" @@ -375,6 +378,7 @@ func NewSimApp( // Create IBC Router ibcRouter := porttypes.NewRouter() + ibcRouterV2 := ibcapi.NewRouter() // Middleware Stacks maxCallbackGas := uint64(1_000_000) @@ -481,8 +485,13 @@ func NewSimApp( feeWithMockModule = ibccallbacks.NewIBCMiddleware(feeWithMockModule, app.IBCFeeKeeper, app.MockContractKeeper, maxCallbackGas) ibcRouter.AddRoute(MockFeePort, feeWithMockModule) + // add transfer v2 module wrapped by callbacks v2 middleware + cbTransferModulev2 := ibccallbacksv2.NewIBCMiddleware(transferv2.NewIBCModule(app.TransferKeeper), app.IBCKeeper.ChannelKeeperV2, app.MockContractKeeper, app.IBCKeeper.ChannelKeeperV2, maxCallbackGas) + ibcRouterV2.AddRoute(ibctransfertypes.PortID, cbTransferModulev2) + // Seal the IBC Router app.IBCKeeper.SetRouter(ibcRouter) + app.IBCKeeper.SetRouterV2(ibcRouterV2) clientKeeper := app.IBCKeeper.ClientKeeper storeProvider := app.IBCKeeper.ClientKeeper.GetStoreProvider() diff --git a/modules/apps/callbacks/types/callbacks.go b/modules/apps/callbacks/types/callbacks.go index 0137a365241..6d72dc210e4 100644 --- a/modules/apps/callbacks/types/callbacks.go +++ b/modules/apps/callbacks/types/callbacks.go @@ -10,6 +10,7 @@ import ( channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v9/modules/core/05-port/types" + "github.com/cosmos/ibc-go/v9/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v9/modules/core/exported" ) @@ -51,6 +52,13 @@ type CallbacksCompatibleModule interface { porttypes.PacketDataUnmarshaler } +// CallbacksCompatibleModuleV2 is an interface that combines the IBCModuleV2 and PacketDataUnmarshaler +// interfaces to assert that the underlying application supports both. +type CallbacksCompatibleModuleV2 interface { + api.IBCModule + api.PacketDataUnmarshaler +} + // CallbackData is the callback data parsed from the packet. type CallbackData struct { // CallbackAddress is the address of the callback actor. @@ -82,7 +90,7 @@ func GetSourceCallbackData( return CallbackData{}, errorsmod.Wrap(ErrCannotUnmarshalPacketData, err.Error()) } - return getCallbackData(packetData, version, packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), maxGas, SourceCallbackKey) + return GetCallbackData(packetData, version, packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), maxGas, SourceCallbackKey) } // GetDestCallbackData parses the packet data and returns the destination callback data. @@ -96,14 +104,14 @@ func GetDestCallbackData( return CallbackData{}, errorsmod.Wrap(ErrCannotUnmarshalPacketData, err.Error()) } - return getCallbackData(packetData, version, packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), maxGas, DestinationCallbackKey) + return GetCallbackData(packetData, version, packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), maxGas, DestinationCallbackKey) } -// getCallbackData parses the packet data and returns the callback data. +// GetCallbackData parses the packet data and returns the callback data. // It also checks that the remaining gas is greater than the gas limit specified in the packet data. // The addressGetter and gasLimitGetter functions are used to retrieve the callback // address and gas limit from the callback data. -func getCallbackData( +func GetCallbackData( packetData interface{}, version, srcPortID string, remainingGas, maxGas uint64, diff --git a/modules/apps/callbacks/types/expected_keepers.go b/modules/apps/callbacks/types/expected_keepers.go index 717a9d6108f..cfcb83d0106 100644 --- a/modules/apps/callbacks/types/expected_keepers.go +++ b/modules/apps/callbacks/types/expected_keepers.go @@ -1,10 +1,13 @@ package types import ( + "context" + sdk "github.com/cosmos/cosmos-sdk/types" clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" ibcexported "github.com/cosmos/ibc-go/v9/modules/core/exported" ) @@ -97,3 +100,11 @@ type ContractKeeper interface { version string, ) error } + +type ChannelKeeperV2 interface { + GetAsyncPacket( + ctx context.Context, + clientID string, + sequence uint64, + ) (channeltypesv2.Packet, bool) +} diff --git a/modules/apps/callbacks/types/export_test.go b/modules/apps/callbacks/types/export_test.go index 6d2434156cc..5b0a32508e3 100644 --- a/modules/apps/callbacks/types/export_test.go +++ b/modules/apps/callbacks/types/export_test.go @@ -4,14 +4,6 @@ package types This file is to allow for unexported functions to be accessible to the testing package. */ -// GetCallbackData is a wrapper around getCallbackData to allow the function to be directly called in tests. -func GetCallbackData( - packetData interface{}, version, srcPortID string, remainingGas, - maxGas uint64, callbackKey string, -) (CallbackData, error) { - return getCallbackData(packetData, version, srcPortID, remainingGas, maxGas, callbackKey) -} - // GetCallbackAddress is a wrapper around getCallbackAddress to allow the function to be directly called in tests. func GetCallbackAddress(callbackData map[string]interface{}) string { return getCallbackAddress(callbackData) diff --git a/modules/apps/callbacks/v2/ibc_middleware.go b/modules/apps/callbacks/v2/ibc_middleware.go new file mode 100644 index 00000000000..59e52b8fba6 --- /dev/null +++ b/modules/apps/callbacks/v2/ibc_middleware.go @@ -0,0 +1,409 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/apps/callbacks/internal" + "github.com/cosmos/ibc-go/modules/apps/callbacks/types" + clienttypes "github.com/cosmos/ibc-go/v9/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc-go/v9/modules/core/api" +) + +var _ api.IBCModule = (*IBCMiddleware)(nil) + +// IBCMiddleware implements the IBC v2 middleware interface +// with the underlying application. +type IBCMiddleware struct { + app types.CallbacksCompatibleModuleV2 + writeAckWrapper api.WriteAcknowledgementWrapper + + contractKeeper types.ContractKeeper + chanKeeperV2 types.ChannelKeeperV2 + + // maxCallbackGas defines the maximum amount of gas that a callback actor can ask the + // relayer to pay for. If a callback fails due to insufficient gas, the entire tx + // is reverted if the relayer hadn't provided the minimum(userDefinedGas, maxCallbackGas). + // If the actor hasn't defined a gas limit, then it is assumed to be the maxCallbackGas. + maxCallbackGas uint64 +} + +// NewIBCMiddleware creates a new IBCMiddleware instance given the keeper and underlying application. +// The underlying application must implement the required callback interfaces. +func NewIBCMiddleware( + app api.IBCModule, writeAckWrapper api.WriteAcknowledgementWrapper, + contractKeeper types.ContractKeeper, chanKeeperV2 types.ChannelKeeperV2, maxCallbackGas uint64, +) IBCMiddleware { + packetDataUnmarshalerApp, ok := app.(types.CallbacksCompatibleModuleV2) + if !ok { + panic(fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil))) + } + + if contractKeeper == nil { + panic(errors.New("contract keeper cannot be nil")) + } + + if writeAckWrapper == nil { + panic(errors.New("write acknowledgement wrapper cannot be nil")) + } + + if chanKeeperV2 == nil { + panic(errors.New("channel keeper v2 cannot be nil")) + } + + if maxCallbackGas == 0 { + panic(errors.New("maxCallbackGas cannot be zero")) + } + + return IBCMiddleware{ + app: packetDataUnmarshalerApp, + writeAckWrapper: writeAckWrapper, + contractKeeper: contractKeeper, + chanKeeperV2: chanKeeperV2, + maxCallbackGas: maxCallbackGas, + } +} + +// WithWriteAckWrapper sets the WriteAcknowledgementWrapper for the middleware. +func (im *IBCMiddleware) WithWriteAckWrapper(writeAckWrapper api.WriteAcknowledgementWrapper) { + im.writeAckWrapper = writeAckWrapper +} + +// GetWriteAckWrapper returns the WriteAckWrapper +func (im *IBCMiddleware) GetWriteAckWrapper() api.WriteAcknowledgementWrapper { + return im.writeAckWrapper +} + +// OnSendPacket implements source callbacks for sending packets. +// It defers to the underlying application and then calls the contract callback. +// If the contract callback returns an error, panics, or runs out of gas, then +// the packet send is rejected. +func (im IBCMiddleware) OnSendPacket( + ctx context.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + signer sdk.AccAddress, +) error { + err := im.app.OnSendPacket(ctx, sourceClient, destinationClient, sequence, payload, signer) + if err != nil { + return err + } + + packetData, err := im.app.UnmarshalPacketData(payload) + // OnSendPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return nil + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + cbData, err := types.GetCallbackData( + packetData, payload.GetVersion(), payload.GetSourcePort(), + sdkCtx.GasMeter().GasRemaining(), im.maxCallbackGas, types.SourceCallbackKey, + ) + // OnSendPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return nil + } + + callbackExecutor := func(cachedCtx sdk.Context) error { + return im.contractKeeper.IBCSendPacketCallback( + cachedCtx, payload.SourcePort, sourceClient, clienttypes.Height{}, 0, payload.Value, cbData.CallbackAddress, cbData.SenderAddress, payload.Version, + ) + } + + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeSendPacket, cbData, callbackExecutor) + // contract keeper is allowed to reject the packet send. + if err != nil { + return err + } + + types.EmitCallbackEvent(sdkCtx, payload.SourcePort, sourceClient, sequence, types.CallbackTypeSendPacket, cbData, nil) + return nil +} + +// OnRecvPacket implements the ReceivePacket destination callbacks for the ibc-callbacks middleware during +// synchronous packet acknowledgement. +// It defers to the underlying application and then calls the contract callback. +// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are +// reverted via a panic. +func (im IBCMiddleware) OnRecvPacket( + ctx context.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) channeltypesv2.RecvPacketResult { + recvResult := im.app.OnRecvPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) + // if ack is nil (asynchronous acknowledgements), then the callback will be handled in WriteAcknowledgement + // if ack is not successful, all state changes are reverted. If a packet cannot be received, then there is + // no need to execute a callback on the receiving chain. + if recvResult.Status == channeltypesv2.PacketStatus_Async || recvResult.Status == channeltypesv2.PacketStatus_Failure { + return recvResult + } + + packetData, err := im.app.UnmarshalPacketData(payload) + // OnRecvPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return recvResult + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + cbData, err := types.GetCallbackData( + packetData, payload.GetVersion(), payload.GetDestinationPort(), + sdkCtx.GasMeter().GasRemaining(), im.maxCallbackGas, types.DestinationCallbackKey, + ) + // OnRecvPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return recvResult + } + + callbackExecutor := func(cachedCtx sdk.Context) error { + // reconstruct a channel v1 packet from the v2 packet + // in order to preserve the same interface for the contract keeper + packetv1 := channeltypes.Packet{ + Sequence: sequence, + SourcePort: payload.SourcePort, + SourceChannel: sourceClient, + DestinationPort: payload.DestinationPort, + DestinationChannel: destinationClient, + Data: payload.Value, + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: 0, + } + // wrap the individual acknowledgement into the channeltypesv2.Acknowledgement since it implements the exported.Acknowledgement interface + // since we return early on failure, we are guaranteed that the ack is a successful acknowledgement + ack := channeltypesv2.NewAcknowledgement(recvResult.Acknowledgement) + return im.contractKeeper.IBCReceivePacketCallback(cachedCtx, packetv1, ack, cbData.CallbackAddress, payload.Version) + } + + // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeReceivePacket, cbData, callbackExecutor) + types.EmitCallbackEvent( + sdkCtx, payload.DestinationPort, destinationClient, sequence, + types.CallbackTypeReceivePacket, cbData, err, + ) + + return recvResult +} + +// OnAcknowledgementPacket implements source callbacks for acknowledgement packets. +// It defers to the underlying application and then calls the contract callback. +// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are +// reverted via a panic. +func (im IBCMiddleware) OnAcknowledgementPacket( + ctx context.Context, + sourceClient string, + destinationClient string, + sequence uint64, + acknowledgement []byte, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) error { + // we first call the underlying app to handle the acknowledgement + err := im.app.OnAcknowledgementPacket(ctx, sourceClient, destinationClient, sequence, acknowledgement, payload, relayer) + if err != nil { + return err + } + + packetData, err := im.app.UnmarshalPacketData(payload) + // OnAcknowledgementPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return nil + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + cbData, err := types.GetCallbackData( + packetData, payload.GetVersion(), payload.GetSourcePort(), + sdkCtx.GasMeter().GasRemaining(), im.maxCallbackGas, types.SourceCallbackKey, + ) + // OnAcknowledgementPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return nil + } + + callbackExecutor := func(cachedCtx sdk.Context) error { + // reconstruct a channel v1 packet from the v2 packet + // in order to preserve the same interface for the contract keeper + packetv1 := channeltypes.Packet{ + Sequence: sequence, + SourcePort: payload.SourcePort, + SourceChannel: sourceClient, + DestinationPort: payload.DestinationPort, + DestinationChannel: destinationClient, + Data: payload.Value, + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: 0, + } + // NOTE: The callback is receiving the acknowledgement that the application received for its particular payload. + // In the case of a successful acknowledgement, this will be the acknowledgement sent by the counterparty application for the given payload + // In the case of an error acknowledgement, this will be the sentinel error acknowledgement bytes defined by IBC v2 protocol. + // Thus, the contract must be aware that the sentinel error acknowledgement signals a failed receive + // and the contract must handle this error case and the corresponding success case (ie ack != ErrorAcknowledgement) accordingly. + return im.contractKeeper.IBCOnAcknowledgementPacketCallback( + cachedCtx, packetv1, acknowledgement, relayer, cbData.CallbackAddress, cbData.SenderAddress, payload.Version, + ) + } + + // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeAcknowledgementPacket, cbData, callbackExecutor) + types.EmitCallbackEvent( + sdkCtx, payload.SourcePort, sourceClient, sequence, + types.CallbackTypeAcknowledgementPacket, cbData, err, + ) + + return nil +} + +// OnTimeoutPacket implements timeout source callbacks for the ibc-callbacks middleware. +// It defers to the underlying application and then calls the contract callback. +// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are +// reverted via a panic. +// OnTimeoutPacket is executed when a packet has timed out on the receiving chain. +func (im IBCMiddleware) OnTimeoutPacket( + ctx context.Context, + sourceClient string, + destinationClient string, + sequence uint64, + payload channeltypesv2.Payload, + relayer sdk.AccAddress, +) error { + err := im.app.OnTimeoutPacket(ctx, sourceClient, destinationClient, sequence, payload, relayer) + if err != nil { + return err + } + + packetData, err := im.app.UnmarshalPacketData(payload) + if err != nil { + return err + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + cbData, err := types.GetCallbackData( + packetData, payload.GetVersion(), payload.GetSourcePort(), + sdkCtx.GasMeter().GasRemaining(), im.maxCallbackGas, types.SourceCallbackKey, + ) + // OnTimeoutPacket is not blocked if the packet does not opt-in to callbacks + if err != nil { + return nil + } + + callbackExecutor := func(cachedCtx sdk.Context) error { + // reconstruct a channel v1 packet from the v2 packet + // in order to preserve the same interface for the contract keeper + packetv1 := channeltypes.Packet{ + Sequence: sequence, + SourcePort: payload.SourcePort, + SourceChannel: sourceClient, + DestinationPort: payload.DestinationPort, + DestinationChannel: destinationClient, + Data: payload.Value, + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: 0, + } + return im.contractKeeper.IBCOnTimeoutPacketCallback( + cachedCtx, packetv1, relayer, cbData.CallbackAddress, cbData.SenderAddress, payload.Version, + ) + } + + // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeTimeoutPacket, cbData, callbackExecutor) + types.EmitCallbackEvent( + sdkCtx, payload.SourcePort, sourceClient, sequence, + types.CallbackTypeTimeoutPacket, cbData, err, + ) + + return nil +} + +// WriteAcknowledgement implements the ReceivePacket destination callbacks for the ibc-callbacks middleware +// during asynchronous packet acknowledgement. +// It defers to the underlying application and then calls the contract callback. +// If the contract callback runs out of gas and may be retried with a higher gas limit then the state changes are +// reverted via a panic. +func (im IBCMiddleware) WriteAcknowledgement( + ctx context.Context, + clientID string, + sequence uint64, + ack channeltypesv2.Acknowledgement, +) error { + packet, found := im.chanKeeperV2.GetAsyncPacket(ctx, clientID, sequence) + if !found { + return errorsmod.Wrapf(channeltypesv2.ErrInvalidAcknowledgement, "async packet not found for clientID (%s) and sequence (%d)", clientID, sequence) + } + + err := im.writeAckWrapper.WriteAcknowledgement(ctx, clientID, sequence, ack) + if err != nil { + return err + } + + // NOTE: use first payload as the payload that is being handled by callbacks middleware + // must reconsider if multipacket data gets supported with async packets + // TRACKING ISSUE: https://github.com/cosmos/ibc-go/issues/7950 + if len(packet.Payloads) != 1 { + return errorsmod.Wrapf(channeltypesv2.ErrInvalidAcknowledgement, "async packet has multiple payloads") + } + payload := packet.Payloads[0] + + packetData, err := im.app.UnmarshalPacketData(payload) + if err != nil { + return err + } + + sdkCtx := sdk.UnwrapSDKContext(ctx) + cbData, err := types.GetCallbackData( + packetData, payload.GetVersion(), payload.GetDestinationPort(), + sdkCtx.GasMeter().GasRemaining(), im.maxCallbackGas, types.DestinationCallbackKey, + ) + // WriteAcknowledgement is not blocked if the packet does not opt-in to callbacks + if err != nil { + return nil + } + + recvResult := channeltypesv2.RecvPacketResult{ + Status: channeltypesv2.PacketStatus_Success, + Acknowledgement: ack.AppAcknowledgements[0], + } + callbackExecutor := func(cachedCtx sdk.Context) error { + // reconstruct a channel v1 packet from the v2 packet + // in order to preserve the same interface for the contract keeper + packetv1 := channeltypes.Packet{ + Sequence: sequence, + SourcePort: payload.SourcePort, + SourceChannel: packet.SourceClient, + DestinationPort: payload.DestinationPort, + DestinationChannel: packet.DestinationClient, + Data: payload.Value, + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: 0, + } + // wrap the individual acknowledgement into the channeltypesv2.Acknowledgement since it implements the exported.Acknowledgement interface + var ack channeltypesv2.Acknowledgement + if recvResult.Status == channeltypesv2.PacketStatus_Failure { + ack = channeltypesv2.NewAcknowledgement(channeltypesv2.ErrorAcknowledgement[:]) + } else { + ack = channeltypesv2.NewAcknowledgement(recvResult.Acknowledgement) + } + return im.contractKeeper.IBCReceivePacketCallback( + cachedCtx, packetv1, ack, cbData.CallbackAddress, payload.Version, + ) + } + + // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions + err = internal.ProcessCallback(sdkCtx, types.CallbackTypeReceivePacket, cbData, callbackExecutor) + types.EmitCallbackEvent( + sdkCtx, payload.DestinationPort, clientID, sequence, + types.CallbackTypeReceivePacket, cbData, err, + ) + + return nil +} diff --git a/modules/apps/callbacks/v2/ibc_middleware_test.go b/modules/apps/callbacks/v2/ibc_middleware_test.go new file mode 100644 index 00000000000..7f2db397a81 --- /dev/null +++ b/modules/apps/callbacks/v2/ibc_middleware_test.go @@ -0,0 +1,843 @@ +package v2_test + +import ( + "fmt" + + errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp" + "github.com/cosmos/ibc-go/modules/apps/callbacks/types" + v2 "github.com/cosmos/ibc-go/modules/apps/callbacks/v2" + transfertypes "github.com/cosmos/ibc-go/v9/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v9/modules/core/04-channel/types" + channelkeeperv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/keeper" + channeltypesv2 "github.com/cosmos/ibc-go/v9/modules/core/04-channel/v2/types" + "github.com/cosmos/ibc-go/v9/modules/core/api" + ibcerrors "github.com/cosmos/ibc-go/v9/modules/core/errors" + ibctesting "github.com/cosmos/ibc-go/v9/testing" + ibcmock "github.com/cosmos/ibc-go/v9/testing/mock" + ibcmockv2 "github.com/cosmos/ibc-go/v9/testing/mock/v2" +) + +func (s *CallbacksTestSuite) TestNewIBCMiddleware() { + testCases := []struct { + name string + instantiateFn func() + expError error + }{ + { + "success", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas) + }, + nil, + }, + { + "panics with nil ics4wrapper", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, nil, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas) + }, + fmt.Errorf("write acknowledgement wrapper cannot be nil"), + }, + { + "panics with nil underlying app", + func() { + _ = v2.NewIBCMiddleware(nil, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas) + }, + fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil)), + }, + { + "panics with nil contract keeper", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, nil, &channelkeeperv2.Keeper{}, maxCallbackGas) + }, + fmt.Errorf("contract keeper cannot be nil"), + }, + { + "panics with nil channel v2 keeper", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, nil, maxCallbackGas) + }, + fmt.Errorf("channel keeper v2 cannot be nil"), + }, + { + "panics with zero maxCallbackGas", + func() { + _ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, uint64(0)) + }, + fmt.Errorf("maxCallbackGas cannot be zero"), + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + if tc.expError == nil { + s.Require().NotPanics(tc.instantiateFn, "unexpected panic: NewIBCMiddleware") + } else { + s.Require().PanicsWithError(tc.expError.Error(), tc.instantiateFn, "expected panic with error: ", tc.expError.Error()) + } + }) + } +} + +func (s *CallbacksTestSuite) TestWithWriteAckWrapper() { + s.setupChains() + + cbsMiddleware := v2.IBCMiddleware{} + s.Require().Nil(cbsMiddleware.GetWriteAckWrapper()) + + cbsMiddleware.WithWriteAckWrapper(s.chainA.App.GetIBCKeeper().ChannelKeeperV2) + writeAckWrapper := cbsMiddleware.GetWriteAckWrapper() + + s.Require().IsType((*channelkeeperv2.Keeper)(nil), writeAckWrapper) +} + +func (s *CallbacksTestSuite) TestSendPacket() { + var packetData transfertypes.FungibleTokenPacketDataV2 + + testCases := []struct { + name string + malleate func() + callbackType types.CallbackType + expPanic bool + expValue interface{} + }{ + { + "success", + func() {}, + types.CallbackTypeSendPacket, + false, + nil, + }, + { + "success: multiple denoms", + func() { + packetData.Tokens = append(packetData.Tokens, transfertypes.Token{ + Denom: transfertypes.NewDenom(ibctesting.SecondaryDenom), + Amount: ibctesting.SecondaryTestCoin.Amount.String(), + }) + }, + types.CallbackTypeSendPacket, + false, + nil, + }, + { + "success: no-op on callback data is not valid", + func() { + //nolint:goconst + packetData.Memo = `{"src_callback": {"address": ""}}` + }, + "none", // improperly formatted callback data should result in no callback execution + false, + nil, + }, + { + "failure: callback execution fails", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.ErrorContract) + }, + types.CallbackTypeSendPacket, + false, + ibcmock.MockApplicationCallbackError, // execution failure on SendPacket should prevent packet sends + }, + { + "failure: callback execution reach out of gas panic, but sufficient gas provided", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"400000"}}`, simapp.OogPanicContract) + }, + types.CallbackTypeSendPacket, + true, + storetypes.ErrorOutOfGas{Descriptor: fmt.Sprintf("mock %s callback oog panic", types.CallbackTypeSendPacket)}, + }, + { + "failure: callback execution reach out of gas error, but sufficient gas provided", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"400000"}}`, simapp.OogErrorContract) + }, + types.CallbackTypeSendPacket, + false, + errorsmod.Wrapf(types.ErrCallbackOutOfGas, "ibc %s callback out of gas", types.CallbackTypeSendPacket), + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + packetData = transfertypes.NewFungibleTokenPacketDataV2( + []transfertypes.Token{ + { + Denom: transfertypes.NewDenom(ibctesting.TestCoin.Denom), + Amount: ibctesting.TestCoin.Amount.String(), + }, + }, + s.chainA.SenderAccount.GetAddress().String(), + ibctesting.TestAccAddress, + fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract), + ibctesting.EmptyForwardingPacketData, + ) + + tc.malleate() + + payload := channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V2, transfertypes.EncodingProtobuf, + packetData.GetBytes(), + ) + + ctx := s.chainA.GetContext() + gasLimit := ctx.GasMeter().Limit() + + var err error + sendPacket := func() { + cbs := s.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + + err = cbs.OnSendPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, + 1, payload, s.chainA.SenderAccount.GetAddress()) + } + + expPass := tc.expValue == nil + switch { + case expPass: + sendPacket() + s.Require().Nil(err) + + expEvent, exists := GetExpectedEvent( + ctx, packetData, gasLimit, payload.Version, + transfertypes.PortID, s.path.EndpointA.ClientID, 1, types.CallbackTypeSendPacket, nil, + ) + if exists { + s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent) + } + + case tc.expPanic: + s.Require().PanicsWithValue(tc.expValue, sendPacket) + + default: + sendPacket() + s.Require().ErrorIs(err, tc.expValue.(error)) + } + + s.AssertHasExecutedExpectedCallback(tc.callbackType, expPass) + }) + } +} + +func (s *CallbacksTestSuite) TestOnAcknowledgementPacket() { + type expResult uint8 + const ( + noExecution expResult = iota + callbackFailed + callbackSuccess + ) + + var ( + packetData transfertypes.FungibleTokenPacketDataV2 + ack []byte + ctx sdk.Context + userGasLimit uint64 + ) + + panicError := fmt.Errorf("panic error") + + testCases := []struct { + name string + malleate func() + expResult expResult + expError error + }{ + { + "success", + func() {}, + callbackSuccess, + nil, + }, + { + "failure: underlying app OnAcknowledgePacket fails", + func() { + ack = []byte("invalid ack") + }, + noExecution, + ibcerrors.ErrUnknownRequest, + }, + { + "success: no-op on callback data is not valid", + func() { + //nolint:goconst + packetData.Memo = `{"src_callback": {"address": ""}}` + }, + noExecution, + nil, + }, + { + "failure: callback execution reach out of gas, but sufficient gas provided by relayer", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit) + }, + callbackFailed, + nil, + }, + { + "failure: callback execution panics on insufficient gas provided by relayer", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit) + + ctx = ctx.WithGasMeter(storetypes.NewGasMeter(300_000)) + }, + callbackFailed, + panicError, + }, + { + "failure: callback execution fails", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.ErrorContract) + }, + callbackFailed, + nil, // execution failure in OnAcknowledgement should not block acknowledgement processing + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + userGasLimit = 600000 + packetData = transfertypes.NewFungibleTokenPacketDataV2( + []transfertypes.Token{ + { + Denom: transfertypes.NewDenom(ibctesting.TestCoin.Denom), + Amount: ibctesting.TestCoin.Amount.String(), + }, + }, + ibctesting.TestAccAddress, + ibctesting.TestAccAddress, + fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.SuccessContract, userGasLimit), + ibctesting.EmptyForwardingPacketData, + ) + + ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement() + ctx = s.chainA.GetContext() + + // may malleate packetData, ack, and ctx + tc.malleate() + + payload := channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V2, transfertypes.EncodingProtobuf, + packetData.GetBytes(), + ) + + gasLimit := ctx.GasMeter().Limit() + + // callbacks module is routed as top level middleware + cbs := s.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + + onAcknowledgementPacket := func() error { + return cbs.OnAcknowledgementPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, 1, ack, payload, s.chainA.SenderAccount.GetAddress()) + } + + switch tc.expError { + case nil: + err := onAcknowledgementPacket() + s.Require().Nil(err) + + case panicError: + s.Require().PanicsWithValue(storetypes.ErrorOutOfGas{ + Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", types.CallbackTypeAcknowledgementPacket, userGasLimit), + }, func() { + _ = onAcknowledgementPacket() + }) + + default: + err := onAcknowledgementPacket() + s.Require().ErrorIs(err, tc.expError) + } + + sourceStatefulCounter := GetSimApp(s.chainA).MockContractKeeper.GetStateEntryCounter(s.chainA.GetContext()) + sourceCounters := GetSimApp(s.chainA).MockContractKeeper.Counters + + switch tc.expResult { + case noExecution: + s.Require().Len(sourceCounters, 0) + s.Require().Equal(uint8(0), sourceStatefulCounter) + + case callbackFailed: + s.Require().Len(sourceCounters, 1) + s.Require().Equal(1, sourceCounters[types.CallbackTypeAcknowledgementPacket]) + s.Require().Equal(uint8(0), sourceStatefulCounter) + + case callbackSuccess: + s.Require().Len(sourceCounters, 1) + s.Require().Equal(1, sourceCounters[types.CallbackTypeAcknowledgementPacket]) + s.Require().Equal(uint8(1), sourceStatefulCounter) + + expEvent, exists := GetExpectedEvent( + ctx, packetData, gasLimit, payload.Version, + payload.SourcePort, s.path.EndpointA.ClientID, 1, types.CallbackTypeAcknowledgementPacket, nil, + ) + s.Require().True(exists) + s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent) + } + }) + } +} + +func (s *CallbacksTestSuite) TestOnTimeoutPacket() { + type expResult uint8 + const ( + noExecution expResult = iota + callbackFailed + callbackSuccess + ) + + var ( + packetData transfertypes.FungibleTokenPacketDataV2 + ctx sdk.Context + ) + + testCases := []struct { + name string + malleate func() + expResult expResult + expValue interface{} + }{ + { + "success", + func() {}, + callbackSuccess, + nil, + }, + { + "failure: underlying app OnTimeoutPacket fails", + func() { + packetData.Tokens = nil + }, + noExecution, + transfertypes.ErrInvalidAmount, + }, + { + "success: no-op on callback data is not valid", + func() { + //nolint:goconst + packetData.Memo = `{"src_callback": {"address": ""}}` + }, + noExecution, + nil, + }, + { + "failure: callback execution reach out of gas, but sufficient gas provided by relayer", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"400000"}}`, simapp.OogPanicContract) + }, + callbackFailed, + nil, + }, + { + "failure: callback execution panics on insufficient gas provided by relayer", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.OogPanicContract) + + ctx = ctx.WithGasMeter(storetypes.NewGasMeter(300_000)) + }, + callbackFailed, + storetypes.ErrorOutOfGas{ + Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", types.CallbackTypeTimeoutPacket, maxCallbackGas), + }, + }, + { + "failure: callback execution fails", + func() { + packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.ErrorContract) + }, + callbackFailed, + nil, // execution failure in OnTimeout should not block timeout processing + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + // NOTE: we call send packet so transfer is setup with the correct logic to + // succeed on timeout + userGasLimit := 600_000 + timeoutTimestamp := uint64(s.chainB.GetContext().BlockTime().Unix()) + packetData = transfertypes.NewFungibleTokenPacketDataV2( + []transfertypes.Token{ + { + Denom: transfertypes.NewDenom(ibctesting.TestCoin.Denom), + Amount: ibctesting.TestCoin.Amount.String(), + }, + }, + s.chainA.SenderAccount.GetAddress().String(), + ibctesting.TestAccAddress, + fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.SuccessContract, userGasLimit), + ibctesting.EmptyForwardingPacketData, + ) + + payload := channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V2, transfertypes.EncodingProtobuf, + packetData.GetBytes(), + ) + + packet, err := s.path.EndpointA.MsgSendPacket(timeoutTimestamp, payload) + s.Require().NoError(err) + + ctx = s.chainA.GetContext() + gasLimit := ctx.GasMeter().Limit() + + tc.malleate() + + // update packet data in payload after malleate + payload.Value = packetData.GetBytes() + + // callbacks module is routed as top level middleware + cbs := s.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + + onTimeoutPacket := func() error { + return cbs.OnTimeoutPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, 1, payload, s.chainA.SenderAccount.GetAddress()) + } + + switch expValue := tc.expValue.(type) { + case nil: + err := onTimeoutPacket() + s.Require().Nil(err) + case error: + err := onTimeoutPacket() + s.Require().ErrorIs(err, expValue) + default: + s.Require().PanicsWithValue(tc.expValue, func() { + _ = onTimeoutPacket() + }) + } + + sourceStatefulCounter := GetSimApp(s.chainA).MockContractKeeper.GetStateEntryCounter(s.chainA.GetContext()) + sourceCounters := GetSimApp(s.chainA).MockContractKeeper.Counters + + // account for SendPacket succeeding + switch tc.expResult { + case noExecution: + s.Require().Len(sourceCounters, 1) + s.Require().Equal(uint8(1), sourceStatefulCounter) + + case callbackFailed: + s.Require().Len(sourceCounters, 2) + s.Require().Equal(1, sourceCounters[types.CallbackTypeTimeoutPacket]) + s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket]) + s.Require().Equal(uint8(1), sourceStatefulCounter) + + case callbackSuccess: + s.Require().Len(sourceCounters, 2) + s.Require().Equal(1, sourceCounters[types.CallbackTypeTimeoutPacket]) + s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket]) + s.Require().Equal(uint8(2), sourceStatefulCounter) + + expEvent, exists := GetExpectedEvent( + ctx, packetData, gasLimit, payload.Version, + payload.SourcePort, s.path.EndpointA.ClientID, packet.Sequence, types.CallbackTypeTimeoutPacket, nil, + ) + s.Require().True(exists) + s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent) + } + }) + } +} + +func (s *CallbacksTestSuite) TestOnRecvPacket() { + type expResult uint8 + type expRecvStatus uint8 + const ( + noExecution expResult = iota + callbackFailed + callbackPanic + callbackSuccess + ) + const ( + success expRecvStatus = iota + panics + failure + ) + + var ( + packetData transfertypes.FungibleTokenPacketDataV2 + ctx sdk.Context + userGasLimit uint64 + ) + + testCases := []struct { + name string + malleate func() + expResult expResult + expRecvStatus expRecvStatus + }{ + { + "success", + func() {}, + callbackSuccess, + success, + }, + { + "failure: underlying app OnRecvPacket fails", + func() { + packetData.Tokens = nil + }, + noExecution, + failure, + }, + { + "success: no-op on callback data is not valid", + func() { + //nolint:goconst + packetData.Memo = `{"dest_callback": {"address": ""}}` + }, + noExecution, + success, + }, + { + "failure: callback execution reach out of gas, but sufficient gas provided by relayer", + func() { + packetData.Memo = fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit) + }, + callbackFailed, + success, + }, + { + "failure: callback execution panics on insufficient gas provided by relayer", + func() { + packetData.Memo = fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit) + + ctx = ctx.WithGasMeter(storetypes.NewGasMeter(300_000)) + }, + callbackFailed, + panics, + }, + { + "failure: callback execution fails", + func() { + packetData.Memo = fmt.Sprintf(`{"dest_callback": {"address":"%s"}}`, simapp.ErrorContract) + }, + callbackFailed, + success, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + // set user gas limit above panic level in mock contract keeper + userGasLimit = 600_000 + packetData = transfertypes.NewFungibleTokenPacketDataV2( + []transfertypes.Token{ + { + Denom: transfertypes.NewDenom(ibctesting.TestCoin.Denom), + Amount: ibctesting.TestCoin.Amount.String(), + }, + }, + ibctesting.TestAccAddress, + s.chainB.SenderAccount.GetAddress().String(), + fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, ibctesting.TestAccAddress, userGasLimit), + ibctesting.EmptyForwardingPacketData, + ) + + payload := channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V2, transfertypes.EncodingProtobuf, + packetData.GetBytes(), + ) + + ctx = s.chainB.GetContext() + gasLimit := ctx.GasMeter().Limit() + + tc.malleate() + + // update packet data in payload after malleate + payload.Value = packetData.GetBytes() + + // callbacks module is routed as top level middleware + cbs := s.chainB.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + + onRecvPacket := func() channeltypesv2.RecvPacketResult { + return cbs.OnRecvPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, 1, payload, s.chainB.SenderAccount.GetAddress()) + } + + switch tc.expRecvStatus { + case success: + recvResult := onRecvPacket() + s.Require().Equal(channeltypesv2.PacketStatus_Success, recvResult.Status) + + case panics: + s.Require().PanicsWithValue(storetypes.ErrorOutOfGas{ + Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", types.CallbackTypeReceivePacket, userGasLimit), + }, func() { + _ = onRecvPacket() + }) + + default: + recvResult := onRecvPacket() + s.Require().Equal(channeltypesv2.PacketStatus_Failure, recvResult.Status) + } + + destStatefulCounter := GetSimApp(s.chainB).MockContractKeeper.GetStateEntryCounter(s.chainB.GetContext()) + destCounters := GetSimApp(s.chainB).MockContractKeeper.Counters + + switch tc.expResult { + case noExecution: + s.Require().Len(destCounters, 0) + s.Require().Equal(uint8(0), destStatefulCounter) + + case callbackFailed: + s.Require().Len(destCounters, 1) + s.Require().Equal(1, destCounters[types.CallbackTypeReceivePacket]) + s.Require().Equal(uint8(0), destStatefulCounter) + + case callbackSuccess: + s.Require().Len(destCounters, 1) + s.Require().Equal(1, destCounters[types.CallbackTypeReceivePacket]) + s.Require().Equal(uint8(1), destStatefulCounter) + + expEvent, exists := GetExpectedEvent( + ctx, packetData, gasLimit, payload.Version, + payload.DestinationPort, s.path.EndpointB.ClientID, 1, types.CallbackTypeReceivePacket, nil, + ) + s.Require().True(exists) + s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent) + } + }) + } +} + +func (s *CallbacksTestSuite) TestWriteAcknowledgement() { + var ( + packetData transfertypes.FungibleTokenPacketDataV2 + destClient string + ctx sdk.Context + ack channeltypesv2.Acknowledgement + multiPayload bool + ) + + successAck := channeltypesv2.NewAcknowledgement(channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement()) + + testCases := []struct { + name string + malleate func() + callbackType types.CallbackType + expError error + }{ + { + "success", + func() { + ack = successAck + }, + types.CallbackTypeReceivePacket, + nil, + }, + { + "success: no-op on callback data is not valid", + func() { + packetData.Memo = `{"dest_callback": {"address": ""}}` + }, + "none", // improperly formatted callback data should result in no callback execution + nil, + }, + { + "failure: ics4Wrapper WriteAcknowledgement call fails", + func() { + destClient = "invalid-client" + }, + "none", + channeltypesv2.ErrInvalidAcknowledgement, + }, + { + "failure: multipayload should fail", + func() { + multiPayload = true + }, + "none", + channeltypesv2.ErrInvalidAcknowledgement, + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + s.SetupTest() + + // set user gas limit above panic level in mock contract keeper + packetData = transfertypes.NewFungibleTokenPacketDataV2( + []transfertypes.Token{ + { + Denom: transfertypes.NewDenom(ibctesting.TestCoin.Denom), + Amount: ibctesting.TestCoin.Amount.String(), + }, + }, + ibctesting.TestAccAddress, + s.chainB.SenderAccount.GetAddress().String(), + fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"600000"}}`, ibctesting.TestAccAddress), + ibctesting.EmptyForwardingPacketData, + ) + + ctx = s.chainB.GetContext() + gasLimit := ctx.GasMeter().Limit() + destClient = s.path.EndpointB.ClientID + + tc.malleate() + + payload := channeltypesv2.NewPayload( + transfertypes.PortID, transfertypes.PortID, + transfertypes.V2, transfertypes.EncodingProtobuf, + packetData.GetBytes(), + ) + timeoutTimestamp := uint64(s.chainB.GetContext().BlockTime().Unix()) + var packet channeltypesv2.Packet + if multiPayload { + packet = channeltypesv2.NewPacket( + 1, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, + timeoutTimestamp, payload, payload, + ) + } else { + packet = channeltypesv2.NewPacket( + 1, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, + timeoutTimestamp, payload, + ) + } + // mock async receive manually so WriteAcknowledgement can pass + s.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetAsyncPacket(ctx, packet.DestinationClient, packet.Sequence, packet) + s.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetPacketReceipt(ctx, packet.DestinationClient, packet.Sequence) + + // callbacks module is routed as top level middleware + cbs := s.chainB.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort) + mw, ok := cbs.(api.WriteAcknowledgementWrapper) + s.Require().True(ok) + + err := mw.WriteAcknowledgement(ctx, destClient, packet.Sequence, ack) + + expPass := tc.expError == nil + s.AssertHasExecutedExpectedCallback(tc.callbackType, expPass) + + if expPass { + s.Require().NoError(err) + + expEvent, exists := GetExpectedEvent( + ctx, packetData, gasLimit, payload.Version, + payload.DestinationPort, packet.DestinationClient, packet.Sequence, types.CallbackTypeReceivePacket, nil, + ) + if exists { + s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent) + } + + } else { + s.Require().ErrorIs(err, tc.expError) + } + }) + } +} diff --git a/modules/apps/callbacks/v2/v2_test.go b/modules/apps/callbacks/v2/v2_test.go new file mode 100644 index 00000000000..b93886174ca --- /dev/null +++ b/modules/apps/callbacks/v2/v2_test.go @@ -0,0 +1,170 @@ +package v2_test + +import ( + "encoding/json" + "errors" + "fmt" + "testing" + + dbm "github.com/cosmos/cosmos-db" + "github.com/stretchr/testify/suite" + + "cosmossdk.io/log" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp" + "github.com/cosmos/ibc-go/modules/apps/callbacks/types" + ibctesting "github.com/cosmos/ibc-go/v9/testing" +) + +const maxCallbackGas = uint64(1000000) + +func init() { + ibctesting.DefaultTestingAppInit = SetupTestingApp +} + +// SetupTestingApp provides the duplicated simapp which is specific to the callbacks module on chain creation. +func SetupTestingApp() (ibctesting.TestingApp, map[string]json.RawMessage) { + db := dbm.NewMemDB() + app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.AppOptionsMap{}) + return app, app.DefaultGenesis() +} + +// GetSimApp returns the duplicated SimApp from within the callbacks directory. +// This must be used instead of chain.GetSimApp() for tests within this directory. +func GetSimApp(chain *ibctesting.TestChain) *simapp.SimApp { + app, ok := chain.App.(*simapp.SimApp) + if !ok { + panic(errors.New("chain is not a simapp.SimApp")) + } + return app +} + +// CallbacksTestSuite defines the needed instances and methods to test callbacks +type CallbacksTestSuite struct { + suite.Suite + + coordinator *ibctesting.Coordinator + + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain + + path *ibctesting.Path +} + +// setupChains sets up a coordinator with 2 test chains. +func (s *CallbacksTestSuite) setupChains() { + s.coordinator = ibctesting.NewCoordinator(s.T(), 2) + s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1)) + s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2)) + s.path = ibctesting.NewPath(s.chainA, s.chainB) +} + +// SetupTransferTest sets up a eureka path between chainA and chainB +func (s *CallbacksTestSuite) SetupTest() { + s.setupChains() + + s.path.SetupV2() +} + +// AssertHasExecutedExpectedCallback checks the stateful entries and counters based on callbacktype. +// It assumes that the source chain is chainA and the destination chain is chainB. +func (s *CallbacksTestSuite) AssertHasExecutedExpectedCallback(callbackType types.CallbackType, expSuccess bool) { + var expStatefulEntries uint8 + if expSuccess { + // if the callback is expected to be successful, + // we expect at least one state entry + expStatefulEntries = 1 + } + + sourceStatefulCounter := GetSimApp(s.chainA).MockContractKeeper.GetStateEntryCounter(s.chainA.GetContext()) + destStatefulCounter := GetSimApp(s.chainB).MockContractKeeper.GetStateEntryCounter(s.chainB.GetContext()) + + switch callbackType { + case "none": + s.Require().Equal(uint8(0), sourceStatefulCounter) + s.Require().Equal(uint8(0), destStatefulCounter) + + case types.CallbackTypeSendPacket: + s.Require().Equal(expStatefulEntries, sourceStatefulCounter, "unexpected stateful entry amount for source send packet callback") + s.Require().Equal(uint8(0), destStatefulCounter) + + case types.CallbackTypeAcknowledgementPacket, types.CallbackTypeTimeoutPacket: + expStatefulEntries *= 2 // expect OnAcknowledgement/OnTimeout to be successful as well as the initial SendPacket + s.Require().Equal(expStatefulEntries, sourceStatefulCounter, "unexpected stateful entry amount for source acknowledgement/timeout callbacks") + s.Require().Equal(uint8(0), destStatefulCounter) + + case types.CallbackTypeReceivePacket: + s.Require().Equal(uint8(0), sourceStatefulCounter) + s.Require().Equal(expStatefulEntries, destStatefulCounter) + + default: + s.FailNow(fmt.Sprintf("invalid callback type %s", callbackType)) + } + + s.AssertCallbackCounters(callbackType) +} + +func (s *CallbacksTestSuite) AssertCallbackCounters(callbackType types.CallbackType) { + sourceCounters := GetSimApp(s.chainA).MockContractKeeper.Counters + destCounters := GetSimApp(s.chainB).MockContractKeeper.Counters + + switch callbackType { + case "none": + s.Require().Len(sourceCounters, 0) + s.Require().Len(destCounters, 0) + + case types.CallbackTypeSendPacket: + s.Require().Len(sourceCounters, 1) + s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket]) + + case types.CallbackTypeAcknowledgementPacket: + s.Require().Len(sourceCounters, 2) + s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket]) + s.Require().Equal(1, sourceCounters[types.CallbackTypeAcknowledgementPacket]) + + s.Require().Len(destCounters, 0) + + case types.CallbackTypeReceivePacket: + s.Require().Len(sourceCounters, 0) + s.Require().Len(destCounters, 1) + s.Require().Equal(1, destCounters[types.CallbackTypeReceivePacket]) + + case types.CallbackTypeTimeoutPacket: + s.Require().Len(sourceCounters, 2) + s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket]) + s.Require().Equal(1, sourceCounters[types.CallbackTypeTimeoutPacket]) + + s.Require().Len(destCounters, 0) + + default: + s.FailNow(fmt.Sprintf("invalid callback type %s", callbackType)) + } +} + +// GetExpectedEvent returns the expected event for a callback. +func GetExpectedEvent( + ctx sdk.Context, packetData interface{}, remainingGas uint64, version string, + eventPortID, eventChannelID string, seq uint64, callbackType types.CallbackType, expError error, +) (abci.Event, bool) { + callbackKey := types.SourceCallbackKey + if callbackType == types.CallbackTypeReceivePacket { + callbackKey = types.DestinationCallbackKey + } + callbackData, err := types.GetCallbackData(packetData, version, eventPortID, remainingGas, maxCallbackGas, callbackKey) + if err != nil { + return abci.Event{}, false + } + + newCtx := sdk.Context{}.WithEventManager(sdk.NewEventManager()) + types.EmitCallbackEvent(newCtx, eventPortID, eventChannelID, seq, callbackType, callbackData, expError) + return newCtx.EventManager().Events().ToABCIEvents()[0], true +} + +func TestIBCCallbacksTestSuite(t *testing.T) { + suite.Run(t, new(CallbacksTestSuite)) +} diff --git a/modules/apps/transfer/v2/ibc_module.go b/modules/apps/transfer/v2/ibc_module.go index 395b842fddd..3a68da435a1 100644 --- a/modules/apps/transfer/v2/ibc_module.go +++ b/modules/apps/transfer/v2/ibc_module.go @@ -202,3 +202,9 @@ func (im *IBCModule) OnAcknowledgementPacket(ctx context.Context, sourceChannel return nil } + +// UnmarshalPacketData unmarshals the ICS20 packet data based on the version and encoding +// it implements the PacketDataUnmarshaler interface +func (*IBCModule) UnmarshalPacketData(payload channeltypesv2.Payload) (interface{}, error) { + return types.UnmarshalPacketData(payload.Value, payload.Version, payload.Encoding) +} diff --git a/modules/core/04-channel/v2/types/acknowledgement.go b/modules/core/04-channel/v2/types/acknowledgement.go index ef59f221f77..22e67fb7d0d 100644 --- a/modules/core/04-channel/v2/types/acknowledgement.go +++ b/modules/core/04-channel/v2/types/acknowledgement.go @@ -1,12 +1,20 @@ package types import ( + "bytes" "crypto/sha256" + proto "github.com/cosmos/gogoproto/proto" + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/ibc-go/v9/modules/core/exported" ) -var ErrorAcknowledgement = sha256.Sum256([]byte("UNIVERSAL_ERROR_ACKNOWLEDGEMENT")) +var ( + ErrorAcknowledgement = sha256.Sum256([]byte("UNIVERSAL_ERROR_ACKNOWLEDGEMENT")) + _ exported.Acknowledgement = &Acknowledgement{} +) // NewAcknowledgement creates a new Acknowledgement containing the provided app acknowledgements. func NewAcknowledgement(appAcknowledgements ...[]byte) Acknowledgement { @@ -27,3 +35,18 @@ func (ack Acknowledgement) Validate() error { return nil } + +// Success returns true if the acknowledgement is successful +// it implements the exported.Acknowledgement interface +func (ack Acknowledgement) Success() bool { + return !bytes.Equal(ack.AppAcknowledgements[0], ErrorAcknowledgement[:]) +} + +// Acknowledgement returns the acknowledgement bytes to implement the acknowledgement interface +func (ack Acknowledgement) Acknowledgement() []byte { + bz, err := proto.Marshal(&ack) + if err != nil { + panic(err) + } + return bz +} diff --git a/modules/core/api/module.go b/modules/core/api/module.go index 4d26947f42a..48013e2301c 100644 --- a/modules/core/api/module.go +++ b/modules/core/api/module.go @@ -16,8 +16,8 @@ type IBCModule interface { // for this specific application. OnSendPacket( ctx context.Context, - sourceChannel string, - destinationChannel string, + sourceClient string, + destinationClient string, sequence uint64, payload channeltypesv2.Payload, signer sdk.AccAddress, @@ -25,8 +25,8 @@ type IBCModule interface { OnRecvPacket( ctx context.Context, - sourceChannel string, - destinationChannel string, + sourceClient string, + destinationClient string, sequence uint64, payload channeltypesv2.Payload, relayer sdk.AccAddress, @@ -35,8 +35,8 @@ type IBCModule interface { // OnTimeoutPacket is executed when a packet has timed out on the receiving chain. OnTimeoutPacket( ctx context.Context, - sourceChannel string, - destinationChannel string, + sourceClient string, + destinationClient string, sequence uint64, payload channeltypesv2.Payload, relayer sdk.AccAddress, @@ -45,11 +45,29 @@ type IBCModule interface { // OnAcknowledgementPacket is executed when a packet gets acknowledged OnAcknowledgementPacket( ctx context.Context, - sourceChannel string, - destinationChannel string, + sourceClient string, + destinationClient string, sequence uint64, acknowledgement []byte, payload channeltypesv2.Payload, relayer sdk.AccAddress, ) error } + +type WriteAcknowledgementWrapper interface { + // WriteAcknowledgement writes the acknowledgement for an async acknowledgement + WriteAcknowledgement( + ctx context.Context, + srcClientID string, + sequence uint64, + ack channeltypesv2.Acknowledgement, + ) error +} + +// 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 + // the payload is provided and the packet data interface is returned + UnmarshalPacketData(payload channeltypesv2.Payload) (interface{}, error) +} diff --git a/testing/mock/v2/ibc_module.go b/testing/mock/v2/ibc_module.go index 20a63bc2b83..082aececbe2 100644 --- a/testing/mock/v2/ibc_module.go +++ b/testing/mock/v2/ibc_module.go @@ -67,3 +67,10 @@ func (im IBCModule) OnTimeoutPacket(ctx context.Context, sourceChannel string, d } return nil } + +func (IBCModule) UnmarshalPacketData(payload channeltypesv2.Payload) (interface{}, error) { + if bytes.Equal(payload.Value, mockv1.MockPacketData) { + return mockv1.MockPacketData, nil + } + return nil, mockv1.MockApplicationCallbackError +}