Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callbacks Eureka #7934

Merged
merged 20 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/apps/callbacks/callbacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
19 changes: 0 additions & 19 deletions modules/apps/callbacks/export_test.go

This file was deleted.

60 changes: 5 additions & 55 deletions modules/apps/callbacks/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"

errorsmod "cosmossdk.io/errors"
storetypes "cosmossdk.io/store/types"

sdk "github.com/cosmos/cosmos-sdk/types"

Expand Down Expand Up @@ -114,7 +113,7 @@ func (im IBCMiddleware) SendPacket(
)
}

err = im.processCallback(sdkCtx, types.CallbackTypeSendPacket, callbackData, callbackExecutor)
err = types.ProcessCallback(sdkCtx, types.CallbackTypeSendPacket, callbackData, callbackExecutor)
// contract keeper is allowed to reject the packet send.
if err != nil {
return 0, err
Expand Down Expand Up @@ -158,7 +157,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 = types.ProcessCallback(sdkCtx, types.CallbackTypeAcknowledgementPacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
sdkCtx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
types.CallbackTypeAcknowledgementPacket, callbackData, err,
Expand Down Expand Up @@ -192,7 +191,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 = types.ProcessCallback(sdkCtx, types.CallbackTypeTimeoutPacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
sdkCtx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
types.CallbackTypeTimeoutPacket, callbackData, err,
Expand Down Expand Up @@ -229,7 +228,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 = types.ProcessCallback(sdkCtx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
sdkCtx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(),
types.CallbackTypeReceivePacket, callbackData, err,
Expand Down Expand Up @@ -272,7 +271,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 = types.ProcessCallback(sdkCtx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
types.EmitCallbackEvent(
sdkCtx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(),
types.CallbackTypeReceivePacket, callbackData, err,
Expand All @@ -281,55 +280,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,
Expand Down
17 changes: 6 additions & 11 deletions modules/apps/callbacks/ibc_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,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 {
Expand Down Expand Up @@ -391,7 +391,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)
Expand Down Expand Up @@ -554,7 +554,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)
Expand Down Expand Up @@ -723,7 +723,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)
Expand Down Expand Up @@ -823,7 +823,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 {
Expand Down Expand Up @@ -953,13 +953,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 = types.ProcessCallback(ctx, callbackType, callbackData, callbackExecutor)
}

expPass := tc.expValue == nil
Expand Down
9 changes: 9 additions & 0 deletions modules/apps/callbacks/testing/simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import (
cmted25519 "github.com/cometbft/cometbft/crypto/ed25519"

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"
Expand All @@ -105,8 +106,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"
Expand Down Expand Up @@ -460,6 +463,7 @@ func NewSimApp(

// Create IBC Router
ibcRouter := porttypes.NewRouter()
ibcRouterV2 := ibcapi.NewRouter()

// Middleware Stacks
maxCallbackGas := uint64(1_000_000)
Expand Down Expand Up @@ -568,8 +572,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), nil, 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()
Expand Down
67 changes: 63 additions & 4 deletions modules/apps/callbacks/types/callbacks.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package types

import (
"fmt"
"strconv"
"strings"

errorsmod "cosmossdk.io/errors"
storetypes "cosmossdk.io/store/types"

sdk "github.com/cosmos/cosmos-sdk/types"

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"
)

Expand Down Expand Up @@ -51,6 +54,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.
Expand Down Expand Up @@ -82,7 +92,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.
Expand All @@ -96,14 +106,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,
Expand Down Expand Up @@ -146,6 +156,55 @@ func getCallbackData(
}, 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 ProcessCallback(
ctx sdk.Context, callbackType CallbackType,
callbackData 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 == CallbackTypeSendPacket {
panic(r)
}
err = errorsmod.Wrapf(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(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
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't be part of the public API. Can we create an internal package that can be shared between v1 and v2?

func computeExecAndCommitGasLimit(callbackData map[string]interface{}, remainingGas, maxGas uint64) (uint64, uint64) {
// get the gas limit from the callback data
commitGasLimit := getUserDefinedGasLimit(callbackData)
Expand Down
13 changes: 13 additions & 0 deletions modules/apps/callbacks/types/expected_keepers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
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"
"github.com/cosmos/ibc-go/v9/modules/core/api"
ibcexported "github.com/cosmos/ibc-go/v9/modules/core/exported"
)

Expand Down Expand Up @@ -97,3 +101,12 @@ type ContractKeeper interface {
version string,
) error
}

type ChannelKeeperV2 interface {
api.WriteAcknowledgementWrapper
GetAsyncPacket(
ctx context.Context,
clientID string,
sequence uint64,
) (channeltypesv2.Packet, bool)
}
8 changes: 0 additions & 8 deletions modules/apps/callbacks/types/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading