概述

在链上执行任意代码的成本可能高得没有上限。一般来说,回调可能消耗无限 Gas(例如一个永远循环的回调)。这会带来几个问题:
  • 它可能阻塞数据包生命周期。
  • 它可能被用来耗尽中继者的全部资金和 Gas。
  • 中继者可以通过发送一个 Gas 数量较低的数据包,对回调执行发起 DOS 攻击。
为防止这些问题,callbacks 中间件引入了两个 Gas 限制:一个全链 Gas 限制(maxCallbackGas)和一个用户自定义 Gas 限制。

全链 Gas 限制

由于 callbacks 中间件没有 keeper,因此它不会使用治理参数来设置全链 Gas 限制。相反,全链 Gas 限制会在初始化 callbacks 中间件时作为参数传入。
/ app.go
    maxCallbackGas := uint64(10_000_000)

var transferStack porttypes.IBCModule
transferStack = transfer.NewIBCModule(app.TransferKeeper)

transferStack = ibccallbacks.NewIBCMiddleware(transferStack, app.MockContractKeeper, maxCallbackGas)

/ Add transfer stack to IBC Router
ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack)

用户定义的 Gas 限制

用户定义的 Gas 限制由 IBC Actor 在创建数据包时设置。用户定义的 Gas 限制写在数据包 memo 中。如果未设置用户定义的 Gas 限制,或者用户定义的 Gas 限制大于全链 Gas 限制,则会使用全链 Gas 限制作为用户定义的 Gas 限制。
{
  "src_callback": {
  "address": "callbackAddressString",
    / optional
    "gas_limit": "userDefinedGasLimitString",
  
},
  "dest_callback": {
  "address": "callbackAddressString",
    / optional
    "gas_limit": "userDefinedGasLimitString",
  }
}

Gas 限制执行

在执行回调期间,会强制执行三种 Gas 限制:
  • 用户定义的 Gas 限制
  • 全链 Gas 限制
  • 上下文 Gas 限制(即中继者为本次执行剩余的 Gas 数量)
如上一节所述,全链 Gas 限制被用作用户定义的 Gas 限制的上限。如果未提供用户 Gas 限制,它也可以作为默认值使用。因此,在本节剩余部分中,我们可以忽略全链 Gas 限制,转而使用全链 Gas 限制与用户定义的 Gas 限制中的较小值。这个较小值称为提交 Gas 限制。 Gas 限制的强制执行方式,是在一个带有新 Gas meter 的缓存上下文中执行回调。该 Gas meter 会以提交 Gas 限制与上下文 Gas 限制中的较小值进行初始化。这个较小值称为执行 Gas 限制。若 context gas limit < commit gas limit,则表示允许重试;否则,表示不允许重试。 如果回调执行因 Gas 耗尽错误而失败,中间件随后会检查是否允许重试。如果不允许重试,它会从 Gas 耗尽错误中恢复,在原始上下文中消耗执行 Gas 限制,然后继续数据包生命周期。如果允许重试,它会以 Gas 耗尽错误触发 panic,以回滚整个 tx。随后可以使用更高的 Gas 限制再次提交该数据包。Gas 耗尽 panic 的描述如下所示。
fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", callbackType, callbackData.CommitGasLimit)
}
如果回调执行不是因 Gas 耗尽错误而失败,那么无论是否允许重试,callbacks 中间件都不会阻塞数据包生命周期。

Overview

Executing arbitrary code on a chain can be arbitrarily expensive. In general, a callback may consume infinite gas (think of a callback that loops forever). This is problematic for a few reasons:
  • It can block the packet lifecycle.
  • It can be used to consume all of the relayer’s funds and gas.
  • A relayer can DOS the callback execution by sending a packet with a low amount of gas.
To prevent these, the callbacks middleware introduces two gas limits: a chain wide gas limit (maxCallbackGas) and a user defined gas limit.

Chain Wide Gas Limit

Since the callbacks middleware does not have a keeper, it does not use a governance parameter to set the chain wide gas limit. Instead, the chain wide gas limit is passed in as a parameter to the callbacks middleware during initialization.
/ app.go
    maxCallbackGas := uint64(10_000_000)

var transferStack porttypes.IBCModule
transferStack = transfer.NewIBCModule(app.TransferKeeper)

transferStack = ibccallbacks.NewIBCMiddleware(transferStack, app.MockContractKeeper, maxCallbackGas)

/ Add transfer stack to IBC Router
ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack)

User Defined Gas Limit

The user defined gas limit is set by the IBC Actor during packet creation. The user defined gas limit is set in the packet memo. If the user defined gas limit is not set or if the user defined gas limit is greater than the chain wide gas limit, then the chain wide gas limit is used as the user defined gas limit.
{
  "src_callback": {
  "address": "callbackAddressString",
    / optional
    "gas_limit": "userDefinedGasLimitString",
  
},
  "dest_callback": {
  "address": "callbackAddressString",
    / optional
    "gas_limit": "userDefinedGasLimitString",
  }
}

Gas Limit Enforcement

During a callback execution, there are three types of gas limits that are enforced:
  • User defined gas limit
  • Chain wide gas limit
  • Context gas limit (amount of gas that the relayer has left for this execution)
Chain wide gas limit is used as a maximum to the user defined gas limit as explained in the previous section. It may also be used as a default value if no user gas limit is provided. Therefore, we can ignore the chain wide gas limit for the rest of this section and work with the minimum of the chain wide gas limit and user defined gas limit. This minimum is called the commit gas limit. The gas limit enforcement is done by executing the callback inside a cached context with a new gas meter. The gas meter is initialized with the minimum of the commit gas limit and the context gas limit. This minimum is called the execution gas limit. We say that retries are allowed if context gas limit < commit gas limit. Otherwise, we say that retries are not allowed. If the callback execution fails due to an out of gas error, then the middleware checks if retries are allowed. If retries are not allowed, then it recovers from the out of gas error, consumes execution gas limit from the original context, and continues with the packet life cycle. If retries are allowed, then it panics with an out of gas error to revert the entire tx. The packet can then be submitted again with a higher gas limit. The out of gas panic descriptor is shown below.
fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", callbackType, callbackData.CommitGasLimit)
}
If the callback execution does not fail due to an out of gas error then the callbacks middleware does not block the packet life cycle regardless of whether retries are allowed or not.