变更记录

  • 10-08-2022: 初始草案
  • 22-03-2023: 已合并
  • 13-09-2023: 根据实现中的决策更新
  • 24-02-2025: RecvPacket 回调错误现会返回错误确认

状态

已接受,中间件已实现

背景

IBC 在设计上支持核心 IBC 与 IBC 应用之间的回调。IBC 应用会向核心 IBC 发送数据包。当数据包生命周期的结果最终确定为确认或超时后,核心 IBC 会调用 IBC 应用上的回调,使 IBC 应用能够基于该结果执行操作(例如为 ICS-20 解托管代币)。 这种设置对于与 IBC 应用交互的链下用户而言运行良好。 现在,我们看到有需求希望次级应用(例如智能合约、模块)能够作为其状态机逻辑的一部分调用 IBC 应用,然后基于数据包结果执行一些操作。或者在从 IBC 收到数据包后,于接收时执行一些逻辑。 示例用例:
  • 发送一个 ICS-20 数据包,如果成功,则再发送一个 ICA 数据包,以在 LP 上交换代币并将资金返还给发送者
  • 在向某个智能合约地址接收到代币转账时执行一些逻辑
这需要第二层回调。IBC 应用已经能从核心 IBC 获取数据包结果,但目前还没有标准化方式将该信息继续传递给 actor 模块或智能合约。

定义

  • Actor:actor 是链上的一个模块(可以是链二进制中硬编码的模块,也可以是智能合约),它希望在 IBC 收到其已发送或已接收的数据包流时执行自定义逻辑。它必须能够通过字符串值进行寻址。

决策

创建一个中间件,用于在 IBC 应用与智能合约 VM 之间进行交互。IBC 应用和智能合约 VM 将分别实现各自的接口,然后由回调中间件将其组合起来,使任何兼容 VM 中的智能合约都能够以编程方式与 IBC 应用交互。

数据结构

CallbackPacketData 结构体将基于应用数据包中的自定义回调数据构建。CallbackAddress 是应被调用回调的 IBC Actor 地址。SenderAddress 也会一并提供,以便 VM 可选择性地确保发送者与回调地址相同。 该结构体还定义了 CommitGasLimit,即回调允许使用的最大 gas。如果回调超过该限制,回调将发生 panic,随后交易会提交,但不会包含该回调的状态变更。 ExecutionGasLimit 是在上下文 gas meter 中设置的交易执行实际限制。它取 CommitGasLimit 与上下文 gas meter 中剩余 gas 的最小值,而剩余 gas 由 relayer 选择的交易 gas 限制决定。如果 ExecutionGasLimit < CommitGasLimit,则 gas 不足错误会回滚整个交易且不提交任何内容,从而允许其他 relayer 使用更大的交易 gas 限制重试。 任何面向该接口处理回调的中间件都应定义一个全局限制,用于限制回调允许消耗的 gas 上限(尤其是在 AcknowledgePacket 和 TimeoutPacket 上),以避免自定义回调阻止数据包生命周期完成。不过,由于这是全局上限,它很可能会非常大。因此,用户可以指定一个更小的限制,以限制 relayer 代表用户完成数据包生命周期时必须支付的费用。
// 由任何希望支持 PacketActor 回调的数据包类型实现
// 对于未实现此接口的任何数据包类型,PacketActor 都无法对其进行处理
type CallbackPacketData struct {
    CallbackAddress: string
    ExecutionGasLimit: uint64
    SenderAddress: string
    CommitGasLimit: uint64
}
然后,IBC 应用或中间件便可以在它们自己的回调中像这样调用 IBCActor 回调:

回调中间件

CallbackMiddleware 会包装底层 IBC 应用以及一个 contractKeeper,由后者将回调委托给虚拟机。这样,只要应用实现了 CallbacksCompatibleModule 接口,且 VM 实现了 ContractKeeper 接口,回调中间件就可以让任何兼容的 IBC 应用与任何兼容的 VM(例如 EVM、WASM)进行交互。
// IBCMiddleware 为 ibc-callbacks 中间件实现 ICS26 回调,
// 并包装底层应用。
type IBCMiddleware struct {
	app         types.CallbacksCompatibleModule
	ics4Wrapper porttypes.ICS4Wrapper

	contractKeeper types.ContractKeeper

	// maxCallbackGas 定义回调 actor 可以要求 relayer 支付的最大 gas 数量。
	// 如果回调因 gas 不足而失败,而 relayer 没有提供最小值
	// min(userDefinedGas, maxCallbackGas),则整个交易会被回滚。
	// 如果 actor 没有定义 gas 限制,则默认视为 maxCallbackGas。
	maxCallbackGas uint64
}

兼容回调的 IBC 应用

CallbacksCompatibleModule 扩展了 porttypes.IBCModule,加入了 UnmarshalPacketData 函数,使中间件能够请求底层应用对数据包数据进行反序列化。这样,中间件便可以从任意一组 IBC 应用数据包中提取特定于回调的数据。
// CallbacksCompatibleModule 是一个组合了 IBCModule 和 PacketDataUnmarshaler
// 接口的接口,用于断言底层应用同时支持二者。
type CallbacksCompatibleModule interface {
	porttypes.IBCModule
	porttypes.PacketDataUnmarshaler
}

// PacketDataUnmarshaler 定义了一个可选接口,允许中间件
// 请求由基础应用对数据包数据进行反序列化。
type PacketDataUnmarshaler interface {
	// UnmarshalPacketData 将数据包数据反序列化为具体类型
	// ctx、portID、channelID 作为参数提供,以便在需要时
	// 可基于通道版本对数据包数据进行反序列化。
	// 同时也会返回底层应用的版本。
	UnmarshalPacketData(ctx sdk.Context, portID, channelID string, bz []byte) (interface{}, string, error)
}
应用的数据包数据还必须额外实现以下接口:
// PacketData 定义了一个可选接口,应用的数据包数据结构可实现该接口。
type PacketData interface {
	// GetPacketSender 返回数据包数据的发送者地址。
	// 如果数据包发送者未知或未定义,应返回空字符串。
	GetPacketSender(sourcePortID string) string
}

// PacketDataProvider 定义了一个可选接口,用于获取代表另一个应用存储的自定义数据包数据。
// IBC 中间件设计中的一个现有问题是,中间件无法定义自己的数据包数据类型,
// 并插入由数据包发送者提供的信息。
// 一个短期解决方案是,在若干应用的数据包数据中引入 memo 字段,
// 以代表另一个应用承载这类信息。
// 该接口将这一行为标准化。一旦中间件能够定义自己的数据包数据类型,
// 该接口将被弃用并逐步移除。
type PacketDataProvider interface {
	// GetCustomPacketData 返回代表另一个应用持有的数据包数据。
	// 应将该信息存储时所使用的名称作为 key 提供。
	// 如果该 key 不存在自定义数据包数据,应返回 nil。
	GetCustomPacketData(key string) interface{}
}
可以通过在适当 key 下,为源回调和目标回调提供自定义数据包数据,将回调数据嵌入到应用数据包中。
// 以 JSON 对象形式嵌入在数据包数据中的自定义 Packet 数据

// 源回调自定义数据
{
  "src_callback": {
    "address": "callbackAddressString",
    // 可选
    "gas_limit": "userDefinedGasLimitString",
  }
}

// 目标回调自定义数据
{
  "dest_callback": {
    "address": "callbackAddressString",
    // 可选
    "gas_limit": "userDefinedGasLimitString",
  }
}

// 同时嵌入源回调和目标回调自定义数据
{
  "src_callback": {
    "address": "callbackAddressString",
    // 可选
    "gas_limit": "userDefinedGasLimitString",
  },
  "dest_callback": {
    "address": "callbackAddressString",
    // 可选
    "gas_limit": "userDefinedGasLimitString",
  }
}

ContractKeeper

任何希望支持 IBC 回调的 VM 都必须实现 ContractKeeper 接口。这样可以实现关注点分离:中间件负责处理面向所有 VM 的通用逻辑(例如设置 gas meter、提取回调数据、发出事件),而 ContractKeeper 则可以处理调用对应 VM 的具体细节。 ContractKeeper 可以 增加额外检查,例如在源链回调中确保合约地址与数据包发送者相同。它也可以通过直接执行 no-op 来禁用某些回调方法。
// ContractKeeper defines the entry points exposed to the VM module which invokes a smart contract
type ContractKeeper 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,
		sourcePort string,
		sourceChannel string,
		timeoutHeight clienttypes.Height,
		timeoutTimestamp uint64,
		packetData []byte,
		contractAddress,
		packetSenderAddress string,
		version 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,
		packet channeltypes.Packet,
		acknowledgement []byte,
		relayer sdk.AccAddress,
		contractAddress,
		packetSenderAddress string,
		version 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,
		packet channeltypes.Packet,
		relayer sdk.AccAddress,
		contractAddress,
		packetSenderAddress string,
		version 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.
	// This entry point is called with a cached context. If an error is returned, then the error
	// will be written as an error acknowledgement. This will cause the context changes made by the contract
	// to be reverted along with any state changes made by the underlying application.
	// The error acknowledgement will then be relayed to the sending application which can perform
	// its error acknowledgement logic (e.g. refunding tokens back to user)
	//
	// 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,
		packet ibcexported.PacketI,
		ack ibcexported.Acknowledgement,
		contractAddress string,
		version string,
	) error
}

PacketCallbacks

中间件中实现的数据包回调会先调用底层应用,然后在后处理步骤中路由到 IBC actor 回调。 它会从应用数据包中提取回调数据,并根据全局限制、用户限制以及交易 gas meter 中剩余的 gas 设置回调 gas meter。 随后,回调会通过 callback keeper 进行路由,该 keeper 要么触发 panic,要么返回结果(成功或失败)。如果发生(非 oog)panic 或错误,回调状态变更会被丢弃, 而交易仍会被提交。 如果在达到用户定义的 gas 限制或全局回调 gas 限制之前,先超过了 relayer 定义的 gas 限制,则整个交易会被回滚,以便重新提交。如果达到的是链定义或用户定义的 gas 限制, 则回调状态变更会被回滚,而交易仍会被提交。 对于 SendPacket 回调,任何类型的错误或 panic 都会导致整个交易被回滚。这是因为此时数据包生命周期尚未开始,因此如果回调未成功,我们可以完全回滚,以避免启动数据包生命周期。
// SendPacket 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) SendPacket(
	ctx sdk.Context,
	chanCap *capabilitytypes.Capability,
	sourcePort string,
	sourceChannel string,
	timeoutHeight clienttypes.Height,
	timeoutTimestamp uint64,
	data []byte,
) (uint64, error) {
    // run underlying app logic first
    // IBCActor logic will postprocess
	seq, err := im.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data)
	if err != nil {
		return 0, err
	}

    // use underlying app to get source callback information from packet data
	callbackData, err := types.GetSourceCallbackData(im.app, data, sourcePort, ctx.GasMeter().GasRemaining(), im.maxCallbackGas)
	// SendPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return seq, nil
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		return im.contractKeeper.IBCSendPacketCallback(
			cachedCtx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data, callbackData.CallbackAddress, callbackData.SenderAddress,
		)
	}

	err = im.processCallback(ctx, types.CallbackTypeSendPacket, callbackData, callbackExecutor)
	// contract keeper is allowed to reject the packet send.
	if err != nil {
		return 0, err
	}

    types.EmitCallbackEvent(ctx, sourcePort, sourceChannel, seq, types.CallbackTypeSendPacket, callbackData, nil)
	return seq, 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 sdk.Context,
	chanCap *capabilitytypes.Capability,
	packet ibcexported.PacketI,
	ack ibcexported.Acknowledgement,
) error {
    // run underlying app logic first
    // IBCActor logic will postprocess
	err := im.ics4Wrapper.WriteAcknowledgement(ctx, chanCap, packet, ack)
	if err != nil {
		return err
	}

    // use underlying app to get destination callback information from packet data
	callbackData, err := types.GetDestCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// WriteAcknowledgement 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.IBCReceivePacketCallback(cachedCtx, packet, ack, callbackData.CallbackAddress)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
	// emit events
    types.EmitCallbackEvent(
		ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
		types.CallbackTypeAcknowledgementPacket, callbackData, err,
	)

	return nil
}

// Call the IBCActor recvPacket callback after processing the packet
// if the recvPacket callback exists. If the callback returns an error
// then return an error ack to revert all packet data processing. 
func (im IBCMiddleware) OnRecvPacket(
    ctx sdk.Context,
    packet channeltypes.Packet,
    relayer sdk.AccAddress,
) (ack exported.Acknowledgement) {
    // run underlying app logic first
    // IBCActor logic will postprocess
    ack := im.app.OnRecvPacket(ctx, packet, 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 ack == nil || !ack.Success() {
		return ack
	}

    // use underlying app to get destination callback information from packet data
    callbackData, err := types.GetDestCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// OnRecvPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return ack
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		return im.contractKeeper.IBCReceivePacketCallback(cachedCtx, packet, ack, callbackData.CallbackAddress)
	}

    // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(),
		types.CallbackTypeReceivePacket, callbackData, err,
	)
	if err != nil {
		return channeltypes.NewErrorAcknowledgement(err)
	}

	return ack
}

// Call the IBCActor acknowledgementPacket callback after processing the packet
// if the ackPacket callback exists and returns an error
// DO NOT return the error upstream. The acknowledgement must complete for the packet
// lifecycle to end, so the custom callback cannot block completion.
// Instead we emit error events and set the error in state
// so that users and on-chain logic can handle this appropriately
func (im IBCModule) OnAcknowledgementPacket(
    ctx sdk.Context,
    packet channeltypes.Packet,
    acknowledgement []byte,
    relayer sdk.AccAddress,
) error {
    // we first call the underlying app to handle the acknowledgement
    // IBCActor logic will postprocess
	err := im.app.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
	if err != nil {
		return err
	}

    // use underlying app to get source callback information from packet data
	callbackData, err := types.GetSourceCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// OnAcknowledgementPacket 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.IBCOnAcknowledgementPacketCallback(
			cachedCtx, packet, acknowledgement, relayer, callbackData.CallbackAddress, callbackData.SenderAddress,
		)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeAcknowledgementPacket, callbackData, callbackExecutor)
    types.EmitCallbackEvent(
		ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
		types.CallbackTypeAcknowledgementPacket, callbackData, err,
	)

	return nil
}

// Call the IBCActor timeoutPacket callback after processing the packet
// if the timeoutPacket callback exists and returns an error
// DO NOT return the error upstream. The timeout must complete for the packet
// lifecycle to end, so the custom callback cannot block completion.
// Instead we emit error events and set the error in state
// so that users and on-chain logic can handle this appropriately
func (im IBCModule) OnTimeoutPacket(
    ctx sdk.Context,
    packet channeltypes.Packet,
    relayer sdk.AccAddress,
) error {
    // application-specific onTimeoutPacket logic
    err := im.app.OnTimeoutPacket(ctx, packet, relayer)
	if err != nil {
		return err
	}

    // use underlying app to get source callback information from packet data
	callbackData, err := types.GetSourceCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// OnTimeoutPacket 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.IBCOnTimeoutPacketCallback(cachedCtx, packet, relayer, callbackData.CallbackAddress, callbackData.SenderAddress)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeTimeoutPacket, callbackData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
		types.CallbackTypeTimeoutPacket, callbackData, err,
	)

	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
}
预期各链会指定一个 maxCallbackGas,以确保回调不会消耗任意数量的 gas。因此,即使 actor 回调无法成功运行,relayer 也应始终能够完成数据包生命周期。

后果

正面

  • IBC Actor 现在可以通过编程方式执行这样的逻辑:发送一个数据包,并在数据包生命周期完成后执行额外逻辑
  • 实现 ADR-8 的中间件通常可用于任何应用
  • 复用了与 core IBC 和 IBC 应用之间所使用架构相似的回调架构

负面

  • 回调现在可能会出现无界 gas 消耗,因为 actor 可以执行任意逻辑。实现此功能的链应注意限制 actor 回调可消耗的 gas 数量。
  • 回调的 gas 费用由 relayer 支付,而不是由 IBCActor 支付

中性

  • 希望支持 ADR-8 的应用数据包,其数据还必须实现 PacketDataProvider 和 PacketData 接口。
  • 应用必须实现 PacketDataUnmarshaler 接口
  • 接收回调的模块必须实现 ContractKeeper 接口

参考资料


Changelog

  • 10-08-2022: Initial Draft
  • 22-03-2023: Merged
  • 13-09-2023: Updated with decisions made in implementation
  • 24-02-2025: RecvPacket callback error now returns error acknowledgement

Status

Accepted, middleware implemented

Context

IBC was designed with callbacks between core IBC and IBC applications. IBC apps would send a packet to core IBC. When the result of the packet lifecycle eventually resolved into either an acknowledgement or a timeout, core IBC called a callback on the IBC application so that the IBC application could take action on the basis of the result (e.g. unescrow tokens for ICS-20). This setup worked well for off-chain users interacting with IBC applications. We are now seeing the desire for secondary applications (e.g. smart contracts, modules) to call into IBC apps as part of their state machine logic and then do some actions on the basis of the packet result. Or to receive a packet from IBC and do some logic upon receipt. Example Usecases:
  • Send an ICS-20 packet, and if it is successful, then send an ICA-packet to swap tokens on LP and return funds to sender
  • Execute some logic upon receipt of token transfer to a smart contract address
This requires a second layer of callbacks. The IBC application already gets the result of the packet from core IBC, but currently there is no standardized way to pass this information on to an actor module/smart contract.

Definitions

  • Actor: an actor is an on-chain module (this may be a hardcoded module in the chain binary or a smart contract) that wishes to execute custom logic whenever IBC receives a packet flow that it has either sent or received. It must be addressable by a string value.

Decision

Create a middleware that can interface between IBC applications and smart contract VMs. The IBC applications and smart contract VMs will implement respective interfaces that will then be composed together by the callback middleware to allow a smart contract of any compatible VM to interact programmatically with an IBC application.

Data structures

The CallbackPacketData struct will get constructed from custom callback data in the application packet. The CallbackAddress is the IBC Actor address on which the callback should be called on. The SenderAddress is also provided to optionally allow a VM to ensure that the sender is the same as the callback address. The struct also defines a CommitGasLimit which is the maximum gas a callback is allowed to use. If the callback exceeds this limit, the callback will panic and the tx will commit without the callback’s state changes. The ExecutionGasLimit is the practical limit of the tx execution that is set in the context gas meter. It is the minimum of the CommitGasLimit and the gas left in the context gas meter which is determined by the relayer’s choice of tx gas limit. If ExecutionGasLimit < CommitGasLimit, then an out-of-gas error will revert the entire transaction without committing anything, allowing for a different relayer to retry with a larger tx gas limit. Any middleware targeting this interface for callback handling should define a global limit that caps the gas that a callback is allowed to take (especially on AcknowledgePacket and TimeoutPacket) so that a custom callback does not prevent the packet lifecycle from completing. However, since this is a global cap it is likely to be very large. Thus, users may specify a smaller limit to cap the amount of fees a relayer must pay in order to complete the packet lifecycle on the user’s behalf.
// Implemented by any packet data type that wants to support PacketActor callbacks
// PacketActor's will be unable to act on any packet data type that does not implement
// this interface. 
type CallbackPacketData struct {
    CallbackAddress: string
    ExecutionGasLimit: uint64
    SenderAddress: string
    CommitGasLimit: uint64
}
IBC Apps or middleware can then call the IBCActor callbacks like so in their own callbacks:

Callback Middleware

The CallbackMiddleware wraps an underlying IBC application along with a contractKeeper that delegates the callback to a virtual machine. This allows the Callback middleware to interface any compatible IBC application with any compatible VM (e.g. EVM, WASM) so long as the application implements the CallbacksCompatibleModule interface and the VM implements the ContractKeeper interface.
// IBCMiddleware implements the ICS26 callbacks for the ibc-callbacks middleware given
// the underlying application.
type IBCMiddleware struct {
	app         types.CallbacksCompatibleModule
	ics4Wrapper porttypes.ICS4Wrapper

	contractKeeper types.ContractKeeper

	// 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
}

Callback-Compatible IBC Application

The CallbacksCompatibleModule extends porttypes.IBCModule to include an UnmarshalPacketData function that allows the middleware to request that the underlying app unmarshal the packet data. This will then allow the middleware to retrieve the callback specific data from an arbitrary set of IBC application packets.
// CallbacksCompatibleModule is an interface that combines the IBCModule and PacketDataUnmarshaler
// interfaces to assert that the underlying application supports both.
type CallbacksCompatibleModule interface {
	porttypes.IBCModule
	porttypes.PacketDataUnmarshaler
}

// 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
	// ctx, portID, channelID are provided as arguments, so that (if needed)
	// the packet data can be unmarshaled based on the channel version.
	// the version of the underlying app is also returned.
	UnmarshalPacketData(ctx sdk.Context, portID, channelID string, bz []byte) (interface{}, string, error)
}
The application’s packet data must additionally implement the following interfaces:
// PacketData defines an optional interface which an application's packet data structure may implement.
type PacketData interface {
	// GetPacketSender returns the sender address of the packet data.
	// If the packet sender is unknown or undefined, an empty string should be returned.
	GetPacketSender(sourcePortID string) string
}

// PacketDataProvider defines an optional interfaces for retrieving custom packet data stored on behalf of another application.
// An existing problem in the IBC middleware design is the inability for a middleware to define its own packet data type and insert packet sender provided information.
// A short term solution was introduced into several application's packet data to utilize a memo field to carry this information on behalf of another application.
// This interfaces standardizes that behaviour. Upon realization of the ability for middleware's to define their own packet data types, this interface will be deprecated and removed with time.
type PacketDataProvider interface {
	// GetCustomPacketData returns the packet data held on behalf of another application.
	// The name the information is stored under should be provided as the key.
	// If no custom packet data exists for the key, nil should be returned.
	GetCustomPacketData(key string) interface{}
}
The callback data can be embedded in an application packet by providing custom packet data for source and destination callback in the custom packet data under the appropriate key.
// Custom Packet data embedded as a JSON object in the packet data

// src callback custom data
{
  "src_callback": {
    "address": "callbackAddressString",
    // optional
    "gas_limit": "userDefinedGasLimitString",
  }
}

// dest callback custom data
{
  "dest_callback": {
    "address": "callbackAddressString",
    // optional
    "gas_limit": "userDefinedGasLimitString",
  }
}

// src and dest callback custom data embedded together
{
  "src_callback": {
    "address": "callbackAddressString",
    // optional
    "gas_limit": "userDefinedGasLimitString",
  },
  "dest_callback": {
    "address": "callbackAddressString",
    // optional
    "gas_limit": "userDefinedGasLimitString",
  }
}

ContractKeeper

The ContractKeeper interface must be implemented by any VM that wants to support IBC callbacks. This allows for separation of concerns between the middleware which is handling logic intended for all VMs (e.g. setting gas meter, extracting callback data, emitting events), while the ContractKeeper can handle the specific details of calling into the VM in question. The ContractKeeper may impose additional checks such as ensuring that the contract address is the same as the packet sender in source callbacks. It may also disable certain callback methods by simply performing a no-op.
// ContractKeeper defines the entry points exposed to the VM module which invokes a smart contract
type ContractKeeper 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,
		sourcePort string,
		sourceChannel string,
		timeoutHeight clienttypes.Height,
		timeoutTimestamp uint64,
		packetData []byte,
		contractAddress,
		packetSenderAddress string,
		version 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,
		packet channeltypes.Packet,
		acknowledgement []byte,
		relayer sdk.AccAddress,
		contractAddress,
		packetSenderAddress string,
		version 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,
		packet channeltypes.Packet,
		relayer sdk.AccAddress,
		contractAddress,
		packetSenderAddress string,
		version 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.
	// This entry point is called with a cached context. If an error is returned, then the error
	// will be written as an error acknowledgement. This will cause the context changes made by the contract
	// to be reverted along with any state changes made by the underlying application.
	// The error acknowledgement will then be relayed to the sending application which can perform
	// its error acknowledgement logic (e.g. refunding tokens back to user)
	//
	// 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,
		packet ibcexported.PacketI,
		ack ibcexported.Acknowledgement,
		contractAddress string,
		version string,
	) error
}

PacketCallbacks

The packet callbacks implemented in the middleware will first call the underlying application and then route to the IBC actor callback in the post-processing step. It will extract the callback data from the application packet and set the callback gas meter depending on the global limit, the user limit, and the gas left in the transaction gas meter. The callback will then be routed through the callback keeper which will either panic or return a result (success or failure). In the event of a (non-oog) panic or an error, the callback state changes are discarded and the transaction is committed. If the relayer-defined gas limit is exceeded before the user-defined gas limit or global callback gas limit is exceeded, then the entire transaction is reverted to allow for resubmission. If the chain-defined or user-defined gas limit is reached, the callback state changes are reverted and the transaction is committed. For the SendPacket callback, we will revert the entire transaction on any kind of error or panic. This is because the packet lifecycle has not yet started, so we can revert completely to avoid starting the packet lifecycle if the callback is not successful.
// SendPacket 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) SendPacket(
	ctx sdk.Context,
	chanCap *capabilitytypes.Capability,
	sourcePort string,
	sourceChannel string,
	timeoutHeight clienttypes.Height,
	timeoutTimestamp uint64,
	data []byte,
) (uint64, error) {
    // run underlying app logic first
    // IBCActor logic will postprocess
	seq, err := im.ics4Wrapper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data)
	if err != nil {
		return 0, err
	}

    // use underlying app to get source callback information from packet data
	callbackData, err := types.GetSourceCallbackData(im.app, data, sourcePort, ctx.GasMeter().GasRemaining(), im.maxCallbackGas)
	// SendPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return seq, nil
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		return im.contractKeeper.IBCSendPacketCallback(
			cachedCtx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data, callbackData.CallbackAddress, callbackData.SenderAddress,
		)
	}

	err = im.processCallback(ctx, types.CallbackTypeSendPacket, callbackData, callbackExecutor)
	// contract keeper is allowed to reject the packet send.
	if err != nil {
		return 0, err
	}

    types.EmitCallbackEvent(ctx, sourcePort, sourceChannel, seq, types.CallbackTypeSendPacket, callbackData, nil)
	return seq, 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 sdk.Context,
	chanCap *capabilitytypes.Capability,
	packet ibcexported.PacketI,
	ack ibcexported.Acknowledgement,
) error {
    // run underlying app logic first
    // IBCActor logic will postprocess
	err := im.ics4Wrapper.WriteAcknowledgement(ctx, chanCap, packet, ack)
	if err != nil {
		return err
	}

    // use underlying app to get destination callback information from packet data
	callbackData, err := types.GetDestCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// WriteAcknowledgement 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.IBCReceivePacketCallback(cachedCtx, packet, ack, callbackData.CallbackAddress)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
	// emit events
    types.EmitCallbackEvent(
		ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
		types.CallbackTypeAcknowledgementPacket, callbackData, err,
	)

	return nil
}

// Call the IBCActor recvPacket callback after processing the packet
// if the recvPacket callback exists. If the callback returns an error
// then return an error ack to revert all packet data processing. 
func (im IBCMiddleware) OnRecvPacket(
    ctx sdk.Context,
    packet channeltypes.Packet,
    relayer sdk.AccAddress,
) (ack exported.Acknowledgement) {
    // run underlying app logic first
    // IBCActor logic will postprocess
    ack := im.app.OnRecvPacket(ctx, packet, 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 ack == nil || !ack.Success() {
		return ack
	}

    // use underlying app to get destination callback information from packet data
    callbackData, err := types.GetDestCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// OnRecvPacket is not blocked if the packet does not opt-in to callbacks
	if err != nil {
		return ack
	}

	callbackExecutor := func(cachedCtx sdk.Context) error {
		return im.contractKeeper.IBCReceivePacketCallback(cachedCtx, packet, ack, callbackData.CallbackAddress)
	}

    // callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeReceivePacket, callbackData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, packet.GetDestPort(), packet.GetDestChannel(), packet.GetSequence(),
		types.CallbackTypeReceivePacket, callbackData, err,
	)
	if err != nil {
		return channeltypes.NewErrorAcknowledgement(err)
	}

	return ack
}

// Call the IBCActor acknowledgementPacket callback after processing the packet
// if the ackPacket callback exists and returns an error
// DO NOT return the error upstream. The acknowledgement must complete for the packet
// lifecycle to end, so the custom callback cannot block completion.
// Instead we emit error events and set the error in state
// so that users and on-chain logic can handle this appropriately
func (im IBCModule) OnAcknowledgementPacket(
    ctx sdk.Context,
    packet channeltypes.Packet,
    acknowledgement []byte,
    relayer sdk.AccAddress,
) error {
    // we first call the underlying app to handle the acknowledgement
    // IBCActor logic will postprocess
	err := im.app.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
	if err != nil {
		return err
	}

    // use underlying app to get source callback information from packet data
	callbackData, err := types.GetSourceCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// OnAcknowledgementPacket 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.IBCOnAcknowledgementPacketCallback(
			cachedCtx, packet, acknowledgement, relayer, callbackData.CallbackAddress, callbackData.SenderAddress,
		)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeAcknowledgementPacket, callbackData, callbackExecutor)
    types.EmitCallbackEvent(
		ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
		types.CallbackTypeAcknowledgementPacket, callbackData, err,
	)

	return nil
}

// Call the IBCActor timeoutPacket callback after processing the packet
// if the timeoutPacket callback exists and returns an error
// DO NOT return the error upstream. The timeout must complete for the packet
// lifecycle to end, so the custom callback cannot block completion.
// Instead we emit error events and set the error in state
// so that users and on-chain logic can handle this appropriately
func (im IBCModule) OnTimeoutPacket(
    ctx sdk.Context,
    packet channeltypes.Packet,
    relayer sdk.AccAddress,
) error {
    // application-specific onTimeoutPacket logic
    err := im.app.OnTimeoutPacket(ctx, packet, relayer)
	if err != nil {
		return err
	}

    // use underlying app to get source callback information from packet data
	callbackData, err := types.GetSourceCallbackData(
		im.app, packet.GetData(), packet.GetSourcePort(), ctx.GasMeter().GasRemaining(), im.maxCallbackGas,
	)
	// OnTimeoutPacket 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.IBCOnTimeoutPacketCallback(cachedCtx, packet, relayer, callbackData.CallbackAddress, callbackData.SenderAddress)
	}

	// callback execution errors are not allowed to block the packet lifecycle, they are only used in event emissions
	err = im.processCallback(ctx, types.CallbackTypeTimeoutPacket, callbackData, callbackExecutor)
	types.EmitCallbackEvent(
		ctx, packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence(),
		types.CallbackTypeTimeoutPacket, callbackData, err,
	)

	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
}
Chains are expected to specify a maxCallbackGas to ensure that callbacks do not consume an arbitrary amount of gas. Thus, it should always be possible for a relayer to complete the packet lifecycle even if the actor callbacks cannot run successfully.

Consequences

Positive

  • IBC Actors can now programmatically execute logic that involves sending a packet and then performing some additional logic once the packet lifecycle is complete
  • Middleware implementing ADR-8 can be generally used for any application
  • Leverages a similar callback architecture to the one used between core IBC and IBC applications

Negative

  • Callbacks may now have unbounded gas consumption since the actor may execute arbitrary logic. Chains implementing this feature should take care to place limitations on how much gas an actor callback can consume.
  • The relayer pays for the callback gas instead of the IBCActor

Neutral

  • Application packets that want to support ADR-8 must additionally have their packet data implement PacketDataProvider and PacketData interfaces.
  • Applications must implement PacketDataUnmarshaler interface
  • Callback receiving module must implement the ContractKeeper interface

References