概述

路由模块是一个次级模块的默认实现,它会接收外部数据报,并调用区块间通信协议处理器来处理握手与数据包中继。 路由模块维护一张模块查找表,当收到数据包时,它可以用这张表查找并调用相应模块,因此外部中继器只需要始终将数据包中继到路由模块。

设计动机

默认的 IBC 处理器使用接收方调用模式,其中模块必须分别调用 IBC 处理器,才能绑定端口、发起握手、接受握手、发送和接收数据包等。这种方式灵活且简单,但理解起来稍有难度,并且可能会给中继进程带来额外工作,因为它们必须跟踪许多模块的状态。本标准描述了一种 IBC“路由模块”,用于自动化大多数常见功能、路由数据包,并简化中继器的任务。 路由模块也可以承担 ICS 5 中讨论的模块管理器角色,并实现 用于确定模块何时允许绑定端口以及这些端口可以如何命名的逻辑。

定义

IBC 处理器接口提供的所有函数均按 ICS 25 中的定义。 函数 newCapability 和 authenticateCapability 按 ICS 5 中的定义。 函数 writeChannel 和 writeAcknowledgement 按 ICS 4 中的定义。

期望属性

  • 模块应能够通过路由模块绑定端口并拥有通道。
  • 除了调用间接层之外,数据包发送和接收不应增加额外开销。
  • 当模块需要对数据包执行操作时,路由模块应调用模块上指定的处理函数。

技术规范

注意:如果宿主状态机正在使用对象能力认证(见 ICS 005),则所有使用端口的函数都需要额外接收一个 capability 参数。

模块回调接口

模块必须向路由模块暴露以下函数签名,这些函数会在收到各种数据报时被调用:

OnChanOpenInit

onChanOpenInit 将验证由中继器选择的参数 是否有效,并执行任何自定义 INIT 逻辑。 如果所选参数无效,它可以返回错误, 此时握手会被中止。 如果提供的版本字符串非空,onChanOpenInit 应返回 该版本字符串;如果提供的版本无效,则返回错误。 如果版本字符串为空,预期 onChanOpenInit 返回一个默认版本字符串,以表示 它所支持的版本。 如果该应用没有默认版本字符串, 那么在提供的版本为空字符串时,它应返回错误。
function onChanOpenInit(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  version: string) => (version: string, err: Error) {
    // defined by the module
}

OnChanOpenTry

onChanOpenTry 将验证 INIT 阶段所选择的参数,以及 对手方选择的版本字符串,并执行自定义 TRY 逻辑。 如果 INIT 阶段选择的参数 无效,则该回调必须返回错误以中止握手。 如果对手方选择的版本与该模块 支持的版本不兼容,则该回调必须返回错误以中止握手。 如果版本兼容,则 try 回调必须选择最终的版本 字符串,并将其返回给核心 IBC。 onChanOpenTry 也可以执行自定义初始化逻辑。
function onChanOpenTry(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) => (version: string, err: Error) {
    // defined by the module
}

OnChanOpenAck

onChanOpenAck 会在对手方选择的版本字符串 无效时抛出错误,以中止握手。它也可以执行自定义 ACK 逻辑。
function onChanOpenAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier, 
  counterpartyVersion: string) {
    // defined by the module
}

OnChanOpenConfirm

onChanOpenConfirm 将执行自定义 CONFIRM 逻辑,并且可能通过报错来中止握手。
function onChanOpenConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // defined by the module
}
function onChanCloseInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // defined by the module
}

function onChanCloseConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier): void {
    // defined by the module
}

function onRecvPacket(packet: Packet, relayer: string): bytes {
    // defined by the module, returns acknowledgement
}

function onTimeoutPacket(packet: Packet, relayer: string) {
    // defined by the module
}

function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: string) {
    // defined by the module
}

function onTimeoutPacketClose(packet: Packet, relayer: string) {
    // defined by the module
}
必须通过抛出异常来表示失败,并拒绝握手、传入数据包等。 这些函数被组合在一个 ModuleCallbacks 接口中:
interface ModuleCallbacks {
  onChanOpenInit: onChanOpenInit
  onChanOpenTry: onChanOpenTry
  onChanOpenAck: onChanOpenAck
  onChanOpenConfirm: onChanOpenConfirm
  onChanCloseInit: onChanCloseInit
  onChanCloseConfirm: onChanCloseConfirm
  onRecvPacket: onRecvPacket
  onTimeoutPacket: onTimeoutPacket
  onAcknowledgePacket: onAcknowledgePacket
  onTimeoutPacketClose: onTimeoutPacketClose
}
当模块绑定到某个端口时,需要提供这些回调。
function callbackPath(portIdentifier: Identifier): Path {
    return "callbacks/{portIdentifier}"
}
还会存储调用模块的标识符,以便将来在需要变更回调时进行认证。
function authenticationPath(portIdentifier: Identifier): Path {
    return "authentication/{portIdentifier}"
}

作为模块管理器的端口绑定

IBC 路由模块位于处理器模块(ICS 25)与宿主状态机上的各个模块之间。 路由模块作为模块管理器,会区分两类端口:
  • “现有名称”端口:例如“bank”,具有标准化的既有含义,不应采用先到先得的方式
  • “新名称”端口:新的身份(例如智能合约),无既有关系,新的随机数端口,生成后的端口名可以通过其他通道进行传递
当宿主状态机实例化路由模块时,会分配一组现有名称及其对应模块。 随后,路由模块允许模块在任意时间分配新端口,但它们必须使用特定的标准化前缀。 模块可以调用函数 bindPort,通过路由模块绑定到某个端口,并设置回调。
function bindPort(
  id: Identifier,
  callbacks: Callbacks): CapabilityKey {
    abortTransactionUnless(privateStore.get(callbackPath(id)) === null)
    privateStore.set(callbackPath(id), callbacks)
    capability = handler.bindPort(id)
    claimCapability(authenticationPath(id), capability)
    return capability
}
模块可以调用函数 updatePort 来修改回调。
function updatePort(
  id: Identifier,
  capability: CapabilityKey,
  newCallbacks: Callbacks) {
    abortTransactionUnless(authenticateCapability(authenticationPath(id), capability))
    privateStore.set(callbackPath(id), newCallbacks)
}
模块可以调用函数 releasePort 来释放之前正在使用的端口。
警告:释放端口将允许其他模块绑定到该端口,并可能拦截传入的通道打开握手。模块仅应在确认安全时释放端口。
function releasePort(
  id: Identifier,
  capability: CapabilityKey) {
    abortTransactionUnless(authenticateCapability(authenticationPath(id), capability))
    handler.releasePort(id)
    privateStore.delete(callbackPath(id))
    privateStore.delete(authenticationPath(id))
}
路由模块可以使用函数 lookupModule 来查找绑定到特定端口的回调。
function lookupModule(portId: Identifier) {
    return privateStore.get(callbackPath(portId))
}

数据报处理器(写入)

数据报 是由路由模块作为交易接收的外部数据块。本节为每种数据报定义了一个处理函数, 当相应数据报在某笔交易中提交给路由模块时,就会执行该函数。 所有数据报也都可以由其他模块安全地提交给路由模块。 除非有明确说明,否则不假定存在任何消息签名或数据有效性检查。

客户端生命周期管理

ClientCreate 使用指定的标识符和共识状态创建一个新的轻客户端。
interface ClientCreate {
  identifier: Identifier
  clientState: ClientState
  consensusState: ConsensusState
}
function handleClientCreate(datagram: ClientCreate) {
    handler.createClient(datagram.clientState, datagram.consensusState)
}
ClientUpdate 使用指定的标识符和新的头部更新一个现有轻客户端。
interface ClientUpdate {
  identifier: Identifier
  header: Header
}
function handleClientUpdate(datagram: ClientUpdate) {
    handler.updateClient(datagram.identifier, datagram.header)
}
ClientSubmitMisbehaviour 向指定标识符的现有轻客户端提交作恶证明。
interface ClientMisbehaviour {
  identifier: Identifier
  evidence: bytes
}
function handleClientMisbehaviour(datagram: ClientMisbehaviour) {
    handler.submitMisbehaviourToClient(datagram.identifier, datagram.evidence)
}

连接生命周期管理

ConnOpenInit 数据报会与另一条链上的 IBC 模块启动连接握手流程。
interface ConnOpenInit {
  counterpartyPrefix: CommitmentPrefix
  clientIdentifier: Identifier
  counterpartyClientIdentifier: Identifier
  version: string
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
}
function handleConnOpenInit(datagram: ConnOpenInit) {
  handler.connOpenInit(
    datagram.counterpartyPrefix,
    datagram.clientIdentifier,
    datagram.counterpartyClientIdentifier,
    datagram.version,
    datagram.delayPeriodTime,
    datagram.delayPeriodBlocks
  )
}
ConnOpenTry 数据报会接受来自另一条链上的 IBC 模块的握手请求。
interface ConnOpenTry {
  counterpartyConnectionIdentifier: Identifier
  counterpartyPrefix: CommitmentPrefix
  counterpartyClientIdentifier: Identifier
  clientIdentifier: Identifier
  clientState: ClientState // DEPRECATED
  counterpartyVersions: string[]
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
  proofInit: CommitmentProof
  proofClient: CommitmentProof // DEPRECATED
  proofConsensus: CommitmentProof // DEPRECATED
  proofHeight: Height
  consensusHeight: Height // DEPRECATED
}
function handleConnOpenTry(datagram: ConnOpenTry) {
  handler.connOpenTry(
    datagram.counterpartyConnectionIdentifier,
    datagram.counterpartyPrefix,
    datagram.counterpartyClientIdentifier,
    datagram.clientIdentifier,
    datagram.clientState,
    datagram.counterpartyVersions,
    datagram.delayPeriodTime,
    datagram.delayPeriodBlocks,
    datagram.proofInit,
    datagram.proofClient,
    datagram.proofConsensus,
    datagram.proofHeight,
    datagram.consensusHeight
  )
}
ConnOpenAck 数据报会确认另一条链上的 IBC 模块已接受握手。
interface ConnOpenAck {
  identifier: Identifier
  clientState: ClientState // DEPRECATED
  version: string
  counterpartyIdentifier: Identifier
  proofTry: CommitmentProof
  proofClient: CommitmentProof // DEPRECATED
  proofConsensus: CommitmentProof // DEPRECATED
  proofHeight: Height
  consensusHeight: Height // DEPRECATED
}
function handleConnOpenAck(datagram: ConnOpenAck) {
  handler.connOpenAck(
    datagram.identifier,
    datagram.clientState,
    datagram.version,
    datagram.counterpartyIdentifier,
    datagram.proofTry,
    datagram.proofClient,
    datagram.proofConsensus,
    datagram.proofHeight,
    datagram.consensusHeight
  )
}
ConnOpenConfirm 数据报会确认另一条链上的 IBC 模块发出的握手确认,并完成该连接。
interface ConnOpenConfirm {
  identifier: Identifier
  proofAck: CommitmentProof
  proofHeight: Height
}
function handleConnOpenConfirm(datagram: ConnOpenConfirm) {
    handler.connOpenConfirm(
      datagram.identifier,
      datagram.proofAck,
      datagram.proofHeight
    )
}

通道生命周期管理

interface ChanOpenInit {
  order: ChannelOrder
  connectionHops: [Identifier]
  portIdentifier: Identifier
  counterpartyPortIdentifier: Identifier
  version: string
}
function handleChanOpenInit(datagram: ChanOpenInit) {
  module = lookupModule(datagram.portIdentifier)
  channelIdentifier, channelCapability = handler.chanOpenInit(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    datagram.counterpartyPortIdentifier
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // in this case, the channel identifier will be stored with a sentinel value in the channel path so it is not taken
  // by a new channel handshake and the capability is reserved for the application module.
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an INIT state with the right version and the handshake can proceed on the counterparty.
  version, err = module.onChanOpenInit(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.version
  )
  abortTransactionUnless(err === nil)
  writeChannel(
    datagram.portIdentifier,
    channelIdentifier,
    INIT,
    datagram.order,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.connectionHops,
    version
  )
}
interface ChanOpenTry {
  order: ChannelOrder
  connectionHops: [Identifier]
  portIdentifier: Identifier
  channelIdentifier: Identifier
  counterpartyPortIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  counterpartyVersion: string
  proofInit: CommitmentProof
  proofHeight: Height
}
function handleChanOpenTry(datagram: ChanOpenTry) {
  module = lookupModule(datagram.portIdentifier)
  channelIdentifier, channelCapability = handler.chanOpenTry(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion,
    datagram.proofInit,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // in this case, the channel identifier will be stored with a sentinel value in the channel path so it is not taken
  // by a new channel handshake and the capability is reserved for the application module.
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into a TRY state with the right version and the handshake can proceed on the counterparty.
  version, err = module.onChanOpenTry(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion
  )
  abortTransactionUnless(err === nil)
  writeChannel(
    datagram.portIdentifier,
    channelIdentifier,
    TRYOPEN,
    datagram.order,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.connectionHops,
    version
  )
}
interface ChanOpenAck {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  counterpartyVersion: string
  proofTry: CommitmentProof
  proofHeight: Height
}
function handleChanOpenAck(datagram: ChanOpenAck) {
  module = lookupModule(datagram.portIdentifier)
  handler.chanOpenAck(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion,
    datagram.proofTry,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an OPEN state and the handshake can proceed on the counterparty.
  err = module.onChanOpenAck(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion
  )
  abortTransactionUnless(err === nil)
  channel = provableStore.get(channelPath(datagram.portIdentifier, datagram.channelIdentifier))
  writeChannel(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    OPEN,
    channel.order,
    channel.counterparty.portIdentifier,
    datagram.counterpartyChannelIdentifier,
    channel.connectionHops,
    datagram.counterpartyVersion
  )
}
interface ChanOpenConfirm {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  proofAck: CommitmentProof
  proofHeight: Height
}
function handleChanOpenConfirm(datagram: ChanOpenConfirm) {
  module = lookupModule(datagram.portIdentifier)
  handler.chanOpenConfirm(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.proofAck,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an OPEN state and the handshake can proceed on the counterparty.
  err = module.onChanOpenConfirm(
    datagram.portIdentifier,
    datagram.channelIdentifier
  )
  abortTransactionUnless(err === nil)
  channel = provableStore.get(channelPath(datagram.portIdentifier, datagram.channelIdentifier))
  writeChannel(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    OPEN,
    channel.order,
    channel.counterparty.portIdentifier,
    channel.counterparty.channelIdentifier,
    channel.connectionHops,
    channel.version
  )
}
interface ChanCloseInit {
  portIdentifier: Identifier
  channelIdentifier: Identifier
}
function handleChanCloseInit(datagram: ChanCloseInit) {
    module = lookupModule(datagram.portIdentifier)
    err = module.onChanCloseInit(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
    abortTransactionUnless(err === nil)
    handler.chanCloseInit(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
}
interface ChanCloseConfirm {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  proofInit: CommitmentProof
  proofHeight: Height
}
function handleChanCloseConfirm(datagram: ChanCloseConfirm) {  
    handler.chanCloseConfirm(
      datagram.portIdentifier,
      datagram.channelIdentifier,
      datagram.proofInit,
      datagram.proofHeight
    )
    // SYNCHRONOUS: the following calls happen synchronously with the call above
    // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
    // When the module eventually executes its callback it must call writeChannel so that the channel
    // can be written into a CLOSED state and the handshake can proceed on the counterparty.
    module = lookupModule(datagram.portIdentifier)
    err = module.onChanCloseConfirm(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
    abortTransactionUnless(err === nil)
    writeChannel(
      datagram.portIdentifier,
      datagram.channelIdentifier,
      CLOSED,
    )
}

数据包中继

数据包由模块直接发送(即由模块调用 IBC 处理器发送)。
interface PacketRecv {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketRecv(datagram: PacketRecv) {
  module = lookupModule(datagram.packet.destPort)
  handler.recvPacket(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight,
    acknowledgement
  )
  acknowledgement = module.onRecvPacket(datagram.packet)
  writeAcknowledgement(datagram.packet, acknowledgement)
}
interface PacketAcknowledgement {
  packet: Packet
  acknowledgement: string
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketAcknowledgement(datagram: PacketAcknowledgement) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.acknowledgePacket(
    datagram.packet,
    datagram.acknowledgement,
    datagram.proof,
    datagram.proofHeight
  )
  module.onAcknowledgePacket(
    datagram.packet,
    datagram.acknowledgement
  )   
}

数据包超时

interface PacketTimeout {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
  nextSequenceRecv: Maybe<uint64>
}
function handlePacketTimeout(datagram: PacketTimeout) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.timeoutPacket(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight,
    datagram.nextSequenceRecv
  )
  module.onTimeoutPacket(datagram.packet)
}
interface PacketTimeoutOnClose {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketTimeoutOnClose(datagram: PacketTimeoutOnClose) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.timeoutOnClose(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight
  )
  module.onTimeoutPacket(datagram.packet)
}

基于超时的关闭与数据包清理

interface PacketCleanup {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
  nextSequenceRecvOrAcknowledgement: Either<uint64, bytes>
}

查询(只读)函数

客户端、连接和通道的所有查询函数都应由 IBC 处理程序模块直接暴露为只读接口。

接口使用示例

用法示例见 ICS 20。

属性与不变量

  • 代理端口绑定遵循先到先得原则:一旦某个模块通过 IBC 路由模块绑定到某个端口,在该模块释放该端口之前,只有该模块可以使用该端口。

向后兼容性

不适用。

向前兼容性

路由模块与 IBC 处理程序接口紧密耦合。

实现示例

历史

2019 年 6 月 9 日 - 提交草案 2019 年 7 月 28 日 - 重大修订 2019 年 8 月 25 日 - 重大修订 2023 年 3 月 28 日 - 修复模块处理程序与应用回调的执行顺序

版权

此处的所有内容均依据 Apache 2.0 许可发布。

Synopsis

The routing module is a default implementation of a secondary module which will accept external datagrams and call into the interblockchain communication protocol handler to deal with handshakes and packet relay. The routing module keeps a lookup table of modules, which it can use to look up and call a module when a packet is received, so that external relayers need only ever relay packets to the routing module.

Motivation

The default IBC handler uses a receiver call pattern, where modules must individually call the IBC handler in order to bind to ports, start handshakes, accept handshakes, send and receive packets, etc. This is flexible and simple but is a bit tricky to understand and may require extra work on the part of relayer processes, who must track the state of many modules. This standard describes an IBC “routing module” to automate most common functionality, route packets, and simplify the task of relayers. The routing module can also play the role of the module manager as discussed in ICS 5 and implement logic to determine when modules are allowed to bind to ports and what those ports can be named.

Definitions

All functions provided by the IBC handler interface are defined as in ICS 25. The functions newCapability & authenticateCapability are defined as in ICS 5. The functions writeChannel and writeAcknowledgement are defined as in ICS 4

Desired Properties

  • Modules should be able to bind to ports and own channels through the routing module.
  • No overhead should be added for packet sends and receives other than the layer of call indirection.
  • The routing module should call specified handler functions on modules when they need to act upon packets.

Technical Specification

Note: If the host state machine is utilising object capability authentication (see ICS 005), all functions utilising ports take an additional capability parameter.

Module callback interface

Modules must expose the following function signatures to the routing module, which are called upon the receipt of various datagrams:

OnChanOpenInit

onChanOpenInit will verify that the relayer-chosen parameters are valid and perform any custom INIT logic. It may return an error if the chosen parameters are invalid in which case the handshake is aborted. If the provided version string is non-empty, onChanOpenInit should return the version string or an error if the provided version is invalid. If the version string is empty, onChanOpenInit is expected to return a default version string representing the version(s) it supports. If there is no default version string for the application, it should return an error if provided version is empty string.
function onChanOpenInit(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  version: string) => (version: string, err: Error) {
    // defined by the module
}

OnChanOpenTry

onChanOpenTry will verify the INIT-chosen parameters along with the counterparty-chosen version string and perform custom TRY logic. If the INIT-chosen parameters are invalid, the callback must return an error to abort the handshake. If the counterparty-chosen version is not compatible with this modules supported versions, the callback must return an error to abort the handshake. If the versions are compatible, the try callback must select the final version string and return it to core IBC. onChanOpenTry may also perform custom initialization logic
function onChanOpenTry(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) => (version: string, err: Error) {
    // defined by the module
}

OnChanOpenAck

onChanOpenAck will error if the counterparty selected version string is invalid to abort the handshake. It may also perform custom ACK logic.
function onChanOpenAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier, 
  counterpartyVersion: string) {
    // defined by the module
}

OnChanOpenConfirm

onChanOpenConfirm will perform custom CONFIRM logic and may error to abort the handshake.
function onChanOpenConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // defined by the module
}
function onChanCloseInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // defined by the module
}

function onChanCloseConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier): void {
    // defined by the module
}

function onRecvPacket(packet: Packet, relayer: string): bytes {
    // defined by the module, returns acknowledgement
}

function onTimeoutPacket(packet: Packet, relayer: string) {
    // defined by the module
}

function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: string) {
    // defined by the module
}

function onTimeoutPacketClose(packet: Packet, relayer: string) {
    // defined by the module
}
Exceptions MUST be thrown to indicate failure and reject the handshake, incoming packet, etc. These are combined together in a ModuleCallbacks interface:
interface ModuleCallbacks {
  onChanOpenInit: onChanOpenInit
  onChanOpenTry: onChanOpenTry
  onChanOpenAck: onChanOpenAck
  onChanOpenConfirm: onChanOpenConfirm
  onChanCloseInit: onChanCloseInit
  onChanCloseConfirm: onChanCloseConfirm
  onRecvPacket: onRecvPacket
  onTimeoutPacket: onTimeoutPacket
  onAcknowledgePacket: onAcknowledgePacket
  onTimeoutPacketClose: onTimeoutPacketClose
}
Callbacks are provided when the module binds to a port.
function callbackPath(portIdentifier: Identifier): Path {
    return "callbacks/{portIdentifier}"
}
The calling module identifier is also stored for future authentication should the callbacks need to be altered.
function authenticationPath(portIdentifier: Identifier): Path {
    return "authentication/{portIdentifier}"
}

Port binding as module manager

The IBC routing module sits in-between the handler module (ICS 25) and individual modules on the host state machine. The routing module, acting as a module manager, differentiates between two kinds of ports:
  • “Existing name” ports: e.g. “bank”, with standardised prior meanings, which should not be first-come-first-serve
  • “Fresh name” ports: new identity (perhaps a smart contract) w/no prior relationships, new random number port, post-generation port name can be communicated over another channel
A set of existing names are allocated, along with corresponding modules, when the routing module is instantiated by the host state machine. The routing module then allows allocation of fresh ports at any time by modules, but they must use a specific standardised prefix. The function bindPort can be called by a module in order to bind to a port, through the routing module, and set up callbacks.
function bindPort(
  id: Identifier,
  callbacks: Callbacks): CapabilityKey {
    abortTransactionUnless(privateStore.get(callbackPath(id)) === null)
    privateStore.set(callbackPath(id), callbacks)
    capability = handler.bindPort(id)
    claimCapability(authenticationPath(id), capability)
    return capability
}
The function updatePort can be called by a module in order to alter the callbacks.
function updatePort(
  id: Identifier,
  capability: CapabilityKey,
  newCallbacks: Callbacks) {
    abortTransactionUnless(authenticateCapability(authenticationPath(id), capability))
    privateStore.set(callbackPath(id), newCallbacks)
}
The function releasePort can be called by a module in order to release a port previously in use.
Warning: releasing a port will allow other modules to bind to that port and possibly intercept incoming channel opening handshakes. Modules should release ports only when doing so is safe.
function releasePort(
  id: Identifier,
  capability: CapabilityKey) {
    abortTransactionUnless(authenticateCapability(authenticationPath(id), capability))
    handler.releasePort(id)
    privateStore.delete(callbackPath(id))
    privateStore.delete(authenticationPath(id))
}
The function lookupModule can be used by the routing module to lookup the callbacks bound to a particular port.
function lookupModule(portId: Identifier) {
    return privateStore.get(callbackPath(portId))
}

Datagram handlers (write)

Datagrams are external data blobs accepted as transactions by the routing module. This section defines a handler function for each datagram, which is executed when the associated datagram is submitted to the routing module in a transaction. All datagrams can also be safely submitted by other modules to the routing module. No message signatures or data validity checks are assumed beyond those which are explicitly indicated.

Client lifecycle management

ClientCreate creates a new light client with the specified identifier & consensus state.
interface ClientCreate {
  identifier: Identifier
  clientState: ClientState
  consensusState: ConsensusState
}
function handleClientCreate(datagram: ClientCreate) {
    handler.createClient(datagram.clientState, datagram.consensusState)
}
ClientUpdate updates an existing light client with the specified identifier & new header.
interface ClientUpdate {
  identifier: Identifier
  header: Header
}
function handleClientUpdate(datagram: ClientUpdate) {
    handler.updateClient(datagram.identifier, datagram.header)
}
ClientSubmitMisbehaviour submits proof-of-misbehaviour to an existing light client with the specified identifier.
interface ClientMisbehaviour {
  identifier: Identifier
  evidence: bytes
}
function handleClientMisbehaviour(datagram: ClientMisbehaviour) {
    handler.submitMisbehaviourToClient(datagram.identifier, datagram.evidence)
}

Connection lifecycle management

The ConnOpenInit datagram starts the connection handshake process with an IBC module on another chain.
interface ConnOpenInit {
  counterpartyPrefix: CommitmentPrefix
  clientIdentifier: Identifier
  counterpartyClientIdentifier: Identifier
  version: string
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
}
function handleConnOpenInit(datagram: ConnOpenInit) {
  handler.connOpenInit(
    datagram.counterpartyPrefix,
    datagram.clientIdentifier,
    datagram.counterpartyClientIdentifier,
    datagram.version,
    datagram.delayPeriodTime,
    datagram.delayPeriodBlocks
  )
}
The ConnOpenTry datagram accepts a handshake request from an IBC module on another chain.
interface ConnOpenTry {
  counterpartyConnectionIdentifier: Identifier
  counterpartyPrefix: CommitmentPrefix
  counterpartyClientIdentifier: Identifier
  clientIdentifier: Identifier
  clientState: ClientState // DEPRECATED
  counterpartyVersions: string[]
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
  proofInit: CommitmentProof
  proofClient: CommitmentProof // DEPRECATED
  proofConsensus: CommitmentProof // DEPRECATED
  proofHeight: Height
  consensusHeight: Height // DEPRECATED
}
function handleConnOpenTry(datagram: ConnOpenTry) {
  handler.connOpenTry(
    datagram.counterpartyConnectionIdentifier,
    datagram.counterpartyPrefix,
    datagram.counterpartyClientIdentifier,
    datagram.clientIdentifier,
    datagram.clientState,
    datagram.counterpartyVersions,
    datagram.delayPeriodTime,
    datagram.delayPeriodBlocks,
    datagram.proofInit,
    datagram.proofClient,
    datagram.proofConsensus,
    datagram.proofHeight,
    datagram.consensusHeight
  )
}
The ConnOpenAck datagram confirms a handshake acceptance by the IBC module on another chain.
interface ConnOpenAck {
  identifier: Identifier
  clientState: ClientState // DEPRECATED
  version: string
  counterpartyIdentifier: Identifier
  proofTry: CommitmentProof
  proofClient: CommitmentProof // DEPRECATED
  proofConsensus: CommitmentProof // DEPRECATED
  proofHeight: Height
  consensusHeight: Height // DEPRECATED
}
function handleConnOpenAck(datagram: ConnOpenAck) {
  handler.connOpenAck(
    datagram.identifier,
    datagram.clientState,
    datagram.version,
    datagram.counterpartyIdentifier,
    datagram.proofTry,
    datagram.proofClient,
    datagram.proofConsensus,
    datagram.proofHeight,
    datagram.consensusHeight
  )
}
The ConnOpenConfirm datagram acknowledges a handshake acknowledgement by an IBC module on another chain & finalises the connection.
interface ConnOpenConfirm {
  identifier: Identifier
  proofAck: CommitmentProof
  proofHeight: Height
}
function handleConnOpenConfirm(datagram: ConnOpenConfirm) {
    handler.connOpenConfirm(
      datagram.identifier,
      datagram.proofAck,
      datagram.proofHeight
    )
}

Channel lifecycle management

interface ChanOpenInit {
  order: ChannelOrder
  connectionHops: [Identifier]
  portIdentifier: Identifier
  counterpartyPortIdentifier: Identifier
  version: string
}
function handleChanOpenInit(datagram: ChanOpenInit) {
  module = lookupModule(datagram.portIdentifier)
  channelIdentifier, channelCapability = handler.chanOpenInit(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    datagram.counterpartyPortIdentifier
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // in this case, the channel identifier will be stored with a sentinel value in the channel path so it is not taken
  // by a new channel handshake and the capability is reserved for the application module.
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an INIT state with the right version and the handshake can proceed on the counterparty.
  version, err = module.onChanOpenInit(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.version
  )
  abortTransactionUnless(err === nil)
  writeChannel(
    datagram.portIdentifier,
    channelIdentifier,
    INIT,
    datagram.order,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.connectionHops,
    version
  )
}
interface ChanOpenTry {
  order: ChannelOrder
  connectionHops: [Identifier]
  portIdentifier: Identifier
  channelIdentifier: Identifier
  counterpartyPortIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  counterpartyVersion: string
  proofInit: CommitmentProof
  proofHeight: Height
}
function handleChanOpenTry(datagram: ChanOpenTry) {
  module = lookupModule(datagram.portIdentifier)
  channelIdentifier, channelCapability = handler.chanOpenTry(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion,
    datagram.proofInit,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // in this case, the channel identifier will be stored with a sentinel value in the channel path so it is not taken
  // by a new channel handshake and the capability is reserved for the application module.
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into a TRY state with the right version and the handshake can proceed on the counterparty.
  version, err = module.onChanOpenTry(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion
  )
  abortTransactionUnless(err === nil)
  writeChannel(
    datagram.portIdentifier,
    channelIdentifier,
    TRYOPEN,
    datagram.order,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.connectionHops,
    version
  )
}
interface ChanOpenAck {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  counterpartyVersion: string
  proofTry: CommitmentProof
  proofHeight: Height
}
function handleChanOpenAck(datagram: ChanOpenAck) {
  module = lookupModule(datagram.portIdentifier)
  handler.chanOpenAck(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion,
    datagram.proofTry,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an OPEN state and the handshake can proceed on the counterparty.
  err = module.onChanOpenAck(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion
  )
  abortTransactionUnless(err === nil)
  channel = provableStore.get(channelPath(datagram.portIdentifier, datagram.channelIdentifier))
  writeChannel(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    OPEN,
    channel.order,
    channel.counterparty.portIdentifier,
    datagram.counterpartyChannelIdentifier,
    channel.connectionHops,
    datagram.counterpartyVersion
  )
}
interface ChanOpenConfirm {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  proofAck: CommitmentProof
  proofHeight: Height
}
function handleChanOpenConfirm(datagram: ChanOpenConfirm) {
  module = lookupModule(datagram.portIdentifier)
  handler.chanOpenConfirm(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.proofAck,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an OPEN state and the handshake can proceed on the counterparty.
  err = module.onChanOpenConfirm(
    datagram.portIdentifier,
    datagram.channelIdentifier
  )
  abortTransactionUnless(err === nil)
  channel = provableStore.get(channelPath(datagram.portIdentifier, datagram.channelIdentifier))
  writeChannel(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    OPEN,
    channel.order,
    channel.counterparty.portIdentifier,
    channel.counterparty.channelIdentifier,
    channel.connectionHops,
    channel.version
  )
}
interface ChanCloseInit {
  portIdentifier: Identifier
  channelIdentifier: Identifier
}
function handleChanCloseInit(datagram: ChanCloseInit) {
    module = lookupModule(datagram.portIdentifier)
    err = module.onChanCloseInit(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
    abortTransactionUnless(err === nil)
    handler.chanCloseInit(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
}
interface ChanCloseConfirm {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  proofInit: CommitmentProof
  proofHeight: Height
}
function handleChanCloseConfirm(datagram: ChanCloseConfirm) {  
    handler.chanCloseConfirm(
      datagram.portIdentifier,
      datagram.channelIdentifier,
      datagram.proofInit,
      datagram.proofHeight
    )
    // SYNCHRONOUS: the following calls happen synchronously with the call above
    // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
    // When the module eventually executes its callback it must call writeChannel so that the channel
    // can be written into a CLOSED state and the handshake can proceed on the counterparty.
    module = lookupModule(datagram.portIdentifier)
    err = module.onChanCloseConfirm(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
    abortTransactionUnless(err === nil)
    writeChannel(
      datagram.portIdentifier,
      datagram.channelIdentifier,
      CLOSED,
    )
}

Packet relay

Packets are sent by the module directly (by the module calling the IBC handler).
interface PacketRecv {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketRecv(datagram: PacketRecv) {
  module = lookupModule(datagram.packet.destPort)
  handler.recvPacket(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight,
    acknowledgement
  )
  acknowledgement = module.onRecvPacket(datagram.packet)
  writeAcknowledgement(datagram.packet, acknowledgement)
}
interface PacketAcknowledgement {
  packet: Packet
  acknowledgement: string
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketAcknowledgement(datagram: PacketAcknowledgement) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.acknowledgePacket(
    datagram.packet,
    datagram.acknowledgement,
    datagram.proof,
    datagram.proofHeight
  )
  module.onAcknowledgePacket(
    datagram.packet,
    datagram.acknowledgement
  )   
}

Packet timeouts

interface PacketTimeout {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
  nextSequenceRecv: Maybe<uint64>
}
function handlePacketTimeout(datagram: PacketTimeout) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.timeoutPacket(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight,
    datagram.nextSequenceRecv
  )
  module.onTimeoutPacket(datagram.packet)
}
interface PacketTimeoutOnClose {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketTimeoutOnClose(datagram: PacketTimeoutOnClose) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.timeoutOnClose(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight
  )
  module.onTimeoutPacket(datagram.packet)
}

Closure-by-timeout & packet cleanup

interface PacketCleanup {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
  nextSequenceRecvOrAcknowledgement: Either<uint64, bytes>
}

Query (read-only) functions

All query functions for clients, connections, and channels should be exposed (read-only) directly by the IBC handler module.

Interface usage example

See ICS 20 for a usage example.

Properties & Invariants

  • Proxy port binding is first-come-first-serve: once a module binds to a port through the IBC routing module, only that module can utilise that port until the module releases it.

Backwards Compatibility

Not applicable.

Forwards Compatibility

routing modules are closely tied to the IBC handler interface.

Example Implementations

History

Jun 9, 2019 - Draft submitted Jul 28, 2019 - Major revisions Aug 25, 2019 - Major revisions Mar 28, 2023 - Fix order of executing module handler and application callback All content herein is licensed under Apache 2.0.