概述

本标准规定了实现区块链间通信(IBC)协议的状态机之共识算法必须满足的属性。 这些属性是上层协议抽象进行高效且安全验证所必需的。 IBC 中用于验证远程状态机状态更新的算法称为有效性谓词。 将有效性谓词与一个受信任状态(即验证者假定为正确的状态)配对, 即可在宿主状态机上为远程状态机实现一个轻客户端(通常简称为客户端)的功能。 除状态更新验证之外,每个轻客户端还能够通过错误行为谓词检测共识错误行为。 除本规范所描述的属性之外,IBC 不对状态机及其共识算法的内部运行方式施加任何要求。 状态机可以是使用私钥签名操作的单一进程(即所谓的“单机”)、协同签名的进程仲裁组、 运行拜占庭容错共识算法(例如 Tendermint)的多个进程,或尚未发明的其他配置。 从 IBC 的视角来看,状态机完全由其轻客户端验证逻辑与错误行为检测逻辑来定义。 本标准还规定了如何注册轻客户端功能,以及 IBC 协议如何存储和更新其数据。 已存储的客户端实例可以被第三方参与者检查, 例如用户检查状态机状态并决定是否发送 IBC 数据包。

动机

在 IBC 协议中,一个参与者可以是终端用户、链下进程,或状态机上的某个模块, 其需要能够验证另一台状态机(即远程状态机)的状态更新。 这意味着只能接受由远程状态机共识算法达成一致的状态更新。 远程状态机的轻客户端,就是使该参与者能够验证该状态机状态更新的算法。 请注意,轻客户端通常不会包含对完整状态转移逻辑的验证 (因为那将等价于直接执行另一台状态机),但在特定情况下 可以选择验证部分状态转移。 本标准对轻客户端模型及其要求进行了形式化定义。 因此,只要提供满足所列要求的必要轻客户端算法, IBC 协议就可以轻松集成运行新型共识算法的新状态机。 IBC 协议也可用于与概率终局性共识算法交互。 在这种情况下,不同应用可能需要不同的有效性谓词。对于概率终局性共识,有效性谓词由终局阈值定义(例如,该阈值定义了一个区块之上还需要叠加多少个区块,才能将其视为已终局)。 因此,客户端可以充当其他客户端的阈值视图: 一个只写客户端可用于存储状态更新(但不具备验证能力), 而多个具有不同终局阈值的只读客户端(即状态更新被视为最终确定所需的确认深度) 则用于验证状态更新。 客户端协议还应支持第三方引入。 例如,若 A、B 和 C 是三个状态机,其中 Alice 是 A 上的一个模块,Bob 是 B 上的一个模块,Carol 是 C 上的一个模块,并且 Alice 同时认识 Bob 和 Carol,但 Bob 只认识 Alice 而不认识 Carol, 那么 Alice 就可以利用现有的到 Bob 的通道,传递给 Bob 一个可规范序列化的 Carol 有效性谓词。 随后 Bob 就可以使用该有效性谓词打开连接和通道, 从而使 Bob 与 Carol 能够直接通信。 如有必要,在 Bob 发起连接尝试之前,Alice 也可以将 Bob 的有效性谓词传递给 Carol, 以便 Carol 知道应当接受该传入请求。 客户端接口的构造还应确保能够安全地提供自定义验证逻辑, 以便在运行时定义自定义客户端,前提是底层状态机能够提供适当的 gas 计量机制来对计算和存储收费。例如,在支持 WASM 执行的宿主状态机上, 可以在创建客户端实例时,将有效性谓词和错误行为谓词 作为可执行的 WASM 函数提供。

定义

  • get、set、Path 和 Identifier 的定义见 ICS 24。
  • Consensus 是一种生成状态更新的算法。它接收状态机的前一状态, 以及一组消息(即状态机交易),并生成该状态机的有效状态更新。 每个状态机都必须具有一个 Consensus,它从创世状态开始,生成唯一且有序的状态更新列表。 本规范要求 Consensus 生成的状态更新 满足以下属性:
    • 每个状态更新都不得在状态更新列表中拥有多个直接后继。 换言之,状态机必须保证终局性和安全性。
    • 每个状态更新最终都必须在状态更新列表中拥有后继。 换言之,状态机必须保证活性。
    • 每个状态更新都必须有效(即状态转移有效)。 换言之,Consensus 必须是诚实的, 例如,当 Consensus 是拜占庭容错共识算法(如 Tendermint)时, 拜占庭区块生产者可以少于三分之一。
    除非状态机满足上述全部属性,否则 IBC 协议 可能无法按预期工作,例如用户资产可能被盗。请注意,特定客户端 类型可能还需要额外属性。
  • Height 指定状态机状态更新的顺序,例如序列号。 这意味着每个状态更新都会映射到一个 Height。
  • CommitmentRoot 的定义见 ICS 23。 它为上层协议抽象提供了一种高效方式,用于验证 远程状态机上是否发生了某个特定状态转移;也就是说, 它支持对远程状态机在特定 Height 下的状态中, 某一路径上的特定值是否包含或不包含进行证明。
  • ClientMessage 是由客户端类型定义的任意消息,中继者可以提交该消息以更新客户端。 ClientMessage 可以是常规更新,用于新增可供证明验证使用的共识状态;也可以包含 应导致客户端冻结的错误行为。
  • ValidityPredicate 是一个函数,用于验证中继者为了更新客户端而发送的 ClientMessage。 使用 ValidityPredicate 在计算上应当比执行 Consensus 更高效。
  • ConsensusState 是状态机在特定 Height 下状态的受信视图。 它必须包含足够的信息,以使 ValidityPredicate 能够验证状态更新, 而这些状态更新随后可用于生成新的 ConsensusState。 它必须能够以规范方式序列化,以便远程参与方(例如远程状态机) 检查某个特定 ConsensusState 是否已被某个特定状态机存储。 它还必须可被其所表示视图对应的状态机检视, 即状态机能够查找其自身在过去各个 Height 上的 ConsensusState。
  • ClientState 是客户端的状态。它必须向上层协议抽象暴露接口, 例如用于验证在特定 Height 的特定路径上某个特定值存在性的证明函数。
  • MisbehaviourPredicate 是一个函数,用于检查 Consensus 的规则是否被破坏, 若被破坏,则客户端必须被冻结,即不能再生成后续的 ConsensusState。
  • Misbehaviour 是 MisbehaviourPredicate 用于判断 是否发生了共识协议违规所需的证明。例如,在状态机 是区块链的情况下,Misbehaviour 可能由两个已签名区块头组成, 它们具有不同的 CommitmentRoot,但拥有相同的 Height。

期望属性

轻客户端必须提供状态验证函数,以安全方式 使用现有 ConsensusState 验证远程状态机的状态。 这些状态验证函数使上层协议抽象能够 验证远程状态机状态的子组件。 ValidityPredicate 必须反映远程状态机及其 Consensus 的行为,也就是说, ValidityPredicate 只能接受包含由远程状态机 Consensus 生成之状态更新的状态更新。 在出现错误行为时,ValidityPredicate 的行为可能与 远程状态机及其 Consensus 的行为不同(因为客户端并不执行远程状态机的 Consensus)。 在这种情况下,应将 Misbehaviour 提交到宿主状态机, 从而导致客户端被冻结,并需要更高层级的干预。

技术规范

本规范概述了每种客户端类型必须定义的内容。客户端类型是一组定义, 其中包括运行轻客户端所需的数据结构、初始化逻辑、有效性谓词以及错误行为谓词。 实现 IBC 协议的状态机可以支持任意数量的客户端 类型,并且每种客户端类型都可以使用不同的初始共识状态进行实例化,以跟踪 不同的共识实例。为了在两个状态机之间建立连接(见 ICS 3), 双方状态机都必须支持与对方状态机共识算法相对应的客户端类型。 具体的客户端类型将在本规范后续版本中定义,并且本仓库中将提供规范列表。 实现 IBC 协议的状态机应当遵循这些客户端类型,尽管它们也可以选择只支持其中的一个子集。

数据结构

Height

Height 是由客户端类型定义的不透明数据结构。 它必须构成一个偏序集,并提供比较操作。
type Height
enum Ord {
  LT
  EQ
  GT
}

type compare = (h1: Height, h2: Height) => Ord
一个高度相对于另一个高度只能是 LT(小于)、EQ(等于)或 GT(大于)。 在本规范其余部分中,>=、>、===、<、<= 都被定义为 compare 的别名。 高度类型还必须存在一个零元素,记作 0,并且它小于所有非零高度。

ConsensusState

ConsensusState 是由某种客户端类型定义的不透明数据结构,供有效性谓词使用,以验证新的提交和状态根。该结构很可能会包含共识过程产生的最后一次提交,包括签名和验证者集合元数据。 ConsensusState 必须从某个 Consensus 实例生成,该实例会为每个 ConsensusState 分配唯一高度 (即每个高度恰好对应一个共识状态)。如果同一条链上的两个 ConsensusState 具有相同高度,但它们的承诺根并不相等,则它们不应当共存。此类事件称为“作恶”(equivocation),并且必须被归类为不当行为。一旦发生,应生成并提交相应证明,以便冻结客户端,并在必要时使先前的状态根失效。 链的 ConsensusState 必须具有规范化序列化形式,以便其他链能够检查某个已存储的共识状态是否与另一个相同(键空间表见 ICS 24)。
type ConsensusState = bytes
ConsensusState 必须存储在下文定义的特定键下,以便其他链能够验证某个特定共识状态已被存储。 ConsensusState 必须定义一个 getTimestamp() 方法,用于返回与该共识状态关联的时间戳:
type getTimestamp = ConsensusState => uint64

ClientState

ClientState 是由某种客户端类型定义的不透明数据结构。 它可以维护任意内部状态,用于跟踪已验证的根以及历史不当行为。 轻客户端在表示层面是不透明的,不同共识算法可以定义不同的轻客户端更新算法, 但它们必须向 IBC 处理器暴露这一组通用查询函数。
type ClientState = bytes
客户端类型必须定义一个方法,使用提供的客户端标识符、客户端状态和共识状态初始化客户端状态,并按需写入内部状态。
type initialise = (identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) => Void
客户端类型必须定义一个方法,用于获取当前高度(即最近一次已验证状态更新的高度)。
type latestClientHeight = (
  clientState: ClientState)
  => Height
客户端类型必须在客户端状态上定义一个方法,用于获取给定高度对应的时间戳。
type getTimestampAtHeight = (
  clientState: ClientState,
  height: Height
) => uint64

ClientMessage

ClientMessage 是由某种客户端类型定义的不透明数据结构,用于提供更新客户端所需的信息。 可以将 ClientMessage 提交给其关联客户端,以添加新的 ConsensusState,和/或更新 ClientState。它们很可能包含高度、证明、承诺根,以及可能对有效性谓词的更新。
type ClientMessage = bytes

存储路径

客户端状态路径存储在唯一的客户端标识符之下。
function clientStatePath(id: Identifier): Path {
  return "clients/{id}/clientState"
}
共识状态路径存储在客户端标识符与高度的唯一组合之下:
function consensusStatePath(id: Identifier, height: Height): Path {
  return "clients/{id}/consensusStates/{height}"
}

有效性谓词

有效性谓词是由某种客户端类型定义的不透明函数,用于根据当前 ConsensusState 验证 ClientMessage。 与针对给定父 ClientMessage 和网络消息列表重放完整共识算法相比,使用有效性谓词在计算上应当高效得多。 有效性谓词定义如下:
type verifyClientMessage = (ClientMessage) => Void
如果提供的 ClientMessage 无效,verifyClientMessage 必须抛出异常。

不当行为谓词

不当行为谓词是由某种客户端类型定义的不透明函数,用于检查某个 ClientMessage 是否构成对共识协议的违反。例如,如果状态机是一条区块链,则这可能表现为两个已签名但状态根不同、却具有相同高度的头部,一个包含无效状态转换的已签名头部,或共识算法定义的其他恶意行为证明。 不当行为谓词定义如下
type checkForMisbehaviour = (ClientMessage) => bool
如果提供的不当行为证明无效,checkForMisbehaviour 必须抛出异常。

更新状态

函数 updateState 是由某种客户端类型定义的不透明函数,用于在收到已验证的 ClientMessage 后更新客户端。注意,该函数适用于非不当行为的 ClientMessage。
type updateState = (ClientMessage) => Void
调用此函数之前,必须先调用 verifyClientMessage,并且 checkForMisbehaviour 必须返回 false。 客户端还必须变更内部状态,以存储 现已最终确认的共识根,并更新未来调用有效性谓词所需的任何签名权限追踪信息(例如验证者集合的变更)。 客户端可以具有时间敏感的有效性谓词,也就是说,如果在一段时间内没有提供 ClientMessage (例如三周的解除质押期),则将不再可能更新客户端,即客户端会被冻结。 在这种情况下,链治理系统或受信任多重签名等有权限的实体可以被允许介入, 以解冻被冻结的客户端,并提供新的正确 ClientMessage。

在不当行为下更新状态

函数 updateStateOnMisbehaviour 是由某种客户端类型定义的不透明函数,用于在收到已验证且构成有效不当行为的 ClientMessage 时更新客户端。
type updateStateOnMisbehaviour = (ClientMessage) => Void
调用此函数之前,必须先调用 verifyClientMessage,并且 checkForMisbehaviour 必须返回 true。 客户端还必须变更内部状态,根据不当行为的性质,将先前被视为有效的相应高度标记为无效。 一旦检测到不当行为,客户端应被冻结,以便未来不能再提交任何更新。 链治理系统或受信任多重签名等有权限的实体可以被允许介入, 以解冻被冻结的客户端,并提供新的正确 ClientMessage,将客户端更新到有效状态。

CommitmentProof

CommitmentProof 是由某种客户端类型根据 ICS 23 定义的不透明数据结构。 它用于验证在某个特定最终确认高度上的状态中,某个特定键值对是否存在或不存在 (该高度必然关联到某个特定承诺根)。

状态验证

客户端类型必须定义函数,用于认证客户端所跟踪的状态机的内部状态。 具体内部实现细节可以不同(例如,回环客户端可以直接从状态中读取,因此无需任何证明)。
  • delayPeriodTime 会传递给与数据包相关证明的验证函数,以允许数据包指定一个时间周期:共识状态被添加后,必须经过该时间周期,才能将其用于与数据包相关的验证。
  • delayPeriodBlocks 会传递给与数据包相关证明的验证函数,以允许数据包指定一个区块周期:共识状态被添加后,必须经过该区块周期,才能将其用于与数据包相关的验证。
verifyMembership 是一个通用的证明验证方法,用于验证在指定高度上,给定 CommitmentPath 处某个值存在的证明。如果验证不成功,它必须返回错误。 调用方应当根据 CommitmentPrefix 与标准化路径构造完整的 CommitmentPath(定义见 ICS 24)。如果调用方希望强制执行特定延迟期, 则可以传入非零的 delayPeriodTime 或 delayPeriodBlocks。如果不需要延迟期,调用方必须为 delayPeriodTime 和 delayPeriodBlocks 传入 0, 客户端在验证时将不会强制执行任何延迟期。
type verifyMembership = (
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: bytes)
  => Error
verifyNonMembership 是一个通用的证明验证方法,用于验证在指定高度上,给定 CommitmentPath 不存在的证明。如果验证不成功,它必须返回错误。 调用方应当根据 CommitmentPrefix 与标准化路径构造完整的 CommitmentPath(定义见 ICS 24)。如果调用方希望强制执行特定延迟期, 则可以传入非零的 delayPeriodTime 或 delayPeriodBlocks。如果不需要延迟期,调用方必须为 delayPeriodTime 和 delayPeriodBlocks 传入 0, 客户端在验证时将不会强制执行任何延迟期。 由于该验证方法的设计旨在给予客户端实现完全控制,客户端可以通过验证某个非空哨兵值 ABSENCE 的存在,来支持那些不提供不存在证明的链。因此在这些特殊情况下,提供的证明将是 ICS-23 的存在性证明,而客户端将验证给定路径在给定高度下是否存储了 ABSENCE 值。
type verifyNonMembership = (
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64,
  proof: CommitmentProof,
  path: CommitmentPath)
  => Error

查询接口

链查询

假定与特定客户端关联的链上的节点会通过 HTTP 或等效的 RPC API 暴露这些查询端点。 queryUpdate 必须由被特定客户端验证的链来定义,并且应允许获取给定高度对应的 clientMessage。该端点被视为不可信。
type queryUpdate = (height: Height) => ClientMessage
queryChainConsensusState 可以由被特定客户端验证的链定义,以便获取当前共识状态,并可据此构造新客户端。 当以这种方式使用时,返回的 ConsensusState 具有主观性,因此必须由发起查询的实体手动确认。该端点被视为不可信。ConsensusState 的具体形式可能因客户端类型而异。
type queryChainConsensusState = (height: Height) => ConsensusState
请注意,按高度获取历史共识状态(而不仅仅是当前共识状态)会比较方便,但并非必需。 queryChainConsensusState 还可以返回创建客户端所需的其他数据,例如某些权益证明安全模型中的“解除质押期”。这些数据也必须由发起查询的实体进行验证。

链上状态查询

本规范定义了一个按标识符查询客户端状态的函数。
function queryClientState(identifier: Identifier): ClientState {
  return provableStore.get(clientStatePath(identifier))
}
ClientState 类型应当暴露其最新已验证高度(如果需要,随后即可通过 queryConsensusState 获取对应的共识状态)。
type latestHeight = (state: ClientState) => Height
客户端类型应当定义以下标准化查询函数,以便中继器和其他链下实体通过标准 API 与链上状态交互。 queryConsensusState 允许按高度获取已存储的共识状态。
type queryConsensusState = (
  identifier: Identifier,
  height: Height,
) => ConsensusState

证明构造

每种客户端类型都应当定义函数,使中继器能够构造客户端状态验证算法所需的证明。根据客户端类型不同,这些函数可能采取不同形式。 例如,Tendermint 客户端的证明可能会与存储查询返回的键值数据一同返回,而 solo 客户端的证明可能需要在对应的 solo 状态机上以交互方式构造(因为用户需要对消息进行签名)。 这些函数既可能包括通过 RPC 对全节点发起的外部查询,也可能包括本地计算或验证。
type queryAndProveClientConsensusState = (
  clientIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  consensusStateHeight: Height) => ConsensusState, Proof

type queryAndProveConnectionState = (
  connectionIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix) => ConnectionEnd, Proof

type queryAndProveChannelState = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix) => ChannelEnd, Proof

type queryAndProvePacketData = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  sequence: uint64) => []byte, Proof

type queryAndProvePacketAcknowledgement = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  sequence: uint64) => []byte, Proof

type queryAndProvePacketAcknowledgementAbsence = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  sequence: uint64) => Proof

type queryAndProveNextSequenceRecv = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix) => uint64, Proof

实现策略

回环
本地状态机的回环客户端只需读取本地状态,并且它必须能够访问该状态。
简单签名
对于具有已知公钥的 solo 状态机客户端,它会检查该本地状态机发送消息上的签名, 这些签名通过 Proof 参数提供。height 参数可用作防重放保护的 nonce。 多重签名或门限签名方案也可以采用这种方式使用。
代理客户端
代理客户端通过验证另一个(代理)状态机对目标状态机的验证来工作,具体做法是: 在证明中首先包含代理状态机上客户端状态的证明,然后再包含相对于代理状态机上该客户端状态的目标状态机子状态的二级证明。这样,代理客户端就可以避免自行存储和跟踪目标状态机的共识状态,代价是增加了对代理状态机正确性的安全性假设。
Merkle 化状态树
对于具有 Merkle 化状态树的状态机客户端,可以通过调用 ICS-23 的 verifyMembership 或 verifyNonMembership 方法来实现这些函数,使用存储在 ClientState 中的、已经过验证的 Merkle 根,以根据 ICS 23 验证特定高度下状态中特定键值对的存在或不存在。
type verifyMembership = (ClientState, Height, CommitmentProof, Path, Value) => boolean
type verifyNonMembership = (ClientState, Height, CommitmentProof, Path) => boolean

子协议

IBC 处理器必须实现下文定义的函数。

标识符校验

客户端存储在唯一的 Identifier 前缀下。 本 ICS 不要求客户端标识符必须以特定方式生成,只要求它们是唯一的。 不过,如果有需要,也可以限制 Identifier 的取值空间。 可以提供校验函数 validateClientIdentifier。
type validateClientIdentifier = (id: Identifier) => boolean
如果未提供,则默认的 validateClientIdentifier 将始终返回 true。
利用历史根
为了避免客户端更新(其会改变状态根)与握手或数据包接收中携带证明的交易之间出现竞态条件,许多 IBC 处理器函数允许调用者指定要引用的特定历史根,并按高度进行查找。执行此操作的 IBC 处理器函数必须确保,同时对调用者传入的高度执行任何必要检查,以保证逻辑正确性。

创建

使用客户端状态和初始共识状态调用 createClient 会创建一个新客户端。
function createClient(clientState: clientState, consensusState: ConsensusState) {
  // implementations may define a identifier generation function
  identifier = generateClientIdentifier()
  abortTransactionUnless(provableStore.get(clientStatePath(identifier)) === null)
  initialise(identifier, clientState, consensusState)
}

查询

可以按标识符查询客户端共识状态和客户端内部状态,但必须查询的具体路径由各客户端类型自行定义。

更新

更新客户端是通过提交新的 ClientMessage 来完成的。Identifier 用于指向逻辑将要更新的已存储 ClientState。当新的 ClientMessage 通过已存储 ClientState 的有效性谓词和 ConsensusState 验证后,客户端必须相应更新其内部状态,这可能包括最终确认承诺根,以及更新已存储共识状态中的签名授权逻辑。 如果客户端无法再被更新(例如信任期已过),则无法再通过与该客户端关联的连接和通道发送任何数据包,也无法对任何在途数据包执行超时处理(因为目标链上的高度和时间戳已无法再被验证)。此时必须进行人工干预,以重置客户端状态或将这些连接和通道迁移到另一个客户端。这个过程无法完全安全地自动完成,但实现 IBC 的链可以选择允许治理机制执行这些操作(甚至可以在多签或合约中按客户端、连接或通道分别执行)。
function updateClient(
  id: Identifier,
  clientMessage: ClientMessage) {
    // get clientState from store with id
    clientState = provableStore.get(clientStatePath(id))
    abortTransactionUnless(clientState !== null)

    verifyClientMessage(clientMessage)
    
    foundMisbehaviour := clientState.CheckForMisbehaviour(clientMessage)
    if foundMisbehaviour {
      updateStateOnMisbehaviour(clientMessage)
      // emit misbehaviour event
    }
    else {    
      updateState(clientMessage) // expects no-op on duplicate clientMessage
      // emit update event
    }
}

错误行为

中继器可以直接向客户端报告错误行为,这可能会使先前有效的状态根失效,并阻止未来的更新。
function submitMisbehaviourToClient(
  id: Identifier,
  clientMessage: ClientMessage) {
    clientState = provableStore.get(clientStatePath(id))
    abortTransactionUnless(clientState !== null)
    // authenticate client message
    verifyClientMessage(clientMessage)
    // check that client message is valid instance of misbehaviour
    abortTransactionUnless(clientState.checkForMisbehaviour(clientMessage))
    // update state based on misbehaviour
    updateStateOnMisbehaviour(misbehaviour)
}

属性与不变量

  • 客户端标识符是不可变的,并遵循先到先得原则。客户端不能被删除(允许删除可能会在标识符被复用时导致未来重放历史数据包)。

向后兼容性

不适用。

向前兼容性

只要符合该接口,IBC 实现即可按需添加新的客户端类型。

示例实现

如需了解如何实现自己的轻客户端,请参阅 ibc-go 中轻客户端的实现示例:(https://github.com/cosmos/ibc-go/blob/main/modules/light-clients)。

历史

2019 年 3 月 5 日 - 初始草案完成并作为 PR 提交 2019 年 5 月 29 日 - 多项修订,尤其是多个承诺根 2019 年 8 月 15 日 - 为了更清晰地说明客户端接口而进行了重大重构 2020 年 1 月 13 日 - 针对客户端类型分离和路径变更的修订 2020 年 1 月 26 日 - 新增查询接口 2022 年 7 月 27 日 - 新增 verifyClientState 函数,并将 ClientState 移至 provableStore 2022 年 8 月 4 日 - 为与 02-client-refactor ADR 中的变更保持一致,对 ClientState 接口及相关处理器进行了修改:(https://github.com/cosmos/ibc-go/pull/1871)

版权

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

Synopsis

This standard specifies the properties that consensus algorithms of state machines implementing the inter-blockchain communication (IBC) protocol are required to satisfy. These properties are necessary for efficient and safe verification in the higher-level protocol abstractions. The algorithm utilised in IBC to verify the state updates of a remote state machine is referred to as a validity predicate. Pairing a validity predicate with a trusted state (i.e., a state that the verifier assumes to be correct), implements the functionality of a light client (often shortened to client) for a remote state machine on the host state machine. In addition to state update verification, every light client is able to detect consensus misbehaviours through a misbehaviour predicate. Beyond the properties described in this specification, IBC does not impose any requirements on the internal operation of the state machines and their consensus algorithms. A state machine may consist of a single process signing operations with a private key (the so-called “solo machine”), a quorum of processes signing in unison, many processes operating a Byzantine fault-tolerant consensus algorithm (e.g., Tendermint), or other configurations yet to be invented — from the perspective of IBC, a state machine is defined entirely by its light client validation and misbehaviour detection logic. This standard also specifies how the light client’s functionality is registered and how its data is stored and updated by the IBC protocol. The stored client instances can be introspected by a third party actor, such as a user inspecting the state of the state machine and deciding whether or not to send an IBC packet.

Motivation

In the IBC protocol, an actor, which may be an end user, an off-chain process, or a module on a state machine, needs to be able to verify updates to the state of another state machine (i.e., the remote state machine). This entails accepting only the state updates that were agreed upon by the remote state machine’s consensus algorithm. A light client of the remote state machine is the algorithm that enables the actor to verify state updates of that state machine. Note that light clients will generally not include validation of the entire state transition logic (as that would be equivalent to simply executing the other state machine), but may elect to validate parts of state transitions in particular cases. This standard formalises the light client model and requirements. As a result, the IBC protocol can easily be integrated with new state machines running new consensus algorithms, as long as the necessary light client algorithms fulfilling the listed requirements are provided. The IBC protocol can be used to interact with probabilistic-finality consensus algorithms. In such cases, different validity predicates may be required by different applications. For probabilistic-finality consensus, a validity predicate is defined by a finality threshold (e.g., the threshold defines how many block needs to be on top of a block in order to consider it finalized). As a result, clients could act as thresholding views of other clients: One write-only client could be used to store state updates (without the ability to verify them), while many read-only clients with different finality thresholds (confirmation depths after which state updates are considered final) are used to verify state updates. The client protocol should also support third-party introduction. For example, if A, B, and C are three state machines, with Alice a module on A, Bob a module on B, and Carol a module on C, such that Alice knows both Bob and Carol, but Bob knows only Alice and not Carol, then Alice can utilise an existing channel to Bob to communicate the canonically-serialisable validity predicate for Carol. Bob can then use this validity predicate to open a connection and channel so that Bob and Carol can talk directly. If necessary, Alice may also communicate to Carol the validity predicate for Bob, prior to Bob’s connection attempt, so that Carol knows to accept the incoming request. Client interfaces should also be constructed so that custom validation logic can be provided safely to define a custom client at runtime, as long as the underlying state machine can provide an appropriate gas metering mechanism to charge for compute and storage. On a host state machine which supports WASM execution, for example, the validity predicate and misbehaviour predicate could be provided as executable WASM functions when the client instance is created.

Definitions

  • get, set, Path, and Identifier are as defined in ICS 24.
  • Consensus is a state update generating algorithm. It takes the previous state of a state machine together with a set of messages (i.e., state machine transactions) and generates a valid state update of the state machine. Every state machine MUST have a Consensus that generates a unique, ordered list of state updates starting from a genesis state. This specification expects that the state updates generated by Consensus satisfy the following properties:
    • Every state update MUST NOT have more than one direct successor in the list of state updates. In other words, the state machine MUST guarantee finality and safety.
    • Every state update MUST eventually have a successor in the list of state updates. In other words, the state machine MUST guarantee liveness.
    • Every state update MUST be valid (i.e., valid state transitions). In other words, Consensus MUST be honest, e.g., in the case Consensus is a Byzantine fault-tolerant consensus algorithm, such as Tendermint, less than a third of block producers MAY be Byzantine.
    Unless the state machine satisfies all of the above properties, the IBC protocol may not work as intended, e.g., users’ assets might be stolen. Note that specific client types may require additional properties.
  • Height specifies the order of the state updates of a state machine, e.g., a sequence number. This entails that each state update is mapped to a Height.
  • CommitmentRoot is as defined in ICS 23. It provides an efficient way for higher-level protocol abstractions to verify whether a particular state transition has occurred on the remote state machine, i.e., it enables proofs of inclusion or non-inclusion of particular values at particular paths in the state of the remote state machine at particular Heights.
  • ClientMessage is an arbitrary message defined by the client type that relayers can submit in order to update the client. The ClientMessage may be intended as a regular update which may add new consensus state for proof verification, or it may contain misbehaviour which should freeze the client.
  • ValidityPredicate is a function that validates a ClientMessage sent by a relayer in order to update the client. Using the ValidityPredicate SHOULD be more computationally efficient than executing Consensus.
  • ConsensusState is the trusted view of the state of a state machine at a particular Height. It MUST contain sufficient information to enable the ValidityPredicate to validate state updates, which can then be used to generate new ConsensusStates. It MUST be serialisable in a canonical fashion so that remote parties, such as remote state machines, can check whether a particular ConsensusState was stored by a particular state machine. It MUST be introspectable by the state machine whose view it represents, i.e., a state machine can look up its own ConsensusStates at past Heights.
  • ClientState is the state of a client. It MUST expose an interface to higher-level protocol abstractions, e.g., functions to verify proofs of the existence of particular values at particular paths at particular Heights.
  • MisbehaviourPredicate is a function that checks whether the rules of Consensus were broken, in which case the client MUST be frozen, i.e., no subsequent ConsensusStates can be generated.
  • Misbehaviour is the proof needed by the MisbehaviourPredicate to determine whether a violation of the consensus protocol occurred. For example, in the case the state machine is a blockchain, a Misbehaviour might consist of two signed block headers with different CommitmentRoots, but the same Height.

Desired Properties

Light clients MUST provide state verification functions that provide a secure way to verify the state of the remote state machines using the existing ConsensusStates. These state verification functions enable higher-level protocol abstractions to verify sub-components of the state of the remote state machines. ValidityPredicates MUST reflect the behaviour of the remote state machine and its Consensus, i.e., ValidityPredicates accept only state updates that contain state updates generated by the Consensus of the remote state machine. In case of misbehavior, the behaviour of the ValidityPredicate might differ from the behaviour of the remote state machine and its Consensus (since clients do not execute the Consensus of the remote state machine). In this case, a Misbehaviour SHOULD be submitted to the host state machine, which would result in the client being frozen and higher-level intervention being necessary.

Technical Specification

This specification outlines what each client type must define. A client type is a set of definitions of the data structures, initialisation logic, validity predicate, and misbehaviour predicate required to operate a light client. State machines implementing the IBC protocol can support any number of client types, and each client type can be instantiated with different initial consensus states in order to track different consensus instances. In order to establish a connection between two state machines (see ICS 3), the state machines must each support the client type corresponding to the other state machine’s consensus algorithm. Specific client types shall be defined in later versions of this specification and a canonical list shall exist in this repository. State machines implementing the IBC protocol are expected to respect these client types, although they may elect to support only a subset.

Data Structures

Height

Height is an opaque data structure defined by a client type. It must form a partially ordered set & provide operations for comparison.
type Height
enum Ord {
  LT
  EQ
  GT
}

type compare = (h1: Height, h2: Height) => Ord
A height is either LT (less than), EQ (equal to), or GT (greater than) another height. >=, >, ===, <, <= are defined through the rest of this specification as aliases to compare. There must also be a zero-element for a height type, referred to as 0, which is less than all non-zero heights.

ConsensusState

ConsensusState is an opaque data structure defined by a client type, used by the validity predicate to verify new commits & state roots. Likely the structure will contain the last commit produced by the consensus process, including signatures and validator set metadata. ConsensusState MUST be generated from an instance of Consensus, which assigns unique heights for each ConsensusState (such that each height has exactly one associated consensus state). Two ConsensusStates on the same chain SHOULD NOT have the same height if they do not have equal commitment roots. Such an event is called an “equivocation” and MUST be classified as misbehaviour. Should one occur, a proof should be generated and submitted so that the client can be frozen and previous state roots invalidated as necessary. The ConsensusState of a chain MUST have a canonical serialisation, so that other chains can check that a stored consensus state is equal to another (see ICS 24 for the keyspace table).
type ConsensusState = bytes
The ConsensusState MUST be stored under a particular key, defined below, so that other chains can verify that a particular consensus state has been stored. The ConsensusState MUST define a getTimestamp() method which returns the timestamp associated with that consensus state:
type getTimestamp = ConsensusState => uint64

ClientState

ClientState is an opaque data structure defined by a client type. It may keep arbitrary internal state to track verified roots and past misbehaviours. Light clients are representation-opaque — different consensus algorithms can define different light client update algorithms — but they must expose this common set of query functions to the IBC handler.
type ClientState = bytes
Client types MUST define a method to initialise a client state with the provided client identifier, client state and consensus state, writing to internal state as appropriate.
type initialise = (identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) => Void
Client types MUST define a method to fetch the current height (height of the most recent validated state update).
type latestClientHeight = (
  clientState: ClientState)
  => Height
Client types MUST define a method on the client state to fetch the timestamp at a given height
type getTimestampAtHeight = (
  clientState: ClientState,
  height: Height
) => uint64

ClientMessage

A ClientMessage is an opaque data structure defined by a client type which provides information to update the client. ClientMessages can be submitted to an associated client to add new ConsensusState(s) and/or update the ClientState. They likely contain a height, a proof, a commitment root, and possibly updates to the validity predicate.
type ClientMessage = bytes

Store paths

Client state paths are stored under a unique client identifier.
function clientStatePath(id: Identifier): Path {
  return "clients/{id}/clientState"
}
Consensus state paths are stored under a unique combination of client identifier and height:
function consensusStatePath(id: Identifier, height: Height): Path {
  return "clients/{id}/consensusStates/{height}"
}

Validity predicate

A validity predicate is an opaque function defined by a client type to verify ClientMessages depending on the current ConsensusState. Using the validity predicate SHOULD be far more computationally efficient than replaying the full consensus algorithm for the given parent ClientMessage and the list of network messages. The validity predicate is defined as:
type verifyClientMessage = (ClientMessage) => Void
verifyClientMessage MUST throw an exception if the provided ClientMessage was not valid.

Misbehaviour predicate

A misbehaviour predicate is an opaque function defined by a client type, used to check if a ClientMessage constitutes a violation of the consensus protocol. For example, if the state machine is a blockchain, this might be two signed headers with different state roots but the same height, a signed header containing invalid state transitions, or other proof of malfeasance as defined by the consensus algorithm. The misbehaviour predicate is defined as
type checkForMisbehaviour = (ClientMessage) => bool
checkForMisbehaviour MUST throw an exception if the provided proof of misbehaviour was not valid.

Update state

Function updateState is an opaque function defined by a client type that will update the client given a verified ClientMessage. Note that this function is intended for non-misbehaviour ClientMessages.
type updateState = (ClientMessage) => Void
verifyClientMessage must be called before this function, and checkForMisbehaviour must return false before this function is called. The client MUST also mutate internal state to store now-finalised consensus roots and update any necessary signature authority tracking (e.g. changes to the validator set) for future calls to the validity predicate. Clients MAY have time-sensitive validity predicates, such that if no ClientMessage is provided for a period of time (e.g. an unbonding period of three weeks) it will no longer be possible to update the client, i.e., the client is being frozen. In this case, a permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed to intervene to unfreeze a frozen client & provide a new correct ClientMessage.

Update state on misbehaviour

Function updateStateOnMisbehaviour is an opaque function defined by a client type that will update the client upon receiving a verified ClientMessage that is valid misbehaviour.
type updateStateOnMisbehaviour = (ClientMessage) => Void
verifyClientMessage must be called before this function, and checkForMisbehaviour must return true before this function is called. The client MUST also mutate internal state to mark appropriate heights which were previously considered valid as invalid, according to the nature of the misbehaviour. Once misbehaviour is detected, clients SHOULD be frozen so that no future updates can be submitted. A permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed to intervene to unfreeze a frozen client & provide a new correct ClientMessage which updates the client to a valid state.

CommitmentProof

CommitmentProof is an opaque data structure defined by a client type in accordance with ICS 23. It is utilised to verify presence or absence of a particular key/value pair in state at a particular finalised height (necessarily associated with a particular commitment root).

State verification

Client types must define functions to authenticate internal state of the state machine which the client tracks. Internal implementation details may differ (for example, a loopback client could simply read directly from the state and require no proofs).
  • The delayPeriodTime is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of time which must pass after a consensus state is added before it can be used for packet-related verification.
  • The delayPeriodBlocks is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of blocks which must pass after a consensus state is added before it can be used for packet-related verification.
verifyMembership is a generic proof verification method which verifies a proof of the existence of a value at a given CommitmentPath at the specified height. It MUST return an error if the verification is not successful. The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). If the caller desires a particular delay period to be enforced, then it can pass in a non-zero delayPeriodTime or delayPeriodBlocks. If a delay period is not necessary, the caller must pass in 0 for delayPeriodTime and delayPeriodBlocks, and the client will not enforce any delay period for verification.
type verifyMembership = (
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: bytes)
  => Error
verifyNonMembership is a generic proof verification method which verifies a proof of absence of a given CommitmentPath at the specified height. It MUST return an error if the verification is not successful. The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). If the caller desires a particular delay period to be enforced, then it can pass in a non-zero delayPeriodTime or delayPeriodBlocks. If a delay period is not necessary, the caller must pass in 0 for delayPeriodTime and delayPeriodBlocks, and the client will not enforce any delay period for verification. Since the verification method is designed to give complete control to client implementations, clients can support chains that do not provide absence proofs by verifying the existence of a non-empty sentinel ABSENCE value. Thus in these special cases, the proof provided will be an ICS-23 Existence proof, and the client will verify that the ABSENCE value is stored under the given path for the given height.
type verifyNonMembership = (
  clientState: ClientState,
  height: Height,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64,
  proof: CommitmentProof,
  path: CommitmentPath)
  => Error

Query interface

Chain queries

These query endpoints are assumed to be exposed over HTTP or an equivalent RPC API by nodes of the chain associated with a particular client. queryUpdate MUST be defined by the chain which is validated by a particular client, and should allow for retrieval of clientMessage for a given height. This endpoint is assumed to be untrusted.
type queryUpdate = (height: Height) => ClientMessage
queryChainConsensusState MAY be defined by the chain which is validated by a particular client, to allow for the retrieval of the current consensus state which can be used to construct a new client. When used in this fashion, the returned ConsensusState MUST be manually confirmed by the querying entity, since it is subjective. This endpoint is assumed to be untrusted. The precise nature of the ConsensusState may vary per client type.
type queryChainConsensusState = (height: Height) => ConsensusState
Note that retrieval of past consensus states by height (as opposed to just the current consensus state) is convenient but not required. queryChainConsensusState MAY also return other data necessary to create clients, such as the “unbonding period” for certain proof-of-stake security models. This data MUST also be verified by the querying entity.

On-chain state queries

This specification defines a single function to query the state of a client by-identifier.
function queryClientState(identifier: Identifier): ClientState {
  return provableStore.get(clientStatePath(identifier))
}
The ClientState type SHOULD expose its latest verified height (from which the consensus state can then be retrieved using queryConsensusState if desired).
type latestHeight = (state: ClientState) => Height
Client types SHOULD define the following standardised query functions in order to allow relayers & other off-chain entities to interface with on-chain state in a standard API. queryConsensusState allows stored consensus states to be retrieved by height.
type queryConsensusState = (
  identifier: Identifier,
  height: Height,
) => ConsensusState

Proof construction

Each client type SHOULD define functions to allow relayers to construct the proofs required by the client’s state verification algorithms. These may take different forms depending on the client type. For example, Tendermint client proofs may be returned along with key-value data from store queries, and solo client proofs may need to be constructed interactively on the solo state machine in question (since the user will need to sign the message). These functions may constitute external queries over RPC to a full node as well as local computation or verification.
type queryAndProveClientConsensusState = (
  clientIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  consensusStateHeight: Height) => ConsensusState, Proof

type queryAndProveConnectionState = (
  connectionIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix) => ConnectionEnd, Proof

type queryAndProveChannelState = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix) => ChannelEnd, Proof

type queryAndProvePacketData = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  sequence: uint64) => []byte, Proof

type queryAndProvePacketAcknowledgement = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  sequence: uint64) => []byte, Proof

type queryAndProvePacketAcknowledgementAbsence = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix,
  sequence: uint64) => Proof

type queryAndProveNextSequenceRecv = (
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  height: Height,
  prefix: CommitmentPrefix) => uint64, Proof

Implementation strategies

Loopback
A loopback client of a local state machine merely reads from the local state, to which it must have access.
Simple signatures
A client of a solo state machine with a known public key checks signatures on messages sent by that local state machine, which are provided as the Proof parameter. The height parameter can be used as a replay protection nonce. Multi-signature or threshold signature schemes can also be used in such a fashion.
Proxy clients
Proxy clients verify another (proxy) state machine’s verification of the target state machine, by including in the proof first a proof of the client state on the proxy state machine, and then a secondary proof of the sub-state of the target state machine with respect to the client state on the proxy state machine. This allows the proxy client to avoid storing and tracking the consensus state of the target state machine itself, at the cost of adding security assumptions of proxy state machine correctness.
Merklized state trees
For clients of state machines with Merklized state trees, these functions can be implemented by calling the ICS-23 verifyMembership or verifyNonMembership methods, using a verified Merkle root stored in the ClientState, to verify presence or absence of particular key/value pairs in state at particular heights in accordance with ICS 23.
type verifyMembership = (ClientState, Height, CommitmentProof, Path, Value) => boolean
type verifyNonMembership = (ClientState, Height, CommitmentProof, Path) => boolean

Sub-protocols

IBC handlers MUST implement the functions defined below.

Identifier validation

Clients are stored under a unique Identifier prefix. This ICS does not require that client identifiers be generated in a particular manner, only that they be unique. However, it is possible to restrict the space of Identifiers if required. The validation function validateClientIdentifier MAY be provided.
type validateClientIdentifier = (id: Identifier) => boolean
If not provided, the default validateClientIdentifier will always return true.
Utilising past roots
To avoid race conditions between client updates (which change the state root) and proof-carrying transactions in handshakes or packet receipt, many IBC handler functions allow the caller to specify a particular past root to reference, which is looked up by height. IBC handler functions which do this must ensure that they also perform any requisite checks on the height passed in by the caller to ensure logical correctness.

Create

Calling createClient with the client state and initial consensus state creates a new client.
function createClient(clientState: clientState, consensusState: ConsensusState) {
  // implementations may define a identifier generation function
  identifier = generateClientIdentifier()
  abortTransactionUnless(provableStore.get(clientStatePath(identifier)) === null)
  initialise(identifier, clientState, consensusState)
}

Query

Client consensus state and client internal state can be queried by identifier, but the specific paths which must be queried are defined by each client type.

Update

Updating a client is done by submitting a new ClientMessage. The Identifier is used to point to the stored ClientState that the logic will update. When a new ClientMessage is verified with the stored ClientState’s validity predicate and ConsensusState, the client MUST update its internal state accordingly, possibly finalising commitment roots and updating the signature authority logic in the stored consensus state. If a client can no longer be updated (if, for example, the trusting period has passed), it will no longer be possible to send any packets over connections & channels associated with that client, or timeout any packets in-flight (since the height & timestamp on the destination chain can no longer be verified). Manual intervention must take place to reset the client state or migrate the connections & channels to another client. This cannot safely be done completely automatically, but chains implementing IBC could elect to allow governance mechanisms to perform these actions (perhaps even per-client/connection/channel in a multi-sig or contract).
function updateClient(
  id: Identifier,
  clientMessage: ClientMessage) {
    // get clientState from store with id
    clientState = provableStore.get(clientStatePath(id))
    abortTransactionUnless(clientState !== null)

    verifyClientMessage(clientMessage)
    
    foundMisbehaviour := clientState.CheckForMisbehaviour(clientMessage)
    if foundMisbehaviour {
      updateStateOnMisbehaviour(clientMessage)
      // emit misbehaviour event
    }
    else {    
      updateState(clientMessage) // expects no-op on duplicate clientMessage
      // emit update event
    }
}

Misbehaviour

A relayer may alert the client to the misbehaviour directly, possibly invalidating previously valid state roots & preventing future updates.
function submitMisbehaviourToClient(
  id: Identifier,
  clientMessage: ClientMessage) {
    clientState = provableStore.get(clientStatePath(id))
    abortTransactionUnless(clientState !== null)
    // authenticate client message
    verifyClientMessage(clientMessage)
    // check that client message is valid instance of misbehaviour
    abortTransactionUnless(clientState.checkForMisbehaviour(clientMessage))
    // update state based on misbehaviour
    updateStateOnMisbehaviour(misbehaviour)
}

Properties & Invariants

  • Client identifiers are immutable & first-come-first-serve. Clients cannot be deleted (allowing deletion would potentially allow future replay of past packets if identifiers were re-used).

Backwards Compatibility

Not applicable.

Forwards Compatibility

New client types can be added by IBC implementations at-will as long as they conform to this interface.

Example Implementations

Please see the ibc-go implementations of light clients for examples of how to implement your own: (https://github.com/cosmos/ibc-go/blob/main/modules/light-clients).

History

Mar 5, 2019 - Initial draft finished and submitted as a PR May 29, 2019 - Various revisions, notably multiple commitment-roots Aug 15, 2019 - Major rework for clarity around client interface Jan 13, 2020 - Revisions for client type separation & path alterations Jan 26, 2020 - Addition of query interface Jul 27, 2022 - Addition of verifyClientState function, and move ClientState to the provableStore August 4, 2022 - Changes to ClientState interface and associated handler to align with changes in 02-client-refactor ADR: (https://github.com/cosmos/ibc-go/pull/1871) All content herein is licensed under Apache 2.0.