概要

本文档描述了 IBC 连接 的抽象:位于两条独立链上的两个有状态对象(连接端),它们各自关联到对方链的一个轻客户端,并共同促进跨链子状态验证与数据包关联(通过通道)。本文还描述了用于在两条链之间安全建立连接的协议。

动机

IBC 核心协议为数据包提供了 授权 和 排序 语义:前者保证数据包已在发送区块链上提交(并已执行相应的状态转换,例如托管代币),后者保证数据包恰好按照特定顺序提交一次,并且也只能按该顺序恰好交付一次。本标准所规定的 连接 抽象,与 ICS 2 中规定的 客户端 抽象结合,共同定义了 IBC 的 授权 语义。排序语义见 ICS 4)。

定义

与客户端相关的类型和函数定义见 ICS 2。 与通道和数据包相关的函数定义见 ICS 4。 与承诺证明相关的类型和函数定义见 ICS 23 Identifier 及其他主机状态机要求定义见 ICS 24。该标识符不一定旨在作为人类可读名称(而且很可能也不应如此,以避免标识符被抢注或被争抢)。 开启握手协议允许每条链验证对方链上用于引用该连接的标识符,使得每条链上的模块都能够推断对方链上的该引用。 本规范中所称的 参与者,是指能够执行数据报、并为计算/存储付费(通过 gas 或类似机制)、但在其他方面不受信任的实体。可能的参与者包括:
  • 使用账户密钥签名的终端用户
  • 自主运行或响应其他交易的链上智能合约
  • 响应其他交易或按计划方式运行的链上模块

期望属性

  • 实现该标准的区块链应能够安全地允许不受信任的参与者开启和更新连接。

建立前

在连接建立之前:
  • 不应运行任何进一步的 IBC 子协议,因为无法验证跨链子状态。
  • 发起参与者(即创建连接者)必须能够为待连接链指定一个初始共识状态,并为发起连接的链指定一个初始共识状态(隐式指定也可以,例如通过发送交易)。

握手期间

一旦协商握手开始:
  • 只能按顺序执行适当的握手数据报。
  • 任何第三条链都不能冒充两条握手链中的任意一方

建立后

一旦协商握手完成:
  • 两条链上创建的连接对象都包含由发起参与者指定的共识状态。
  • 不能通过重放数据报,在其他链上恶意创建其他连接对象。

技术规范

数据结构

本 ICS 定义了 ConnectionState 和 ConnectionEnd 类型:
enum ConnectionState {
  INIT,
  TRYOPEN,
  OPEN,
}
interface ConnectionEnd {
  state: ConnectionState
  counterpartyConnectionIdentifier: Identifier
  counterpartyPrefix: CommitmentPrefix
  clientIdentifier: Identifier
  counterpartyClientIdentifier: Identifier
  version: string | []string
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
}
  • state 字段描述连接端的当前状态。
  • counterpartyConnectionIdentifier 字段标识与此连接关联的对手链上的连接端。
  • counterpartyPrefix 字段包含与此连接关联的对手链上进行状态验证时使用的前缀。 链应暴露一个端点,允许中继器查询连接前缀。 如果未指定,应使用默认的 counterpartyPrefix 值 "ibc"。
  • clientIdentifier 字段标识与此连接关联的客户端。
  • counterpartyClientIdentifier 字段标识与此连接关联的对手链上的客户端。
  • version 字段是一个不透明字符串,可用于确定使用此连接的通道或数据包所采用的编码或协议。 如果未指定,应使用默认的 version 值 ""。
  • delayPeriodTime 表示一个时间周期:在某个头被验证之后,必须经过该时长,数据包、确认、接收证明或超时才能被处理。
  • delayPeriodBlocks 表示一个区块周期:在某个头被验证之后,必须经过该区块数,数据包、确认、接收证明或超时才能被处理。

存储路径

连接路径存储在一个唯一标识符之下。
function connectionPath(id: Identifier): Path {
  return "connections/{id}"
}
从客户端到一组连接的反向映射(用于查找使用某个客户端的所有连接)存储在每个客户端唯一前缀之下:
function clientConnectionsPath(clientIdentifier: Identifier): Path {
  return "clients/{clientIdentifier}/connections"
}

辅助函数

addConnectionToClient 用于将一个连接标识符添加到与某个客户端关联的连接集合中。
function addConnectionToClient(
  clientIdentifier: Identifier,
  connectionIdentifier: Identifier) {
    conns = privateStore.get(clientConnectionsPath(clientIdentifier))
    conns.add(connectionIdentifier)
    privateStore.set(clientConnectionsPath(clientIdentifier), conns)
}
连接定义了这些辅助函数,用于将与连接关联的 CommitmentPrefix 传递给客户端提供的验证函数。在规范的其他部分中,探查其他链状态时 MUST 使用这些函数,而不是直接调用客户端上的验证函数。
function verifyClientConsensusState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  clientIdentifier: Identifier,
  consensusStateHeight: Height,
  consensusState: ConsensusState
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, consensusStatePath(clientIdentifier, consensusStateHeight))
  return verifyMembership(clientState, height, 0, 0, proof, path, consensusState)
}

function verifyClientState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  clientState: ClientState
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, clientStatePath(clientIdentifier)
  return verifyMembership(clientState, height, 0, 0, proof, path, clientState)
}

function verifyConnectionState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  connectionIdentifier: Identifier,
  connectionEnd: ConnectionEnd
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, connectionPath(connectionIdentifier))
  return verifyMembership(clientState, height, 0, 0, proof, path, connectionEnd)
}

function verifyChannelState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  channelEnd: ChannelEnd
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, channelPath(portIdentifier, channelIdentifier))
  return verifyMembership(clientState, height, 0, 0, proof, path, channelEnd)
}

function verifyPacketCommitment(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  commitmentBytes: bytes
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetCommitmentPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path, commitmentBytes)
}

function verifyPacketAcknowledgement(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  acknowledgement: bytes
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetAcknowledgementPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path, acknowledgement)
}

function verifyPacketReceiptAbsence(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetReceiptPath(portIdentifier, channelIdentifier, sequence))
  return verifyNonMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path)
}

// OPTIONAL: verifyPacketReceipt is only required to support new channel types beyond ORDERED and UNORDERED.
function verifyPacketReceipt(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  receipt: bytes
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetReceiptPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, connection.counterpartyPrefix, proof, portIdentifier, channelIdentifier, sequence, receipt)
}

function verifyNextSequenceRecv(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  nextSequenceRecv: uint64
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, nextSequenceRecvPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path, nextSequenceRecv)
}

function verifyMultihopMembership(
  connection: ConnectionEnd, // the connection end corresponding to the receiving chain.
  height: Height,
  proof: MultihopProof,
  connectionHops: []Identifier,
  key: CommitmentPath,
  value: bytes
) {
  // the connectionEnd corresponding to the end of the multi-hop channel path (sending/counterparty chain).
  multihopConnectionEnd = abortTransactionUnless(getMultihopConnectionEnd(proof))
  prefix = multihopConnectionEnd.GetCounterparty().GetPrefix()
  client = queryClient(connection.clientIdentifier)
  consensusState = queryConsensusState(connection.clientIdentifier, height)

  abortTransactionUnless(client.Status() === "active")
  abortTransactionUnless(client.GetLatestHeight() >= height)

  // verify maximum delay period has passed
  expectedTimePerBlock = queryMaxExpectedTimePerBlock()
  delayPeriodTime = abortTransactionUnless(getMaximumDelayPeriod(proof, connection))
  delayPeriodBlocks = getBlockDelay(delayPeriodTime, expectedTimePerBlock)
  abortTransactionUnless(tendermint.VerifyDelayPeriodPassed(height, delayPeriodTime, delayPeriodBlocks))

  return multihop.VerifyMultihopMembership(consensusState, connectionHops, proof, prefix, key, value) // see ics-033
}

function verifyMultihopNonMembership(
  connection: ConnectionEnd, // the connection end corresponding to the receiving chain.
  height: Height,
  proof: MultihopProof,
  connectionHops: Identifier[],
  key: CommitmentPath
) {
  // the connectionEnd corresponding to the end of the multi-hop channel path (sending/counterparty chain).
  multihopConnectionEnd = abortTransactionUnless(getMultihopConnectionEnd(proof))
  prefix = multihopConnectionEnd.GetCounterparty().GetPrefix()
  client = queryClient(connection.clientIdentifier)
  consensusState = queryConsensusState(connection.clientIdentifier, height)

  abortTransactionUnless(client.Status() === "active")
  abortTransactionUnless(client.GetLatestHeight() >= height)

  // verify maximum delay period has passed
  expectedTimePerBlock = queryMaxExpectedTimePerBlock()
  delayPeriodTime = abortTransactionUnless(getMaximumDelayPeriod(proof, connection))
  delayPeriodBlocks = getBlockDelay(delayPeriodTime, expectedTimePerBlock)
  abortTransactionUnless(tendermint.VerifyDelayPeriodPassed(height, delayPeriodTime, delayPeriodBlocks))

  return multihop.VerifyMultihopNonMembership(consensusState, connectionHops, proof, prefix, key) // see ics-033
}

// Return the maximum expected time per block from the paramstore.
// See 03-connection - GetMaxExpectedTimePerBlock.
function queryMaxExpectedTimePerBlock(): uint64

function getTimestampAtHeight(
  connection: ConnectionEnd,
  height: Height
) {
  return queryConsensusState(connection.clientIdentifier, height).getTimestamp()
}

// Return the connectionEnd corresponding to the source chain.
function getMultihopConnectionEnd(proof: MultihopProof): ConnectionEnd {
  return abortTransactionUnless(Unmarshal(proof.ConnectionProofs[proof.ConnectionProofs.length - 1].Value))
}

// Return the maximum delay period in seconds across all connections in the channel path.
function getMaximumDelayPeriod(proof: MultihopProof, lastConnection: ConnectionEnd): number {
  delayPeriodTime = lastConnection.GetDelayPeriod()
  for connData in range proofs.ConnectionProofs {
    connectionEnd = abortTransactionUnless(Unmarshal(connData.Value))
    if (connectionEnd.DelayPeriod > delayPeriodTime) {
      delayPeriodTime = connectionEnd.DelayPeriod
    }
  }
  return delayPeriodTime
}

子协议

本 ICS 定义了打开握手子协议。连接一旦打开,就不能被关闭,标识符也不能被重新分配(这样可以防止数据包重放或授权混淆)。 头部跟踪和异常行为检测定义见 ICS 2。 状态机图

标识符校验

连接存储在唯一的 Identifier 前缀下。 可以提供校验函数 validateConnectionIdentifier。
type validateConnectionIdentifier = (id: Identifier) => boolean
如果未提供,则默认的 validateConnectionIdentifier 函数始终返回 true。

版本协商

在握手过程中,连接的两端会就与该连接关联的一个版本达成一致。这个 Version 数据类型定义如下:
interface Version {
  identifier: string
  features: [string]
}
identifier 字段指定唯一的版本标识符。值为 "1" 表示 IBC 1.0.0。 features 字段指定与该标识符兼容的特性列表。值 "ORDER_UNORDERED" 和 "ORDER_ORDERED" 分别表示无序通道和有序通道。 宿主状态机 MUST 使用版本数据来协商编码、优先级,或 IBC 之上自定义逻辑所需的连接特定元数据。这里假定执行打开握手的两条链至少有一个共同兼容的版本(即两条链的兼容版本集合必须存在非空交集)。如果两条链没有任何彼此都可接受的版本,则握手将失败。 实现 MUST 定义函数 getCompatibleVersions,返回其支持的版本列表,并按偏好从高到低排序。
type getCompatibleVersions = () => [Version]
实现 MUST 定义函数 pickVersion,用于从版本列表中选择一个版本。
type pickVersion = ([Version]) => Version

打开握手

打开握手子协议用于在两条链之间相互初始化共识状态。 打开握手定义了四个数据报:ConnOpenInit、ConnOpenTry、ConnOpenAck 和 ConnOpenConfirm。 正确的协议执行流程如下(注意,所有调用都根据 ICS 25 通过模块发起):
发起方数据报执行操作的链前置状态(A, B)后置状态(A, B)
参与者ConnOpenInitA(无, 无)(INIT, 无)
中继者ConnOpenTryB(INIT, 无)(INIT, TRYOPEN)
中继者ConnOpenAckA(INIT, TRYOPEN)(OPEN, TRYOPEN)
中继者ConnOpenConfirmB(OPEN, TRYOPEN)(OPEN, OPEN)
在实现该子协议的两条链完成打开握手后,满足以下性质:
  • 每条链都拥有对方正确的共识状态,且该状态与初始发起方最初指定的一致。
  • 每条链都知晓并同意自己在对方链上的标识符。
除防垃圾措施外,该子协议不要求权限控制。 链必须实现一个 generateIdentifier 函数来选择标识符,例如通过递增计数器:
type generateIdentifier = () -> Identifier
也可以选择通过 version 传入特定版本,以确保握手要么以该版本完成,要么失败。 ConnOpenInit 在链 A 上初始化一次连接尝试。
function connOpenInit(
  counterpartyPrefix: CommitmentPrefix,
  clientIdentifier: Identifier,
  counterpartyClientIdentifier: Identifier,
  version: string,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64) {
    // generate a new identifier
    identifier = generateIdentifier()

    abortTransactionUnless(queryClientState(clientIdentifier) !== null)
    abortTransactionUnless(provableStore.get(connectionPath(identifier)) == null)

    state = INIT
    if version != "" {
      // manually selected version must be one we can support
      abortTransactionUnless(getCompatibleVersions().indexOf(version) > -1)
      versions = [version]
    } else {
      versions = getCompatibleVersions()
    }
    connection = ConnectionEnd{state, "", counterpartyPrefix,
      clientIdentifier, counterpartyClientIdentifier, versions, delayPeriodTime, delayPeriodBlocks}
    provableStore.set(connectionPath(identifier), connection)
    addConnectionToClient(clientIdentifier, identifier)
}
ConnOpenTry 将链 A 上的连接尝试通知中继到链 B(这段代码在链 B 上执行)。
function 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,
  hostConsensusStateProof?: bytes, // DEPRECATED
) {
    // generate a new identifier
    identifier = generateIdentifier()

    abortTransactionUnless(queryClientState(clientIdentifier) !== null)
    expectedConnectionEnd = ConnectionEnd{INIT, "", getCommitmentPrefix(), counterpartyClientIdentifier,
                             clientIdentifier, counterpartyVersions, delayPeriodTime, delayPeriodBlocks}

    versionsIntersection = intersection(counterpartyVersions, getCompatibleVersions())
    version = pickVersion(versionsIntersection) // aborts transaction if there is no intersection

    connection = ConnectionEnd{TRYOPEN, counterpartyConnectionIdentifier, counterpartyPrefix,
                               clientIdentifier, counterpartyClientIdentifier, [version], delayPeriodTime, delayPeriodBlocks}
    abortTransactionUnless(connection.verifyConnectionState(proofHeight, proofInit, counterpartyConnectionIdentifier, expectedConnectionEnd))
    
    provableStore.set(connectionPath(identifier), connection)
    addConnectionToClient(clientIdentifier, identifier)
}
ConnOpenAck 将链 B 对连接打开尝试的接受结果中继回链 A(这段代码在链 A 上执行)。
function connOpenAck(
  identifier: Identifier,
  clientState: ClientState, // DEPRECATED
  version: string,
  counterpartyIdentifier: Identifier,
  proofTry: CommitmentProof,
  proofClient: CommitmentProof, // DEPRECATED
  proofConsensus: CommitmentProof, // DEPRECATED
  proofHeight: Height,
  consensusHeight: Height,
  hostConsensusStateProof?: bytes, // DEPRECATED
) {
    connection = provableStore.get(connectionPath(identifier))
    abortTransactionUnless(connection !== null)
    abortTransactionUnless(connection.state === INIT && connection.versions.indexOf(version) !== -1)
    expectedConnectionEnd = ConnectionEnd{
      TRYOPEN,
      identifier,
      getCommitmentPrefix(),
      connection.counterpartyClientIdentifier,
      connection.clientIdentifier,
      [version],
      connection.delayPeriodTime,
      connection.delayPeriodBlocks
    }
    abortTransactionUnless(connection.verifyConnectionState(proofHeight, proofTry, counterpartyIdentifier, expectedConnectionEnd))
    connection.state = OPEN
    connection.versions = [version]
    connection.counterpartyConnectionIdentifier = counterpartyIdentifier
    provableStore.set(connectionPath(identifier), connection)
}
ConnOpenConfirm 向链 B 确认链 A 上的连接已打开,此后该连接会在两条链上都处于打开状态(这段代码在链 B 上执行)。
function connOpenConfirm(
  identifier: Identifier,
  proofAck: CommitmentProof,
  proofHeight: Height) {
    connection = provableStore.get(connectionPath(identifier))
    abortTransactionUnless(connection !== null)
    abortTransactionUnless(connection.state === TRYOPEN)
    expected = ConnectionEnd{OPEN, identifier, getCommitmentPrefix(), connection.counterpartyClientIdentifier,
                             connection.clientIdentifier, connection.versions, connection.delayPeriodTime, connection.delayPeriodBlocks}
    abortTransactionUnless(connection.verifyConnectionState(proofHeight, proofAck, connection.counterpartyConnectionIdentifier, expected))
    connection.state = OPEN
    provableStore.set(connectionPath(identifier), connection)
}

查询

可以通过 queryConnection 按标识符查询连接。
function queryConnection(id: Identifier): ConnectionEnd | void {
    return provableStore.get(connectionPath(id))
}
与特定客户端关联的连接,可以通过 queryClientConnections 按客户端标识符查询。
function queryClientConnections(id: Identifier): Set<Identifier> {
    return privateStore.get(clientConnectionsPath(id))
}

属性与不变量

  • 连接标识符遵循先到先得:一旦连接完成协商,两条链之间就会存在一对唯一的标识符。
  • 连接握手不会被另一条区块链的 IBC 处理器实施中间人攻击。

向后兼容性

在最新的连接握手规范中,connOpenTry 和 connOpenAck 将不再校验对手方的客户端状态和共识状态是否是执行链共识协议的有效客户端。因此,ConnOpenTry 和 ConnOpenACk 数据报中的 clientState、proofClient、proofConsensus 和 consensusHeight 字段已被弃用,并将最终移除。

向前兼容性

此 ICS 的未来版本将在打开握手中加入版本协商。一旦连接建立并完成版本协商,后续版本更新可根据 ICS 6 进行协商。 共识状态只能按照连接建立时所选共识协议中定义的 updateConsensusState 函数允许的方式进行更新。

示例实现

历史

本文档的部分内容受到先前 IBC 规范的启发。 2019 年 3 月 29 日 - 提交初始草案版本 2019 年 5 月 17 日 - 草案定稿 2019 年 7 月 29 日 - 修订为跟踪与客户端关联的连接集合 2022 年 7 月 27 日 - 在 connOpenTry 和 connOpenAck 中新增 ClientState 校验 2024 年 7 月 23 日 - 在 connOpenTry 和 connOpenAck 中移除 ClientState 和 ConsensusState 校验。关于这些变更带来的影响,请参阅附带的图示和影响说明文档

版权

此处所有内容均依据 Apache 2.0 许可证授权。

Synopsis

This standards document describes the abstraction of an IBC connection: two stateful objects (connection ends) on two separate chains, each associated with a light client of the other chain, which together facilitate cross-chain sub-state verification and packet association (through channels). A protocol for safely establishing a connection between two chains is described.

Motivation

The core IBC protocol provides authorisation and ordering semantics for packets: guarantees, respectively, that packets have been committed on the sending blockchain (and according state transitions executed, such as escrowing tokens), and that they have been committed exactly once in a particular order and can be delivered exactly once in that same order. The connection abstraction specified in this standard, in conjunction with the client abstraction specified in ICS 2, defines the authorisation semantics of IBC. Ordering semantics are described in ICS 4).

Definitions

Client-related types & functions are as defined in ICS 2. Channel and packet-related functions are as defined in ICS 4. Commitment proof related types & functions are defined in ICS 23 Identifier and other host state machine requirements are as defined in ICS 24. The identifier is not necessarily intended to be a human-readable name (and likely should not be, to discourage squatting or racing for identifiers). The opening handshake protocol allows each chain to verify the identifier used to reference the connection on the other chain, enabling modules on each chain to reason about the reference on the other chain. An actor, as referred to in this specification, is an entity capable of executing datagrams who is paying for computation / storage (via gas or a similar mechanism) but is otherwise untrusted. Possible actors include:
  • End users signing with an account key
  • On-chain smart contracts acting autonomously or in response to another transaction
  • On-chain modules acting in response to another transaction or in a scheduled manner

Desired Properties

  • Implementing blockchains should be able to safely allow untrusted actors to open and update connections.

Pre-Establishment

Prior to connection establishment:
  • No further IBC sub-protocols should operate, since cross-chain sub-states cannot be verified.
  • The initiating actor (who creates the connection) must be able to specify an initial consensus state for the chain to connect to and an initial consensus state for the connecting chain (implicitly, e.g. by sending the transaction).

During Handshake

Once a negotiation handshake has begun:
  • Only the appropriate handshake datagrams can be executed in order.
  • No third chain can masquerade as one of the two handshaking chains

Post-Establishment

Once a negotiation handshake has completed:
  • The created connection objects on both chains contain the consensus states specified by the initiating actor.
  • No other connection objects can be maliciously created on other chains by replaying datagrams.

Technical Specification

Data Structures

This ICS defines the ConnectionState and ConnectionEnd types:
enum ConnectionState {
  INIT,
  TRYOPEN,
  OPEN,
}
interface ConnectionEnd {
  state: ConnectionState
  counterpartyConnectionIdentifier: Identifier
  counterpartyPrefix: CommitmentPrefix
  clientIdentifier: Identifier
  counterpartyClientIdentifier: Identifier
  version: string | []string
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
}
  • The state field describes the current state of the connection end.
  • The counterpartyConnectionIdentifier field identifies the connection end on the counterparty chain associated with this connection.
  • The counterpartyPrefix field contains the prefix used for state verification on the counterparty chain associated with this connection. Chains should expose an endpoint to allow relayers to query the connection prefix. If not specified, a default counterpartyPrefix of "ibc" should be used.
  • The clientIdentifier field identifies the client associated with this connection.
  • The counterpartyClientIdentifier field identifies the client on the counterparty chain associated with this connection.
  • The version field is an opaque string which can be utilised to determine encodings or protocols for channels or packets utilising this connection. If not specified, a default version of "" should be used.
  • The delayPeriodTime indicates a period in time that must elapse after validation of a header before a packet, acknowledgement, proof of receipt, or timeout can be processed.
  • The delayPeriodBlocks indicates a period in blocks that must elapse after validation of a header before a packet, acknowledgement, proof of receipt, or timeout can be processed.

Store paths

Connection paths are stored under a unique identifier.
function connectionPath(id: Identifier): Path {
  return "connections/{id}"
}
A reverse mapping from clients to a set of connections (utilised to look up all connections using a client) is stored under a unique prefix per-client:
function clientConnectionsPath(clientIdentifier: Identifier): Path {
  return "clients/{clientIdentifier}/connections"
}

Helper functions

addConnectionToClient is used to add a connection identifier to the set of connections associated with a client.
function addConnectionToClient(
  clientIdentifier: Identifier,
  connectionIdentifier: Identifier) {
    conns = privateStore.get(clientConnectionsPath(clientIdentifier))
    conns.add(connectionIdentifier)
    privateStore.set(clientConnectionsPath(clientIdentifier), conns)
}
Helper functions are defined by the connection to pass the CommitmentPrefix associated with the connection to the verification function provided by the client. In the other parts of the specifications, these functions MUST be used for introspecting other chains’ state, instead of directly calling the verification functions on the client.
function verifyClientConsensusState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  clientIdentifier: Identifier,
  consensusStateHeight: Height,
  consensusState: ConsensusState
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, consensusStatePath(clientIdentifier, consensusStateHeight))
  return verifyMembership(clientState, height, 0, 0, proof, path, consensusState)
}

function verifyClientState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  clientState: ClientState
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, clientStatePath(clientIdentifier)
  return verifyMembership(clientState, height, 0, 0, proof, path, clientState)
}

function verifyConnectionState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  connectionIdentifier: Identifier,
  connectionEnd: ConnectionEnd
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, connectionPath(connectionIdentifier))
  return verifyMembership(clientState, height, 0, 0, proof, path, connectionEnd)
}

function verifyChannelState(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  channelEnd: ChannelEnd
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, channelPath(portIdentifier, channelIdentifier))
  return verifyMembership(clientState, height, 0, 0, proof, path, channelEnd)
}

function verifyPacketCommitment(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  commitmentBytes: bytes
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetCommitmentPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path, commitmentBytes)
}

function verifyPacketAcknowledgement(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  acknowledgement: bytes
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetAcknowledgementPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path, acknowledgement)
}

function verifyPacketReceiptAbsence(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetReceiptPath(portIdentifier, channelIdentifier, sequence))
  return verifyNonMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path)
}

// OPTIONAL: verifyPacketReceipt is only required to support new channel types beyond ORDERED and UNORDERED.
function verifyPacketReceipt(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  receipt: bytes
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, packetReceiptPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, connection.counterpartyPrefix, proof, portIdentifier, channelIdentifier, sequence, receipt)
}

function verifyNextSequenceRecv(
  connection: ConnectionEnd,
  height: Height,
  proof: CommitmentProof,
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  sequence: uint64,
  nextSequenceRecv: uint64
) {
  clientState = queryClientState(connection.clientIdentifier)
  path = applyPrefix(connection.counterpartyPrefix, nextSequenceRecvPath(portIdentifier, channelIdentifier, sequence))
  return verifyMembership(clientState, height, connection.delayPeriodTime, connection.delayPeriodBlocks, proof, path, nextSequenceRecv)
}

function verifyMultihopMembership(
  connection: ConnectionEnd, // the connection end corresponding to the receiving chain.
  height: Height,
  proof: MultihopProof,
  connectionHops: []Identifier,
  key: CommitmentPath,
  value: bytes
) {
  // the connectionEnd corresponding to the end of the multi-hop channel path (sending/counterparty chain).
  multihopConnectionEnd = abortTransactionUnless(getMultihopConnectionEnd(proof))
  prefix = multihopConnectionEnd.GetCounterparty().GetPrefix()
  client = queryClient(connection.clientIdentifier)
  consensusState = queryConsensusState(connection.clientIdentifier, height)

  abortTransactionUnless(client.Status() === "active")
  abortTransactionUnless(client.GetLatestHeight() >= height)

  // verify maximum delay period has passed
  expectedTimePerBlock = queryMaxExpectedTimePerBlock()
  delayPeriodTime = abortTransactionUnless(getMaximumDelayPeriod(proof, connection))
  delayPeriodBlocks = getBlockDelay(delayPeriodTime, expectedTimePerBlock)
  abortTransactionUnless(tendermint.VerifyDelayPeriodPassed(height, delayPeriodTime, delayPeriodBlocks))

  return multihop.VerifyMultihopMembership(consensusState, connectionHops, proof, prefix, key, value) // see ics-033
}

function verifyMultihopNonMembership(
  connection: ConnectionEnd, // the connection end corresponding to the receiving chain.
  height: Height,
  proof: MultihopProof,
  connectionHops: Identifier[],
  key: CommitmentPath
) {
  // the connectionEnd corresponding to the end of the multi-hop channel path (sending/counterparty chain).
  multihopConnectionEnd = abortTransactionUnless(getMultihopConnectionEnd(proof))
  prefix = multihopConnectionEnd.GetCounterparty().GetPrefix()
  client = queryClient(connection.clientIdentifier)
  consensusState = queryConsensusState(connection.clientIdentifier, height)

  abortTransactionUnless(client.Status() === "active")
  abortTransactionUnless(client.GetLatestHeight() >= height)

  // verify maximum delay period has passed
  expectedTimePerBlock = queryMaxExpectedTimePerBlock()
  delayPeriodTime = abortTransactionUnless(getMaximumDelayPeriod(proof, connection))
  delayPeriodBlocks = getBlockDelay(delayPeriodTime, expectedTimePerBlock)
  abortTransactionUnless(tendermint.VerifyDelayPeriodPassed(height, delayPeriodTime, delayPeriodBlocks))

  return multihop.VerifyMultihopNonMembership(consensusState, connectionHops, proof, prefix, key) // see ics-033
}

// Return the maximum expected time per block from the paramstore.
// See 03-connection - GetMaxExpectedTimePerBlock.
function queryMaxExpectedTimePerBlock(): uint64

function getTimestampAtHeight(
  connection: ConnectionEnd,
  height: Height
) {
  return queryConsensusState(connection.clientIdentifier, height).getTimestamp()
}

// Return the connectionEnd corresponding to the source chain.
function getMultihopConnectionEnd(proof: MultihopProof): ConnectionEnd {
  return abortTransactionUnless(Unmarshal(proof.ConnectionProofs[proof.ConnectionProofs.length - 1].Value))
}

// Return the maximum delay period in seconds across all connections in the channel path.
function getMaximumDelayPeriod(proof: MultihopProof, lastConnection: ConnectionEnd): number {
  delayPeriodTime = lastConnection.GetDelayPeriod()
  for connData in range proofs.ConnectionProofs {
    connectionEnd = abortTransactionUnless(Unmarshal(connData.Value))
    if (connectionEnd.DelayPeriod > delayPeriodTime) {
      delayPeriodTime = connectionEnd.DelayPeriod
    }
  }
  return delayPeriodTime
}

Sub-protocols

This ICS defines the opening handshake subprotocol. Once opened, connections cannot be closed and identifiers cannot be reallocated (this prevents packet replay or authorisation confusion). Header tracking and misbehaviour detection are defined in ICS 2. State Machine Diagram

Identifier validation

Connections are stored under a unique Identifier prefix. The validation function validateConnectionIdentifier MAY be provided.
type validateConnectionIdentifier = (id: Identifier) => boolean
If not provided, the default validateConnectionIdentifier function will always return true.

Versioning

During the handshake process, two ends of a connection come to agreement on a version associated with that connection. This Version datatype is defined as:
interface Version {
  identifier: string
  features: [string]
}
The identifier field specifies a unique version identifier. A value of "1" specifies IBC 1.0.0. The features field specifies a list of features compatible with the specified identifier. The values "ORDER_UNORDERED" and "ORDER_ORDERED" specify unordered and ordered channels, respectively. Host state machine MUST utilise the version data to negotiate encodings, priorities, or connection-specific metadata related to custom logic on top of IBC. It is assumed that the two chains running the opening handshake have at least one compatible version in common (i.e., the compatible versions of the two chains must have a non-empty intersection). If the two chains do not have any mutually acceptable versions, the handshake will fail. An implementation MUST define a function getCompatibleVersions which returns the list of versions it supports, ranked by descending preference order.
type getCompatibleVersions = () => [Version]
An implementation MUST define a function pickVersion to choose a version from a list of versions.
type pickVersion = ([Version]) => Version

Opening Handshake

The opening handshake sub-protocol serves to initialise consensus states for two chains on each other. The opening handshake defines four datagrams: ConnOpenInit, ConnOpenTry, ConnOpenAck, and ConnOpenConfirm. A correct protocol execution flows as follows (note that all calls are made through modules per ICS 25):
InitiatorDatagramChain acted uponPrior state (A, B)Posterior state (A, B)
ActorConnOpenInitA(none, none)(INIT, none)
RelayerConnOpenTryB(INIT, none)(INIT, TRYOPEN)
RelayerConnOpenAckA(INIT, TRYOPEN)(OPEN, TRYOPEN)
RelayerConnOpenConfirmB(OPEN, TRYOPEN)(OPEN, OPEN)
At the end of an opening handshake between two chains implementing the sub-protocol, the following properties hold:
  • Each chain has each other’s correct consensus state as originally specified by the initiating actor.
  • Each chain has knowledge of and has agreed to its identifier on the other chain.
This sub-protocol need not be permissioned, modulo anti-spam measures. Chains MUST implement a function generateIdentifier which chooses an identifier, e.g. by incrementing a counter:
type generateIdentifier = () -> Identifier
A specific version can optionally be passed as version to ensure that the handshake will either complete with that version or fail. ConnOpenInit initialises a connection attempt on chain A.
function connOpenInit(
  counterpartyPrefix: CommitmentPrefix,
  clientIdentifier: Identifier,
  counterpartyClientIdentifier: Identifier,
  version: string,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64) {
    // generate a new identifier
    identifier = generateIdentifier()

    abortTransactionUnless(queryClientState(clientIdentifier) !== null)
    abortTransactionUnless(provableStore.get(connectionPath(identifier)) == null)

    state = INIT
    if version != "" {
      // manually selected version must be one we can support
      abortTransactionUnless(getCompatibleVersions().indexOf(version) > -1)
      versions = [version]
    } else {
      versions = getCompatibleVersions()
    }
    connection = ConnectionEnd{state, "", counterpartyPrefix,
      clientIdentifier, counterpartyClientIdentifier, versions, delayPeriodTime, delayPeriodBlocks}
    provableStore.set(connectionPath(identifier), connection)
    addConnectionToClient(clientIdentifier, identifier)
}
ConnOpenTry relays notice of a connection attempt on chain A to chain B (this code is executed on chain B).
function 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,
  hostConsensusStateProof?: bytes, // DEPRECATED
) {
    // generate a new identifier
    identifier = generateIdentifier()

    abortTransactionUnless(queryClientState(clientIdentifier) !== null)
    expectedConnectionEnd = ConnectionEnd{INIT, "", getCommitmentPrefix(), counterpartyClientIdentifier,
                             clientIdentifier, counterpartyVersions, delayPeriodTime, delayPeriodBlocks}

    versionsIntersection = intersection(counterpartyVersions, getCompatibleVersions())
    version = pickVersion(versionsIntersection) // aborts transaction if there is no intersection

    connection = ConnectionEnd{TRYOPEN, counterpartyConnectionIdentifier, counterpartyPrefix,
                               clientIdentifier, counterpartyClientIdentifier, [version], delayPeriodTime, delayPeriodBlocks}
    abortTransactionUnless(connection.verifyConnectionState(proofHeight, proofInit, counterpartyConnectionIdentifier, expectedConnectionEnd))
    
    provableStore.set(connectionPath(identifier), connection)
    addConnectionToClient(clientIdentifier, identifier)
}
ConnOpenAck relays acceptance of a connection open attempt from chain B back to chain A (this code is executed on chain A).
function connOpenAck(
  identifier: Identifier,
  clientState: ClientState, // DEPRECATED
  version: string,
  counterpartyIdentifier: Identifier,
  proofTry: CommitmentProof,
  proofClient: CommitmentProof, // DEPRECATED
  proofConsensus: CommitmentProof, // DEPRECATED
  proofHeight: Height,
  consensusHeight: Height,
  hostConsensusStateProof?: bytes, // DEPRECATED
) {
    connection = provableStore.get(connectionPath(identifier))
    abortTransactionUnless(connection !== null)
    abortTransactionUnless(connection.state === INIT && connection.versions.indexOf(version) !== -1)
    expectedConnectionEnd = ConnectionEnd{
      TRYOPEN,
      identifier,
      getCommitmentPrefix(),
      connection.counterpartyClientIdentifier,
      connection.clientIdentifier,
      [version],
      connection.delayPeriodTime,
      connection.delayPeriodBlocks
    }
    abortTransactionUnless(connection.verifyConnectionState(proofHeight, proofTry, counterpartyIdentifier, expectedConnectionEnd))
    connection.state = OPEN
    connection.versions = [version]
    connection.counterpartyConnectionIdentifier = counterpartyIdentifier
    provableStore.set(connectionPath(identifier), connection)
}
ConnOpenConfirm confirms opening of a connection on chain A to chain B, after which the connection is open on both chains (this code is executed on chain B).
function connOpenConfirm(
  identifier: Identifier,
  proofAck: CommitmentProof,
  proofHeight: Height) {
    connection = provableStore.get(connectionPath(identifier))
    abortTransactionUnless(connection !== null)
    abortTransactionUnless(connection.state === TRYOPEN)
    expected = ConnectionEnd{OPEN, identifier, getCommitmentPrefix(), connection.counterpartyClientIdentifier,
                             connection.clientIdentifier, connection.versions, connection.delayPeriodTime, connection.delayPeriodBlocks}
    abortTransactionUnless(connection.verifyConnectionState(proofHeight, proofAck, connection.counterpartyConnectionIdentifier, expected))
    connection.state = OPEN
    provableStore.set(connectionPath(identifier), connection)
}

Querying

Connections can be queried by identifier with queryConnection.
function queryConnection(id: Identifier): ConnectionEnd | void {
    return provableStore.get(connectionPath(id))
}
Connections associated with a particular client can be queried by client identifier with queryClientConnections.
function queryClientConnections(id: Identifier): Set<Identifier> {
    return privateStore.get(clientConnectionsPath(id))
}

Properties & Invariants

  • Connection identifiers are first-come-first-serve: once a connection has been negotiated, a unique identifier pair exists between two chains.
  • The connection handshake cannot be man-in-the-middled by another blockchain’s IBC handler.

Backwards Compatibility

In the latest specification of the connection handshake, connOpenTry and connOpenAck will no longer validate that the counterparty’s clien state and consensus state is a valid client of the executing chain’s consensus protocol. Thus, clientState, proofClient, proofConsensus and consensusHeight fields in the ConnOpenTry and ConnOpenACk datagrams are deprecated and will eventually be removed.

Forwards Compatibility

A future version of this ICS will include version negotiation in the opening handshake. Once a connection has been established and a version negotiated, future version updates can be negotiated per ICS 6. The consensus state can only be updated as allowed by the updateConsensusState function defined by the consensus protocol chosen when the connection is established.

Example Implementations

History

Parts of this document were inspired by the previous IBC specification. Mar 29, 2019 - Initial draft version submitted May 17, 2019 - Draft finalised Jul 29, 2019 - Revisions to track connection set associated with client Jul 27, 2022 - Addition of ClientState validation in connOpenTry and connOpenAck Jul 23, 2024 - Removal of ClientState and ConsensusState validation in connOpenTry and connOpenAck. For information on the consequences of these changes see the attached diagram and consequences document All content herein is licensed under Apache 2.0.