概述

本标准规定了端口分配系统,模块可通过该系统绑定由 IBC 处理程序分配的唯一命名端口。 随后,这些端口可用于打开通道,并且可以由最初绑定它们的模块进行转移或稍后释放。

动机

跨区块链通信协议旨在促进模块到模块的通信,其中模块是在主权账本上执行的、相互独立、可能彼此不信任且自包含的代码单元。为了提供所需的端到端语义,IBC 处理程序必须将通道权限授予特定模块。 本规范定义了实现该模型的端口分配与所有权系统。 对于某个特定端口名绑定何种模块逻辑,可能会逐渐形成约定,例如将 bank 用于同质化代币处理,或将 staking 用于链间抵押。 这类似于端口 80 常用于 HTTP 服务器这一事实,但协议本身无法强制要求某种特定模块逻辑必须实际绑定到这些约定端口,因此用户必须自行检查。 也可以创建带有伪随机标识符的临时端口,用于临时性协议处理。 模块可以绑定多个端口,也可以连接到另一台机器上由其他模块绑定的多个端口。任意数量的(具有唯一标识的)通道都可以同时使用同一个端口。 通道是两个端口之间的端到端连接,而这两个端口都必须事先由某个模块完成绑定,随后该模块将控制通道对应的一端。 可选地,宿主状态机可以选择仅向一个具有特殊权限的模块管理器暴露端口绑定能力, 具体方式是专门为绑定端口这一能力生成一个 capability key。随后,模块管理器 就可以使用自定义规则集来控制模块可以绑定哪些端口,并且仅在它 验证了端口名和模块之后,才将端口转移给模块。该角色可以由路由模块承担(参见 ICS 26)。

定义

Identifier、get、set 和 delete 的定义见 ICS 24。 端口是一类特殊的标识符,用于将打开和使用通道的权限授予模块。 模块是宿主状态机中独立于 IBC 处理程序的子组件。示例包括 Ethereum 智能合约,以及 Cosmos SDK 和 Substrate 模块。 IBC 规范除了要求宿主状态机能够使用对象能力或来源认证机制将端口权限授予模块外,不对模块功能作其他假设。

期望属性

  • 模块一旦绑定某个端口,在其释放该端口之前,其他模块都不能使用该端口
  • 模块可以根据自身选择释放端口,或将其转移给另一个模块
  • 单个模块可以同时绑定多个端口
  • 端口按先到先得方式分配,而已知模块的“保留”端口可以在链首次启动时完成绑定
作为一个有帮助的对比,下列与 TCP 的类比大体上是准确的:
IBC 概念TCP/IP 概念差异
IBCTCP差异很多,参见描述 IBC 的架构文档
端口(例如 bank)端口(例如 80)没有低编号保留端口,端口是字符串
模块(例如 bank)应用程序(例如 Nginx)属于应用特定概念
客户端-没有直接类比,有点像二层路由,也有点像 TLS
连接-没有直接类比,在 TCP 中被折叠进连接语义
通道连接任意数量的通道都可以同时向某个端口打开或从某个端口发起

技术规范

数据结构

宿主状态机必须为模块支持对象能力引用或来源认证中的至少一种。 在前一种对象能力场景下,IBC 处理程序必须能够生成对象能力,即唯一且不透明的引用, 它们可以传递给某个模块,并且不能被其他模块复制。两个示例分别是 Cosmos SDK 中使用的存储键(reference) 以及 Agoric Javascript 运行时中使用的对象引用(reference)。
type CapabilityKey object
newCapability 必须接收一个名称并生成唯一的 capability key,使该名称在本地映射到该 capability key,之后可通过 getCapability 使用它。
function newCapability(name: string): CapabilityKey {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
authenticateCapability 必须接收一个名称和一个 capability,并检查该名称是否在本地映射到了所提供的 capability。该名称可以是不受信任的用户输入。
function authenticateCapability(name: string, capability: CapabilityKey): bool {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
claimCapability 必须接收一个名称和一个 capability(由另一个模块提供),并在本地将该名称映射到此 capability,从而“声明”其供未来使用。
function claimCapability(name: string, capability: CapabilityKey) {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
getCapability 必须允许模块通过名称查找其先前创建或声明的 capability。
function getCapability(name: string): CapabilityKey {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
releaseCapability 必须允许模块释放其拥有的 capability。
function releaseCapability(capability: CapabilityKey) {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
在后一种来源认证场景下,IBC 处理程序必须能够安全地读取调用模块的来源标识符, 即宿主状态机中每个模块唯一对应的字符串,该字符串不能被模块修改,也不能被其他模块伪造。 一个示例是 Ethereum 中使用的智能合约地址(reference)。
type SourceIdentifier string
function callingModuleIdentifier(): SourceIdentifier {
  // provided by host state machine, e.g. contract address in Ethereum
}
随后,newCapability、authenticateCapability、claimCapability、getCapability 和 releaseCapability 的实现如下:
function newCapability(name: string): CapabilityKey {
  return callingModuleIdentifier()
}
function authenticateCapability(name: string, capability: CapabilityKey) {
  return callingModuleIdentifier() === name
}
function claimCapability(name: string, capability: CapabilityKey) {
  // no-op
}
function getCapability(name: string): CapabilityKey {
  // not actually used
  return nil
}
function releaseCapability(capability: CapabilityKey) {
  // no-op
}

存储路径

portPath 接收一个 Identifier,并返回与某个端口关联的对象能力引用或所有者模块标识符应当存储的路径。
function portPath(id: Identifier): Path {
    return "ports/{id}"
}

子协议

标识符校验

端口的所有者模块标识符存储在某个唯一的 Identifier 前缀之下。 可以提供校验函数 validatePortIdentifier。
type validatePortIdentifier = (id: Identifier) => boolean
如果未提供,则默认的 validatePortIdentifier 函数始终返回 true。

绑定到端口

IBC 处理程序必须实现 bindPort。bindPort 会绑定一个尚未分配的端口;如果该端口已经被分配,则必须失败。 如果宿主状态机未实现用于控制端口分配的特殊模块管理器,则 bindPort 应当对所有模块可用。如果实现了,则 bindPort 应当只能由模块管理器调用。
function bindPort(id: Identifier): CapabilityKey {
    abortTransactionUnless(validatePortIdentifier(id))
    abortTransactionUnless(getCapability(portPath(id)) === null)
    capability = newCapability(portPath(id))
    return capability
}

转移端口所有权

如果宿主状态机支持对象能力,则无需额外协议,因为端口引用本身就是一种持有者能力。

释放端口

IBC 处理程序必须实现 releasePort 函数,它允许模块释放某个端口,以便其他模块随后可以绑定该端口。 releasePort 应当对所有模块可用。
警告:释放端口将允许其他模块绑定到该端口,并可能拦截传入的通道打开握手。模块仅应在确认安全的情况下释放端口。
function releasePort(id: Identifier, capability: CapabilityKey) {
    abortTransactionUnless(authenticateCapability(portPath(id), capability))
    releaseCapability(capability)
}

属性与不变量

  • 默认情况下,端口标识符按先到先得方式分配:一旦某个模块绑定了某个端口,在该模块转移或释放该端口之前,只有该模块可以使用该端口。模块管理器可以实现覆盖此行为的自定义逻辑。

向后兼容性

不适用。

向前兼容性

端口绑定不是线协议,因此只要所有权语义不受影响,不同链上的接口可以独立演进。

示例实现

历史

2019 年 6 月 29 日 - 初始草案

版权

此处所有内容均基于 Apache 2.0 许可。

Synopsis

This standard specifies the port allocation system by which modules can bind to uniquely named ports allocated by the IBC handler. Ports can then be used to open channels and can be transferred or later released by the module which originally bound to them.

Motivation

The interblockchain communication protocol is designed to facilitate module-to-module traffic, where modules are independent, possibly mutually distrusted, self-contained elements of code executing on sovereign ledgers. In order to provide the desired end-to-end semantics, the IBC handler must permission channels to particular modules. This specification defines the port allocation and ownership system which realises that model. Conventions may emerge as to what kind of module logic is bound to a particular port name, such as “bank” for fungible token handling or “staking” for interchain collateralisation. This is analogous to port 80’s common use for HTTP servers — the protocol cannot enforce that particular module logic is actually bound to conventional ports, so users must check that themselves. Ephemeral ports with pseudorandom identifiers may be created for temporary protocol handling. Modules may bind to multiple ports and connect to multiple ports bound to by another module on a separate machine. Any number of (uniquely identified) channels can utilise a single port simultaneously. Channels are end-to-end between two ports, each of which must have been previously bound to by a module, which will then control that end of the channel. Optionally, the host state machine can elect to expose port binding only to a specially-permissioned module manager, by generating a capability key specifically for the ability to bind ports. The module manager can then control which ports modules can bind to with a custom rule-set, and transfer ports to modules only when it has validated the port name & module. This role can be played by the routing module (see ICS 26).

Definitions

Identifier, get, set, and delete are defined as in ICS 24. A port is a particular kind of identifier which is used to permission channel opening and usage to modules. A module is a sub-component of the host state machine independent of the IBC handler. Examples include Ethereum smart contracts and Cosmos SDK & Substrate modules. The IBC specification makes no assumptions of module functionality other than the ability of the host state machine to use object-capability or source authentication to permission ports to modules.

Desired Properties

  • Once a module has bound to a port, no other modules can use that port until the module releases it
  • A module can, on its option, release a port or transfer it to another module
  • A single module can bind to multiple ports at once
  • Ports are allocated first-come first-serve, and “reserved” ports for known modules can be bound when the chain is first started
As a helpful comparison, the following analogies to TCP are roughly accurate:
IBC ConceptTCP/IP ConceptDifferences
IBCTCPMany, see the architecture documents describing IBC
Port (e.g. “bank”)Port (e.g. 80)No low-number reserved ports, ports are strings
Module (e.g. “bank”)Application (e.g. Nginx)Application-specific
Client-No direct analogy, a bit like L2 routing and a bit like TLS
Connection-No direct analogy, folded into connections in TCP
ChannelConnectionAny number of channels can be opened to or from a port simultaneously

Technical Specification

Data Structures

The host state machine MUST support either object-capability reference or source authentication for modules. In the former object-capability case, the IBC handler must have the ability to generate object-capabilities, unique, opaque references which can be passed to a module and will not be duplicable by other modules. Two examples are store keys as used in the Cosmos SDK (reference) and object references as used in Agoric’s Javascript runtime (reference).
type CapabilityKey object
newCapability must take a name and generate a unique capability key, such that the name is locally mapped to the capability key and can be used with getCapability later.
function newCapability(name: string): CapabilityKey {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
authenticateCapability must take a name & a capability and check whether the name is locally mapped to the provided capability. The name can be untrusted user input.
function authenticateCapability(name: string, capability: CapabilityKey): bool {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
claimCapability must take a name & a capability (provided by another module) and locally map the name to the capability, “claiming” it for future usage.
function claimCapability(name: string, capability: CapabilityKey) {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
getCapability must allow a module to lookup a capability which it has previously created or claimed by name.
function getCapability(name: string): CapabilityKey {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
releaseCapability must allow a module to release a capability which it owns.
function releaseCapability(capability: CapabilityKey) {
  // provided by host state machine, e.g. ADR 3 / ScopedCapabilityKeeper in Cosmos SDK
}
In the latter source authentication case, the IBC handler must have the ability to securely read the source identifier of the calling module, a unique string for each module in the host state machine, which cannot be altered by the module or faked by another module. An example is smart contract addresses as used by Ethereum (reference).
type SourceIdentifier string
function callingModuleIdentifier(): SourceIdentifier {
  // provided by host state machine, e.g. contract address in Ethereum
}
newCapability, authenticateCapability, claimCapability, getCapability, and releaseCapability are then implemented as follows:
function newCapability(name: string): CapabilityKey {
  return callingModuleIdentifier()
}
function authenticateCapability(name: string, capability: CapabilityKey) {
  return callingModuleIdentifier() === name
}
function claimCapability(name: string, capability: CapabilityKey) {
  // no-op
}
function getCapability(name: string): CapabilityKey {
  // not actually used
  return nil
}
function releaseCapability(capability: CapabilityKey) {
  // no-op
}

Store paths

portPath takes an Identifier and returns the store path under which the object-capability reference or owner module identifier associated with a port should be stored.
function portPath(id: Identifier): Path {
    return "ports/{id}"
}

Sub-protocols

Identifier validation

Owner module identifier for ports are stored under a unique Identifier prefix. The validation function validatePortIdentifier MAY be provided.
type validatePortIdentifier = (id: Identifier) => boolean
If not provided, the default validatePortIdentifier function will always return true.

Binding to a port

The IBC handler MUST implement bindPort. bindPort binds to an unallocated port, failing if the port has already been allocated. If the host state machine does not implement a special module manager to control port allocation, bindPort SHOULD be available to all modules. If it does, bindPort SHOULD only be callable by the module manager.
function bindPort(id: Identifier): CapabilityKey {
    abortTransactionUnless(validatePortIdentifier(id))
    abortTransactionUnless(getCapability(portPath(id)) === null)
    capability = newCapability(portPath(id))
    return capability
}

Transferring ownership of a port

If the host state machine supports object-capabilities, no additional protocol is necessary, since the port reference is a bearer capability.

Releasing a port

The IBC handler MUST implement the releasePort function, which allows a module to release a port such that other modules may then bind to it. releasePort SHOULD be available to all modules.
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(portPath(id), capability))
    releaseCapability(capability)
}

Properties & Invariants

  • By default, port identifiers are first-come-first-serve: once a module has bound to a port, only that module can utilise the port until the module transfers or releases it. A module manager can implement custom logic which overrides this.

Backwards Compatibility

Not applicable.

Forwards Compatibility

Port binding is not a wire protocol, so interfaces can change independently on separate chains as long as the ownership semantics are unaffected.

Example Implementations

History

Jun 29, 2019 - Initial draft All content herein is licensed under Apache 2.0.