概要

本规范描述了一种回环客户端,用于通过 IBC 接口与同一账本上的模块进行交互。

动机

在某些场景下,调用模块事先并不知道目标模块具体位于何处,但希望使用统一的 IBC 消息传递接口(类似于 TCP/IP 中的 127.0.0.1),此时回环客户端可能会很有用。

定义

函数和术语的定义见 ICS 2。 ConnectionEnd 和 generateIdentifier 的定义见 ICS 3 getCommitmentPrefix 的定义见 ICS 24。 removePrefix 的定义见 ICS 23。

期望属性

应当保留预期的客户端语义,并且回环抽象的成本应当可以忽略不计。

技术规范

数据结构

回环客户端不需要共识状态、头部或证据等数据结构。回环客户端不需要存储远程链的共识状态,因为状态验证不需要针对先前已验证的承诺根检查 Merkle 证明。
type ConsensusState object

type Header object

type Misbehaviour object

客户端状态

回环客户端状态会跟踪本地账本的最新高度。
interface ClientState {
  latestHeight: Height
}

高度

回环客户端状态的高度由两个 uint64 组成:修订号,以及该修订中的高度。
interface Height {
  revisionNumber: uint64
  revisionHeight: uint64
}

哨兵对象

与 TCP/IP 中仅存在一个回环地址类似,该协议定义了一个唯一的哨兵 ClientState 实例,其客户端标识符为 09-localhost。必须禁止创建其他回环客户端。 此外,实现 将会 保留一个特殊的连接标识符 connection-localhost,供默认存储的唯一哨兵 ConnectionEnd 使用(即在创世时或升级时存储)。该连接端的 clientIdentifier 和 counterpartyClientIdentifier 都引用哨兵 09-localhost 客户端标识符。counterpartyConnectionIdentifier 则引用特殊连接标识符 connection-localhost。哨兵回环连接端的存在,使 IBC 应用能够直接在该哨兵连接之上建立通道。随后,只需在 ChanOpenInit 数据报的 connectionHops 参数中提供回环连接标识符(connection-localhost),即可发起通道握手。 实现 也可以 允许创建更多与回环客户端关联的连接。这些连接将使用由 generateIdentifier 生成的连接标识符。

中继器消息

支持 localhost 数据包流的中继器必须进行适配,以便将来自发送应用的消息重新提交回源链。 这要求首先检查任何通道级消息所依赖的底层连接标识符。如果底层连接标识符是 connection-localhost,则中继器必须构造该消息并将其发送回源链。由于回环客户端不需要远程账本状态的 Merkle 证明,因此消息中的证明必须使用一个哨兵字节 []byte{0x01};消息中的证明高度可以为零,因为回环客户端会忽略它。 实现 也可以 选择实现一种无需中继器驱动交易、自动调用握手或数据包流中下一条消息的回环方式。不过,实现者必须注意确保自动执行消息不会引发 gas 消耗问题。

客户端初始化

回环客户端初始化需要本地账本的最新高度。
function initialise(identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) {
  assert(clientState.latestHeight > 0)
  assert(consensusState === nil)
  
  provableStore.set("clients/{identifier}/clientState", clientState)
}

有效性判定

回环客户端不需要进行有效性检查;该函数不应被调用。
function verifyClientMessage(clientMsg: ClientMessage) {
  assert(false)
}

误行为判定

回环客户端不需要进行误行为检查;该函数不应被调用。
function checkForMisbehaviour(clientMsg: clientMessage) => bool {
  return false
}

更新状态

函数 updateState 将对回环客户端执行常规更新。clientState 将更新为本地账本的最新高度。该函数应当在每个高度自动调用。
function updateState(clientMsg: clientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")

  // retrieve the latest height from the local ledger
  height = getSelfHeight()
  clientState.latestHeight = height

  // save the client state
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}

在误行为时更新状态

函数 updateStateOnMisbehaviour 不受回环客户端支持,并且执行空操作。
function updateStateOnMisbehaviour(clientMsg: clientMessage) { }

状态验证函数

状态验证函数只需要从本地账本读取状态,并与标准化键路径下存储的字节进行比较。回环客户端需要对本地账本的整个 IBC 存储具有只读访问权限,而不仅仅是对其自身客户端标识符前缀的存储具有访问权限。
function verifyMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: []byte
): Error {
  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since verification is done on the IBC store of the local ledger
  // the prefix needs to be removed from the path to retrieve the
  // correct key in the store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)
  
  // The complete (not only client identifier-prefixed) store is needed
  // to verify that a path has been set to a particular value
  if provableStore.get(unprefixedPath) !== value {
    return error
  }
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath
): Error {
  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since verification is done on the IBC store of the local ledger
  // the prefix needs to be removed from the path to retrieve the
  // correct key in the store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)

  // The complete (not only client identifier-prefixed) store is needed
  // to verify that a path has not been set to a particular value
  if provableStore.get(unprefixedPath) !== nil {
    return error
  }
  return nil
}

属性与不变量

其语义应当等同于这是本地账本的一个远程客户端。

向后兼容性

不适用。

向前兼容性

不适用。对客户端算法的修改将需要一个新的客户端标准。

示例实现

历史

2020 年 1 月 17 日 - 初始版本 2023 年 3 月 2 日 - 在 ICS 02 重构及引入哨兵对象后更新。

版权

本文所有内容均依据 Apache 2.0 许可。

Synopsis

This specification describes a loopback client, designed to be used for interaction over the IBC interface with modules present on the same ledger.

Motivation

Loopback clients may be useful in cases where the calling module does not have prior knowledge of where precisely the destination module lives and would like to use the uniform IBC message-passing interface (similar to 127.0.0.1 in TCP/IP).

Definitions

Functions & terms are as defined in ICS 2. ConnectionEnd and generateIdentifier are as defined in ICS 3 getCommitmentPrefix is as defined in ICS 24. removePrefix is as defined in ICS 23.

Desired Properties

Intended client semantics should be preserved, and loopback abstractions should be negligible cost.

Technical Specification

Data Structures

No consensus state, headers, or evidence data structures are required for a loopback client. The loopback client does not need to store the consensus state of a remote chain, since state verification does not require to check a Merkle proof against a previously validated commitment root.
type ConsensusState object

type Header object

type Misbehaviour object

Client state

The loopback client state tracks the latest height of the local ledger.
interface ClientState {
  latestHeight: Height
}

Height

The height of a loopback client state consists of two uint64s: the revision number, and the height in the revision.
interface Height {
  revisionNumber: uint64
  revisionHeight: uint64
}

Sentinel objects

Similarly as in TCP/IP, where there exists a single loopback address, the protocol defines the existence of a single sentinel ClientState instance with the client identifier 09-localhost. Creation of other loopback clients MUST be forbidden. Additionally, implementations will reserve a special connection identifier connection-localhost to be used by a single sentinel ConnectionEnd stored by default (i.e. at genesis or upgrade). The clientIdentifier and counterpartyClientIdentifier of the connection end both reference the sentinel 09-localhost client identifier. The counterpartyConnectionIdentifier references the special connection identifier connection-localhost. The existence of a sentinel loopback connection end enables IBC applications to establish channels directly on top of the sentinel connection. Channel handshakes can then be initiated by supplying the loopback connection identifier (connection-localhost) in the connectionHops parameter of the ChanOpenInit datagram. Implementations may also allow the creation of more connections associated with the loopback client. These connections would then have a connection identifier as generated by generateIdentifier.

Relayer messages

Relayers supporting localhost packet flow must be adapted to submit messages from sending applications back to the originating chain. This would require first checking the underlying connection identifier on any channel-level messages. If the underlying connection identifier is connection-localhost, then the relayer must construct the message and send it back to the originating chain. The message MUST be constructed with a sentinel byte for the proof ([]byte{0x01}), since the loopback client does not need Merkle proofs of the state of a remote ledger; the proof height in the message may be zero, since it is ignored by the loopback client. Implementations may choose to implement loopback such that the next message in the handshake or packet flow is automatically called without relayer-driven transactions. However, implementers must take care to ensure that automatic message execution does not cause gas consumption issues.

Client initialisation

Loopback client initialisation requires the latest height of the local ledger.
function initialise(identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) {
  assert(clientState.latestHeight > 0)
  assert(consensusState === nil)
  
  provableStore.set("clients/{identifier}/clientState", clientState)
}

Validity predicate

No validity checking is necessary in a loopback client; the function should never be called.
function verifyClientMessage(clientMsg: ClientMessage) {
  assert(false)
}

Misbehaviour predicate

No misbehaviour checking is necessary in a loopback client; the function should never be called.
function checkForMisbehaviour(clientMsg: clientMessage) => bool {
  return false
}

Update state

Function updateState will perform a regular update for the loopback client. The clientState will be updated with the latest height of the local ledger. This function should be called automatically at every height.
function updateState(clientMsg: clientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")

  // retrieve the latest height from the local ledger
  height = getSelfHeight()
  clientState.latestHeight = height

  // save the client state
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}

Update state on misbehaviour

Function updateStateOnMisbehaviour is unsupported by the loopback client and performs a no-op.
function updateStateOnMisbehaviour(clientMsg: clientMessage) { }

State verification functions

State verification functions simply need to read state from the local ledger and compare with the bytes stored under the standardized key paths. The loopback client needs read-only access to the entire IBC store of the local ledger, and not only to its own client identifier-prefixed store.
function verifyMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: []byte
): Error {
  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since verification is done on the IBC store of the local ledger
  // the prefix needs to be removed from the path to retrieve the
  // correct key in the store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)
  
  // The complete (not only client identifier-prefixed) store is needed
  // to verify that a path has been set to a particular value
  if provableStore.get(unprefixedPath) !== value {
    return error
  }
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath
): Error {
  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since verification is done on the IBC store of the local ledger
  // the prefix needs to be removed from the path to retrieve the
  // correct key in the store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)

  // The complete (not only client identifier-prefixed) store is needed
  // to verify that a path has not been set to a particular value
  if provableStore.get(unprefixedPath) !== nil {
    return error
  }
  return nil
}

Properties & Invariants

Semantics are as if this were a remote client of the local ledger.

Backwards Compatibility

Not applicable.

Forwards Compatibility

Not applicable. Alterations to the client algorithm will require a new client standard.

Example Implementations

History

January 17, 2020 - Initial version March 2, 2023 - Update after ICS 02 refactor and addition of sentinel objects. All content herein is licensed under Apache 2.0.