变更记录

  • 23-04-2021:将状态变更为 “deprecated”
  • 23-05-2020:补充 Go 示例代码和更多细节
  • 18-05-2020:初始草案

状态

已弃用

背景

当前的“朴素” IBC Relayer 策略会在两个客户端之间的一条连接之上,建立一条预先确定的 IBC 通道(两个客户端各自可能属于不同链)。随后,该策略通过监听与该通道匹配的 send_packet 和 recv_packet 事件来检测需要中继的数据包,并发送必要的交易来中继这些数据包。 我们希望将这种“朴素”策略扩展为一种“被动”策略,使其能够在给定连接上检测并中继通道握手消息和数据包,而无需在中继前预先知道每一条通道。 为实现这一点,我们提议在 x/ibc/core/04-channel/keeper/handshake.go 和 x/ibc/core/04-channel/keeper/packet.go 模块发出的每笔交易中,增加更完整的事件,以暴露通道元数据。 下面展示了 ChanOpenInit 中会包含的内容示例:
const (
  EventTypeChannelMeta = "channel_meta"
  AttributeKeyAction = "action"
  AttributeKeyHops = "hops"
  AttributeKeyOrder = "order"
  AttributeKeySrcPort = "src_port"
  AttributeKeySrcChannel = "src_channel"
  AttributeKeySrcVersion = "src_version"
  AttributeKeyDstPort = "dst_port"
  AttributeKeyDstChannel = "dst_channel"
  AttributeKeyDstVersion = "dst_version"
)
// ...
// Emit Event with Channel metadata for the relayer to pick up and
// relay to the other chain
// This appears immediately before the successful return statement.
ctx.EventManager().EmitEvents(sdk.Events{
  sdk.NewEvent(
    types.EventTypeChannelMeta,
    sdk.NewAttribute(types.AttributeKeyAction, "open_init"),
    sdk.NewAttribute(types.AttributeKeySrcConnection, connectionHops[0]),
    sdk.NewAttribute(types.AttributeKeyHops, strings.Join(connectionHops, ",")),
    sdk.NewAttribute(types.AttributeKeyOrder, order.String()),
    sdk.NewAttribute(types.AttributeKeySrcPort, portID),
    sdk.NewAttribute(types.AttributeKeySrcChannel, channelID),
    sdk.NewAttribute(types.AttributeKeySrcVersion, version),
    sdk.NewAttribute(types.AttributeKeyDstPort, counterparty.GetPortID()),
    sdk.NewAttribute(types.AttributeKeyDstChannel, counterparty.GetChannelID()),
    // The destination version is not yet known, but a value is necessary to pad
    // the event attribute offsets
    sdk.NewAttribute(types.AttributeKeyDstVersion, ""),
  ),
})
这些元数据事件包含了路由 IBC 通道握手交易所需的全部“头部”信息,而无需客户端查询除其愿意中继的连接 ID 之外的任何数据。设计目标是让 channel_meta.src_connection 成为被动 relayer 正常运行时唯一需要建立索引的事件键。

处理通道打开尝试

对于被动 relayer,当一条链发送 ChanOpenInit 时,relayer 应将此次打开尝试通知另一条链,并允许该链自行决定如何继续握手,以及是否继续。一旦两条链都主动批准打开该通道,握手的其余部分就可以像当前“朴素” relayer 那样继续进行。 为实现这一行为,我们提议将 cbs.OnChanOpenTry 回调替换为新的 cbs.OnAttemptChanOpenTry 回调,后者会显式处理 MsgChannelOpenTry,通常最终会调用 keeper.ChanOpenTry。在 x/ibc-transfer/module.go 中,典型实现将与当前“朴素” relayer 兼容,如下所示:
func (am AppModule) OnAttemptChanOpenTry(
  ctx sdk.Context,
  chanKeeper channel.Keeper,
  portCap *capability.Capability,
  msg channel.MsgChannelOpenTry,
) (*sdk.Result, error) {
  // Require portID is the portID transfer module is bound to
  boundPort := am.keeper.GetPort(ctx)
  if boundPort != msg.PortID {
    return nil, sdkerrors.Wrapf(porttypes.ErrInvalidPort, "invalid port: %s, expected %s", msg.PortID, boundPort)
  }

  // BEGIN NEW CODE
  // Assert our protocol version, overriding the relayer's suggestion.
  msg.Version = types.Version
  // Continue the ChanOpenTry.
  res, chanCap, err := channel.HandleMsgChannelOpenTry(ctx, chanKeeper, portCap, msg)
  if err != nil {
    return nil, err
  }
  // END OF NEW CODE

  // ... the rest of the callback is similar to the existing OnChanOpenTry
  // but uses msg.* directly.
下面是在 x/ibc/handler.go 实现中使用该回调的方式:
// ...
case channel.MsgChannelOpenTry:
  // Lookup module by port capability
  module, portCap, err := k.PortKeeper.LookupModuleByPort(ctx, msg.PortID)
  if err != nil {
    return nil, sdkerrors.Wrap(err, "could not retrieve module from port-id")
  }
  // Retrieve callbacks from router
  cbs, ok := k.Router.GetRoute(module)
  if !ok {
    return nil, sdkerrors.Wrapf(port.ErrInvalidRoute, "route not found to module: %s", module)
  }
  // Delegate to the module's OnAttemptChanOpenTry.
  return cbs.OnAttemptChanOpenTry(ctx, k.ChannelKeeper, portCap, msg)
之所以没有在 x/ibc/handler.go 与端口模块之间设计更结构化的交互方式(例如显式协商版本等),是因为我们不希望约束应用模块必须在这笔交易中,甚至必须在这个区块内完成对 MsgChannelOpenTry 的处理。

决策

  • 暴露相关事件,以支持“被动”连接 relayer。
  • 通过此类被动 relayer 支持由应用发起的通道。
  • 允许端口模块控制如何处理 open-try 消息。

影响

正面

使通道成为完整的应用层抽象。 应用可以完全控制通道的发起与接受,而不必依赖 relayer 告诉它们何时这样做。 被动 relayer 无需知道应用支持何种类型的通道(版本字符串、排序约束、防火墙逻辑)。这些内容由应用之间直接协商。

负面

IBC 消息的事件体积增大。

中性

暴露了更多 IBC 事件。

参考


Changelog

  • 23-04-2021: Change status to “deprecated”
  • 23-05-2020: Provide sample Go code and more details
  • 18-05-2020: Initial Draft

Status

deprecated

Context

The current “naive” IBC Relayer strategy currently establishes a single predetermined IBC channel atop a single connection between two clients (each potentially of a different chain). This strategy then detects packets to be relayed by watching for send_packet and recv_packet events matching that channel, and sends the necessary transactions to relay those packets. We wish to expand this “naive” strategy to a “passive” one which detects and relays both channel handshake messages and packets on a given connection, without the need to know each channel in advance of relaying it. In order to accomplish this, we propose adding more comprehensive events to expose channel metadata for each transaction sent from the x/ibc/core/04-channel/keeper/handshake.go and x/ibc/core/04-channel/keeper/packet.go modules. Here is an example of what would be in ChanOpenInit:
const (
  EventTypeChannelMeta = "channel_meta"
  AttributeKeyAction = "action"
  AttributeKeyHops = "hops"
  AttributeKeyOrder = "order"
  AttributeKeySrcPort = "src_port"
  AttributeKeySrcChannel = "src_channel"
  AttributeKeySrcVersion = "src_version"
  AttributeKeyDstPort = "dst_port"
  AttributeKeyDstChannel = "dst_channel"
  AttributeKeyDstVersion = "dst_version"
)
// ...
// Emit Event with Channel metadata for the relayer to pick up and
// relay to the other chain
// This appears immediately before the successful return statement.
ctx.EventManager().EmitEvents(sdk.Events{
  sdk.NewEvent(
    types.EventTypeChannelMeta,
    sdk.NewAttribute(types.AttributeKeyAction, "open_init"),
    sdk.NewAttribute(types.AttributeKeySrcConnection, connectionHops[0]),
    sdk.NewAttribute(types.AttributeKeyHops, strings.Join(connectionHops, ",")),
    sdk.NewAttribute(types.AttributeKeyOrder, order.String()),
    sdk.NewAttribute(types.AttributeKeySrcPort, portID),
    sdk.NewAttribute(types.AttributeKeySrcChannel, channelID),
    sdk.NewAttribute(types.AttributeKeySrcVersion, version),
    sdk.NewAttribute(types.AttributeKeyDstPort, counterparty.GetPortID()),
    sdk.NewAttribute(types.AttributeKeyDstChannel, counterparty.GetChannelID()),
    // The destination version is not yet known, but a value is necessary to pad
    // the event attribute offsets
    sdk.NewAttribute(types.AttributeKeyDstVersion, ""),
  ),
})
These metadata events capture all the “header” information needed to route IBC channel handshake transactions without requiring the client to query any data except that of the connection ID that it is willing to relay. It is intended that channel_meta.src_connection is the only event key that needs to be indexed for a passive relayer to function.

Handling Channel Open Attempts

In the case of the passive relayer, when one chain sends a ChanOpenInit, the relayer should inform the other chain of this open attempt and allow that chain to decide how (and if) it continues the handshake. Once both chains have actively approved the channel opening, then the rest of the handshake can happen as it does with the current “naive” relayer. To implement this behavior, we propose replacing the cbs.OnChanOpenTry callback with a new cbs.OnAttemptChanOpenTry callback which explicitly handles the MsgChannelOpenTry, usually by resulting in a call to keeper.ChanOpenTry. The typical implementation, in x/ibc-transfer/module.go would be compatible with the current “naive” relayer, as follows:
func (am AppModule) OnAttemptChanOpenTry(
  ctx sdk.Context,
  chanKeeper channel.Keeper,
  portCap *capability.Capability,
  msg channel.MsgChannelOpenTry,
) (*sdk.Result, error) {
  // Require portID is the portID transfer module is bound to
  boundPort := am.keeper.GetPort(ctx)
  if boundPort != msg.PortID {
    return nil, sdkerrors.Wrapf(porttypes.ErrInvalidPort, "invalid port: %s, expected %s", msg.PortID, boundPort)
  }

  // BEGIN NEW CODE
  // Assert our protocol version, overriding the relayer's suggestion.
  msg.Version = types.Version
  // Continue the ChanOpenTry.
  res, chanCap, err := channel.HandleMsgChannelOpenTry(ctx, chanKeeper, portCap, msg)
  if err != nil {
    return nil, err
  }
  // END OF NEW CODE

  // ... the rest of the callback is similar to the existing OnChanOpenTry
  // but uses msg.* directly.
Here is how this callback would be used, in the implementation of x/ibc/handler.go:
// ...
case channel.MsgChannelOpenTry:
  // Lookup module by port capability
  module, portCap, err := k.PortKeeper.LookupModuleByPort(ctx, msg.PortID)
  if err != nil {
    return nil, sdkerrors.Wrap(err, "could not retrieve module from port-id")
  }
  // Retrieve callbacks from router
  cbs, ok := k.Router.GetRoute(module)
  if !ok {
    return nil, sdkerrors.Wrapf(port.ErrInvalidRoute, "route not found to module: %s", module)
  }
  // Delegate to the module's OnAttemptChanOpenTry.
  return cbs.OnAttemptChanOpenTry(ctx, k.ChannelKeeper, portCap, msg)
The reason we do not have a more structured interaction between x/ibc/handler.go and the port’s module (to explicitly negotiate versions, etc) is that we do not wish to constrain the app module to have to finish handling the MsgChannelOpenTry during this transaction or even this block.

Decision

  • Expose events to allow “passive” connection relayers.
  • Enable application-initiated channels via such passive relayers.
  • Allow port modules to control how to handle open-try messages.

Consequences

Positive

Makes channels into a complete application-level abstraction. Applications have full control over initiating and accepting channels, rather than expecting a relayer to tell them when to do so. A passive relayer does not have to know what kind of channel (version string, ordering constraints, firewalling logic) the application supports. These are negotiated directly between applications.

Negative

Increased event size for IBC messages.

Neutral

More IBC events are exposed.

References