Skip to content

Commit

Permalink
callbacks middleware compatible for eureka
Browse files Browse the repository at this point in the history
  • Loading branch information
AdityaSripal committed Jan 30, 2025
1 parent 4ff42e4 commit 0219b43
Show file tree
Hide file tree
Showing 8 changed files with 506 additions and 100 deletions.
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
7 changes: 1 addition & 6 deletions modules/apps/callbacks/ibc_middleware_test.go
Original file line number Diff line number Diff line change
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
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
porttypes.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
}

func computeExecAndCommitGasLimit(callbackData map[string]interface{}, remainingGas, maxGas uint64) (uint64, uint64) {
// get the gas limit from the callback data
commitGasLimit := getUserDefinedGasLimit(callbackData)
Expand Down
101 changes: 101 additions & 0 deletions modules/apps/callbacks/types/expected_keepers.go
Original file line number Diff line number Diff line change
@@ -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"
)

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

// ContractKeeper defines the entry points exposed to the VM module which invokes a smart contract
type ContractKeeperV2 interface {
// IBCSendPacketCallback is called in the source chain when a PacketSend is executed. The
// packetSenderAddress is determined by the underlying module, and may be empty if the sender is
// unknown or undefined. The contract is expected to handle the callback within the user defined
// gas limit, and handle any errors, or panics gracefully.
// This entry point is called with a cached context. If an error is returned, then the changes in
// this context will not be persisted, and the error will be propagated to the underlying IBC
// application, resulting in a packet send failure.
//
// Implementations are provided with the packetSenderAddress and MAY choose to use this to perform
// validation on the origin of a given packet. It is recommended to perform the same validation
// on all source chain callbacks (SendPacket, AcknowledgementPacket, TimeoutPacket). This
// defensively guards against exploits due to incorrectly wired SendPacket ordering in IBC stacks.
//
// The version provided is the base application version for the given packet send. This allows
// contracts to determine how to unmarshal the packetData.
IBCSendPacketCallback(
cachedCtx sdk.Context,
sourceClient string,
sequence uint64,
payload channeltypesv2.Payload,
contractAddress,
packetSenderAddress string,
) error
// IBCOnAcknowledgementPacketCallback is called in the source chain when a packet acknowledgement
// is received. The packetSenderAddress is determined by the underlying module, and may be empty if
// the sender is unknown or undefined. The contract is expected to handle the callback within the
// user defined gas limit, and handle any errors, or panics gracefully.
// This entry point is called with a cached context. If an error is returned, then the changes in
// this context will not be persisted, but the packet lifecycle will not be blocked.
//
// Implementations are provided with the packetSenderAddress and MAY choose to use this to perform
// validation on the origin of a given packet. It is recommended to perform the same validation
// on all source chain callbacks (SendPacket, AcknowledgementPacket, TimeoutPacket). This
// defensively guards against exploits due to incorrectly wired SendPacket ordering in IBC stacks.
//
// The version provided is the base application version for the given packet send. This allows
// contracts to determine how to unmarshal the packetData.
IBCOnAcknowledgementPacketCallback(
cachedCtx sdk.Context,
sourceClient string,
sequence uint64,
acknowledgement []byte,
payload channeltypesv2.Payload,
relayer sdk.AccAddress,
contractAddress,
packetSenderAddress string,
) error
// IBCOnTimeoutPacketCallback is called in the source chain when a packet is not received before
// the timeout height. The packetSenderAddress is determined by the underlying module, and may be
// empty if the sender is unknown or undefined. The contract is expected to handle the callback
// within the user defined gas limit, and handle any error, out of gas, or panics gracefully.
// This entry point is called with a cached context. If an error is returned, then the changes in
// this context will not be persisted, but the packet lifecycle will not be blocked.
//
// Implementations are provided with the packetSenderAddress and MAY choose to use this to perform
// validation on the origin of a given packet. It is recommended to perform the same validation
// on all source chain callbacks (SendPacket, AcknowledgementPacket, TimeoutPacket). This
// defensively guards against exploits due to incorrectly wired SendPacket ordering in IBC stacks.
//
// The version provided is the base application version for the given packet send. This allows
// contracts to determine how to unmarshal the packetData.
IBCOnTimeoutPacketCallback(
cachedCtx sdk.Context,
sourceClient string,
sequence uint64,
payload channeltypesv2.Payload,
relayer sdk.AccAddress,
contractAddress,
packetSenderAddress string,
) error
// IBCReceivePacketCallback is called in the destination chain when a packet acknowledgement is written.
// The contract is expected to handle the callback within the user defined gas limit, and handle any errors,
// out of gas, or panics gracefully.
// This entry point is called with a cached context. If an error is returned, then the changes in
// this context will not be persisted, but the packet lifecycle will not be blocked.
//
// The version provided is the base application version for the given packet send. This allows
// contracts to determine how to unmarshal the packetData.
IBCReceivePacketCallback(
cachedCtx sdk.Context,
destinationClient string,
sequence uint64,
payload channeltypesv2.Payload,
recvResult channeltypesv2.RecvPacketResult,
contractAddress string,
) error
}

type ChannelKeeperV2 interface {
GetAsyncPacket(
ctx context.Context,
clientId string,

Check failure on line 197 in modules/apps/callbacks/types/expected_keepers.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: interface method parameter clientId should be clientID (revive)
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

0 comments on commit 0219b43

Please sign in to comment.