概述

本规范文档描述了一个用于采用 Tendermint 共识的区块链的客户端(验证算法)。

动机

使用 Tendermint 共识算法复制的各类状态机,可能希望通过 IBC 与其他复制状态机或单机状态机进行交互。

定义

函数和术语的定义见 ICS 2。 currentTimestamp 的定义见 ICS 24。 Tendermint 轻客户端使用 ICS 23 中定义的广义 Merkle 证明格式。 hash 是一种通用的抗碰撞哈希函数,并且可以方便地进行配置。

期望性质

本规范必须满足 ICS 2 中定义的客户端接口。

关于“本会被欺骗”逻辑的说明

“本会被欺骗”检测的基本思想是:它使我们能够采取更保守的策略。当我们知道网络中其他某处、采用略有不同更新模式的另一轻客户端本可能被欺骗时,即使我们自己没有被欺骗,也可以冻结本轻客户端。 考虑一个由三条链 A、B 和 C 组成的拓扑,以及两条针对链 A 的客户端 A_1 和 A_2,它们分别运行在链 B 和 C 上。会发生如下事件序列:
  • 链 A 在高度 h_0 生成一个区块(正确)。
  • 客户端 A_1 和 A_2 都被更新到高度 h_0 的区块。
  • 链 A 在高度 h_0 + n 生成一个区块(正确)。
  • 客户端 A_1 被更新到高度 h_0 + n 的区块(客户端 A_2 尚未更新)。
  • 链 A 在高度 h_0 + k 生成第二个区块(即存在双重签名/自相矛盾的区块),其中 k <= n。
如果没有“本会被欺骗”逻辑,就可以冻结客户端 A_2(因为在高度 h_0 + k 处存在两个有效区块,且它们都比 A_2 已知的最新标头更新),但无法冻结 A_1,因为 A_1 已经推进到了 h_0 + k 之后。 可以说,这是不利的,因为 A_1 只是“运气好”,恰好在 A_2 尚未更新时完成了更新,而显然已经发生了某种拜占庭故障,这种情况很可能应当由人工或治理系统介入处理。“本会被欺骗”的思路,就是允许 A_1 从一个可配置的过去标头开始检测不当行为(因此在这个例子中,A_1 可以从 h_0 开始,也会被冻结)。 这里有一个自由参数,即 A_1 愿意回溯多远(当 A_1 已更新到 h_0 + n 时,n 可以大到什么程度,而 A_1 仍愿意去查找 h_0)?与此同时,还存在一个相反方向的顾虑:在解绑期过去之后,默认认为双重签名不再有成本,因此我们不希望为 IBC 客户端引入拒绝服务攻击向量。 因此,必要条件是:A_1 应当愿意回查到它仍然存储着的旧标头,但同时也应当对该不当行为执行“解绑期”检查;如果该不当行为相对于客户端本地时间已经早于解绑期,则应避免冻结客户端。如果担心时钟偏差,可以加入一个小的时间裕量。

技术规范

本规范依赖于 Tendermint 共识算法 和 轻客户端算法 的正确实例化。

客户端状态

Tendermint 客户端状态会跟踪当前修订版本、当前验证者集合、信任期、解绑期、最新高度、最新时间戳(区块时间),以及可能存在的冻结高度。
interface ClientState {
  chainID: string
  trustLevel: Rational
  trustingPeriod: uint64
  unbondingPeriod: uint64
  latestHeight: Height
  frozenHeight: Maybe<uint64>
  upgradePath: []string
  maxClockDrift: uint64
  proofSpecs: []ProofSpec
}

共识状态

Tendermint 客户端会为所有先前已验证的共识状态跟踪时间戳(区块时间)、下一验证者集合的哈希,以及承诺根(这些状态可以在解绑期过后被清理,但在此之前不应被清理)。
interface ConsensusState {
  timestamp: uint64
  nextValidatorsHash: []byte
  commitmentRoot: []byte
}

高度

Tendermint 客户端的高度由两个 uint64 组成:修订号,以及该修订中的高度。
interface Height {
  revisionNumber: uint64
  revisionHeight: uint64
}
高度之间的比较实现如下:
function compare(a: TendermintHeight, b: TendermintHeight): Ord {
  if (a.revisionNumber < b.revisionNumber)
    return LT
  else if (a.revisionNumber === b.revisionNumber)
    if (a.revisionHeight < b.revisionHeight)
      return LT
    else if (a.revisionHeight === b.revisionHeight)
      return EQ
  return GT
}
这样设计是为了允许高度在重置为 0 的同时,将修订号加一,从而在零高度升级过程中保留超时语义。

标头

Tendermint 标头包含高度、时间戳、承诺根、验证者集合的哈希、下一验证者集合的哈希,以及提交该区块的验证者签名。提交给链上客户端的标头还包括完整的验证者集合,以及一个用于更新的受信任高度和验证者集合。这样可以减少链上客户端需要维护的状态量,并防止中继器更新中的竞争条件。
interface TendermintSignedHeader {
  height: uint64
  timestamp: uint64
  commitmentRoot: []byte
  validatorsHash: []byte
  nextValidatorsHash: []byte
  signatures: []Signature
}
interface Header {
  TendermintSignedHeader
  identifier: string
  validatorSet: List<Pair<Address, uint64>>
  trustedHeight: Height
  trustedValidatorSet: List<Pair<Address, uint64>>
}

// GetHeight will return the header Height in the IBC ClientHeight
// format.
// Implementations may use the revision number to increment the height
// across height-resetting upgrades. See ibc-go for an example
func (Header) GetHeight() {
  return Height{0, height}
}
Header 实现了 ClientMessage 接口。

Misbehaviour

Misbehaviour 类型用于检测不当行为,并在适用时冻结客户端,以阻止后续数据包流转。 Tendermint 客户端的 Misbehaviour 由两个处于同一高度、且都会被轻客户端视为有效的标头组成。
interface Misbehaviour {
  identifier: string
  h1: Header
  h2: Header
}
Misbehaviour 实现了 ClientMessage 接口。

客户端初始化

Tendermint 客户端初始化需要一个(主观选择的)最新共识状态,其中包括完整的验证者集合。
function initialise(
  identifier: Identifier, 
  clientState: ClientState, 
  consensusState: ConsensusState
) {
  assert(clientState.trustingPeriod < clientState.unbondingPeriod)
  assert(clientState.height > 0)
  assert(clientState.trustLevel >= 1/3 && clientState.trustLevel <= 1)

  provableStore.set("clients/{identifier}/clientState", clientState)
  provableStore.set("clients/{identifier}/consensusStates/{height}", consensusState)
}
Tendermint 客户端的 latestClientHeight 函数会返回最新存储的高度,并且每当一个新的(更近期的)标头被验证后都会更新该值。
function latestClientHeight(clientState: ClientState): Height {
  return clientState.latestHeight
}

有效性判定

Tendermint 客户端的有效性检查使用 Tendermint 规范 中描述的二分算法。如果提供的标头有效,则会更新客户端状态,并将新验证的承诺写入存储。
function verifyClientMessage(clientMsg: ClientMessage) {
  switch typeof(clientMsg) {
    case Header:
      verifyHeader(clientMsg)
    case Misbehaviour:
      verifyHeader(clientMsg.h1)
      verifyHeader(clientMsg.h2)
  }
}
验证对 Tendermint 客户端的常规更新是否有效。
function verifyHeader(header: Header) {
  clientState = provableStore.get("clients/{header.identifier}/clientState")
  // assert trusting period has not yet passed
  assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod)
  // assert header timestamp is less than trust period in the future. This should be resolved with an intermediate header.
  assert(header.timestamp - clientState.latestTimeStamp < clientState.trustingPeriod)
  // trusted height revision must be the same as header revision
  // if revisions are different, use upgrade client instead
  // trusted height must be less than header height
  assert(header.GetHeight().revisionNumber == header.trustedHeight.revisionNumber)
  assert(header.GetHeight().revisionHeight > header.trustedHeight.revisionHeight)
  // fetch the consensus state at the trusted height
  consensusState = provableStore.get("clients/{header.identifier}/consensusStates/{header.trustedHeight}")
  // assert that header's trusted validator set hashes to consensus state's validator hash
  assert(hash(header.trustedValidatorSet) == consensusState.nextValidatorsHash)

  // call the tendermint client's `verify` function
  assert(tmClient.verify(
    header.trustedValidatorSet,
    clientState.latestHeight,
    clientState.trustingPeriod,
    clientState.maxClockDrift,
    header.TendermintSignedHeader,
  ))
}

恶意行为判定

函数 checkForMisbehaviour 将检查某次更新是否包含恶意行为的证据。如果 ClientMessage 是一个头部,则通过检查存储中是否已存在冲突的共识状态,或该头部是否破坏时间单调性,来判断是否存在隐式的恶意行为证据。
function checkForMisbehaviour(clientMsg: clientMessage): boolean {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  switch typeof(clientMsg) {
    case Header:
      // fetch consensusstate at header height if it exists
      consensusState = provableStore.get("clients/{clientMsg.identifier}/consensusStates/{header.GetHeight()}")
      // if consensus state exists and conflicts with the header
      // then the header is evidence of misbehaviour
      if consensusState != nil && 
          !(
          consensusState.timestamp == header.timestamp &&
          consensusState.commitmentRoot == header.commitmentRoot &&
          consensusState.nextValidatorsHash == header.nextValidatorsHash
          ) {
        return true
      }

      // check for time monotonicity misbehaviour
      // if header is not monotonically increasing with respect to neighboring consensus states
      // then return true
      // NOTE: implementation must have ability to iterate ascending/descending by height
      prevConsState = getPreviousConsensusState(header.GetHeight())
      nextConsState = getNextConsensusState(header.GetHeight())
      if prevConsState.timestamp >= header.timestamp {
        return true
      }
      if nextConsState != nil && nextConsState.timestamp <= header.timestamp {
        return true
      }
    case Misbehaviour:
      if (misbehaviour.h1.height < misbehaviour.h2.height) {
        return false
      }
      // if heights are equal check that this is valid misbehaviour of a fork
      if (misbehaviour.h1.height === misbehaviour.h2.height && misbehaviour.h1.commitmentRoot !== misbehaviour.h2.commitmentRoot) {
        return true
      }
      // otherwise if heights are unequal check that this is valid misbehavior of BFT time violation
      if (misbehaviour.h1.timestamp <= misbehaviour.h2.timestamp) {
        return true
      }

      return false
  }
}

更新状态

函数 updateState 将对 Tendermint 客户端执行一次常规更新。它会向客户端存储中添加一个共识状态。如果该头部高于 clientState 上的最新高度,则会更新 clientState。
function updateState(clientMsg: clientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  header = Header(clientMessage)
  // only update the clientstate if the header height is higher
  // than clientState latest height
  if clientState.height < header.GetHeight() {
    // update latest height
    clientState.latestHeight = header.GetHeight()

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

  // create recorded consensus state, save it
  consensusState = ConsensusState{header.timestamp, header.nextValidatorsHash, header.commitmentRoot}
  provableStore.set("clients/{clientMsg.identifier}/consensusStates/{header.GetHeight()}", consensusState)

  // these may be stored as private metadata within the client in order to verify
  // that the delay period has passed in proof verification
  provableStore.set("clients/{clientMsg.identifier}/processedTimes/{header.GetHeight()}", currentTimestamp())
  provableStore.set("clients/{clientMsg.identifier}/processedHeights/{header.GetHeight()}", currentHeight())
}

发生恶意行为时更新状态

函数 updateStateOnMisbehaviour 会将冻结高度设置为一个非零的哨兵高度,以冻结整个客户端。
function updateStateOnMisbehaviour(clientMsg: clientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  clientState.frozenHeight = Height{0, 1}
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}

升级

该轻客户端所跟踪的链可以选择在状态中写入一个预先确定的特殊键,以便轻客户端更新其客户端状态(例如使用新的链 ID 或修订号),为升级做准备。 由于客户端状态变更会立即执行,一旦新的客户端状态信息被写入该预定键,客户端将无法继续跟随旧链上的区块,因此必须及时完成升级。
function upgradeClientState(
  clientState: ClientState,
  newClientState: ClientState,
  height: Height,
  proof: CommitmentProof
) {
  // assert trusting period has not yet passed
  assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod)
  // check that the revision has been incremented
  assert(newClientState.latestHeight.revisionNumber > clientState.latestHeight.revisionNumber)
  // check proof of updated client state in state at predetermined commitment prefix and key
  path = applyPrefix(clientState.upgradeCommitmentPrefix, clientState.upgradeKey)
  // check that the client is at a sufficient height
  assert(clientState.latestHeight >= height)
  // check that the client is unfrozen or frozen at a higher height
  assert(clientState.frozenHeight === null || clientState.frozenHeight > height)
  // fetch the previously verified commitment root & verify membership
  // Implementations may choose how to pass in the identifier
  // ibc-go provides the identifier-prefixed store to this method
  // so that all state reads are for the client in question
  consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}")
  // verify that the provided consensus state has been stored
  assert(verifyMembership(consensusState.commitmentRoot, proof, path, newClientState))
  // update client state
  clientState = newClientState
  provableStore.set("clients/{clientIdentifier}/clientState", clientState)
}

状态验证函数

Tendermint 客户端的状态验证函数会针对先前已验证的承诺根检查 Merkle 证明。 这些函数使用客户端初始化时所采用的 proofSpecs。
function verifyMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: []byte
): Error {
  // check that the client is at a sufficient height
  assert(clientState.latestHeight >= height)
  // check that the client is unfrozen or frozen at a higher height
  assert(clientState.frozenHeight === null || clientState.frozenHeight > height)
  // assert that enough time has elapsed
  assert(currentTimestamp() >= processedTime + delayPeriodTime)
  // assert that enough blocks have elapsed
  assert(currentHeight() >= processedHeight + delayPeriodBlocks)
  // fetch the previously verified commitment root & verify membership
  // Implementations may choose how to pass in the identifier
  // ibc-go provides the identifier-prefixed store to this method
  // so that all state reads are for the client in question
  consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}")
  // verify that <path, value> has been stored
  if !verifyMembership(consensusState.commitmentRoot, proof, path, value) {
    return error
  }
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath
): Error {
  // check that the client is at a sufficient height
  assert(clientState.latestHeight >= height)
  // check that the client is unfrozen or frozen at a higher height
  assert(clientState.frozenHeight === null || clientState.frozenHeight > height)
  // assert that enough time has elapsed
  assert(currentTimestamp() >= processedTime + delayPeriodTime)
  // assert that enough blocks have elapsed
  assert(currentHeight() >= processedHeight + delayPeriodBlocks)
  // fetch the previously verified commitment root & verify membership
  // Implementations may choose how to pass in the identifier
  // ibc-go provides the identifier-prefixed store to this method
  // so that all state reads are for the client in question
  consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}")
  // verify that nothing has been stored at path
  if !verifyNonMembership(consensusState.commitmentRoot, proof, path) {
    return error
  }
  return nil
}

属性与不变量

由 Tendermint 轻客户端算法提供的正确性保证。

向后兼容性

不适用。

向前兼容性

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

示例实现

历史

2019 年 12 月 10 日 - 初始版本 2019 年 12 月 19 日 - 第一版最终草稿

版权

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

Synopsis

This specification document describes a client (verification algorithm) for a blockchain using Tendermint consensus.

Motivation

State machines of various sorts replicated using the Tendermint consensus algorithm might like to interface with other replicated state machines or solo machines over IBC.

Definitions

Functions & terms are as defined in ICS 2. currentTimestamp is as defined in ICS 24. The Tendermint light client uses the generalised Merkle proof format as defined in ICS 23. hash is a generic collision-resistant hash function, and can easily be configured.

Desired Properties

This specification must satisfy the client interface defined in ICS 2.

Note on “would-have-been-fooled logic

The basic idea of “would-have-been-fooled” detection is that it allows us to be a bit more conservative, and freeze our light client when we know that another light client somewhere else on the network with a slightly different update pattern could have been fooled, even though we weren’t. Consider a topology of three chains - A, B, and C, and two clients for chain A, A_1 and A_2, running on chains B and C respectively. The following sequence of events occurs:
  • Chain A produces a block at height h_0 (correctly).
  • Clients A_1 and A_2 are updated to the block at height h_0.
  • Chain A produces a block at height h_0 + n (correctly).
  • Client A_1 is updated to the block at height h_0 + n (client A_2 is not yet updated).
  • Chain A produces a second (equivocating) block at height h_0 + k, where k <= n.
Without “would-have-been-fooled”, it will be possible to freeze client A_2 (since there are two valid blocks at height h_0 + k which are newer than the latest header A_2 knows), but it will not be possible to freeze A_1, since A_1 has already progressed beyond h_0 + k. Arguably, this is disadvantageous, since A_1 was just “lucky” in having been updated when A_2 was not, and clearly some Byzantine fault has happened that should probably be dealt with by human or governance system intervention. The idea of “would-have-been-fooled” is to allow this to be detected by having A_1 start from a configurable past header to detect misbehaviour (so in this case, A_1 would be able to start from h_0 and would also be frozen). There is a free parameter here - namely, how far back is A_1 willing to go (how big can n be where A_1 will still be willing to look up h_0, having been updated to h_0 + n)? There is also a countervailing concern, in and of that double-signing is presumed to be costless after the unbonding period has passed, and we don’t want to open up a denial-of-service vector for IBC clients. The necessary condition is thus that A_1 should be willing to look up headers as old as it has stored, but should also enforce the “unbonding period” check on the misbehaviour, and avoid freezing the client if the misbehaviour is older than the unbonding period (relative to the client’s local timestamp). If there are concerns about clock skew a slight delta could be added.

Technical Specification

This specification depends on correct instantiation of the Tendermint consensus algorithm and light client algorithm.

Client state

The Tendermint client state tracks the current revision, current validator set, trusting period, unbonding period, latest height, latest timestamp (block time), and a possible frozen height.
interface ClientState {
  chainID: string
  trustLevel: Rational
  trustingPeriod: uint64
  unbondingPeriod: uint64
  latestHeight: Height
  frozenHeight: Maybe<uint64>
  upgradePath: []string
  maxClockDrift: uint64
  proofSpecs: []ProofSpec
}

Consensus state

The Tendermint client tracks the timestamp (block time), the hash of the next validator set, and commitment root for all previously verified consensus states (these can be pruned after the unbonding period has passed, but should not be pruned beforehand).
interface ConsensusState {
  timestamp: uint64
  nextValidatorsHash: []byte
  commitmentRoot: []byte
}

Height

The height of a Tendermint client consists of two uint64s: the revision number, and the height in the revision.
interface Height {
  revisionNumber: uint64
  revisionHeight: uint64
}
Comparison between heights is implemented as follows:
function compare(a: TendermintHeight, b: TendermintHeight): Ord {
  if (a.revisionNumber < b.revisionNumber)
    return LT
  else if (a.revisionNumber === b.revisionNumber)
    if (a.revisionHeight < b.revisionHeight)
      return LT
    else if (a.revisionHeight === b.revisionHeight)
      return EQ
  return GT
}
This is designed to allow the height to reset to 0 while the revision number increases by one in order to preserve timeouts through zero-height upgrades.

Headers

The Tendermint headers include the height, the timestamp, the commitment root, the hash of the validator set, the hash of the next validator set, and the signatures by the validators who committed the block. The header submitted to the on-chain client also includes the entire validator set, and a trusted height and validator set to update from. This reduces the amount of state maintained by the on-chain client and prevents race conditions on relayer updates.
interface TendermintSignedHeader {
  height: uint64
  timestamp: uint64
  commitmentRoot: []byte
  validatorsHash: []byte
  nextValidatorsHash: []byte
  signatures: []Signature
}
interface Header {
  TendermintSignedHeader
  identifier: string
  validatorSet: List<Pair<Address, uint64>>
  trustedHeight: Height
  trustedValidatorSet: List<Pair<Address, uint64>>
}

// GetHeight will return the header Height in the IBC ClientHeight
// format.
// Implementations may use the revision number to increment the height
// across height-resetting upgrades. See ibc-go for an example
func (Header) GetHeight() {
  return Height{0, height}
}
Header implements ClientMessage interface.

Misbehaviour

The Misbehaviour type is used for detecting misbehaviour and freezing the client - to prevent further packet flow - if applicable. Tendermint client Misbehaviour consists of two headers at the same height both of which the light client would have considered valid.
interface Misbehaviour {
  identifier: string
  h1: Header
  h2: Header
}
Misbehaviour implements ClientMessage interface.

Client initialisation

Tendermint client initialisation requires a (subjectively chosen) latest consensus state, including the full validator set.
function initialise(
  identifier: Identifier, 
  clientState: ClientState, 
  consensusState: ConsensusState
) {
  assert(clientState.trustingPeriod < clientState.unbondingPeriod)
  assert(clientState.height > 0)
  assert(clientState.trustLevel >= 1/3 && clientState.trustLevel <= 1)

  provableStore.set("clients/{identifier}/clientState", clientState)
  provableStore.set("clients/{identifier}/consensusStates/{height}", consensusState)
}
The Tendermint client latestClientHeight function returns the latest stored height, which is updated every time a new (more recent) header is validated.
function latestClientHeight(clientState: ClientState): Height {
  return clientState.latestHeight
}

Validity predicate

Tendermint client validity checking uses the bisection algorithm described in the Tendermint spec. If the provided header is valid, the client state is updated & the newly verified commitment written to the store.
function verifyClientMessage(clientMsg: ClientMessage) {
  switch typeof(clientMsg) {
    case Header:
      verifyHeader(clientMsg)
    case Misbehaviour:
      verifyHeader(clientMsg.h1)
      verifyHeader(clientMsg.h2)
  }
}
Verify validity of regular update to the Tendermint client
function verifyHeader(header: Header) {
  clientState = provableStore.get("clients/{header.identifier}/clientState")
  // assert trusting period has not yet passed
  assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod)
  // assert header timestamp is less than trust period in the future. This should be resolved with an intermediate header.
  assert(header.timestamp - clientState.latestTimeStamp < clientState.trustingPeriod)
  // trusted height revision must be the same as header revision
  // if revisions are different, use upgrade client instead
  // trusted height must be less than header height
  assert(header.GetHeight().revisionNumber == header.trustedHeight.revisionNumber)
  assert(header.GetHeight().revisionHeight > header.trustedHeight.revisionHeight)
  // fetch the consensus state at the trusted height
  consensusState = provableStore.get("clients/{header.identifier}/consensusStates/{header.trustedHeight}")
  // assert that header's trusted validator set hashes to consensus state's validator hash
  assert(hash(header.trustedValidatorSet) == consensusState.nextValidatorsHash)

  // call the tendermint client's `verify` function
  assert(tmClient.verify(
    header.trustedValidatorSet,
    clientState.latestHeight,
    clientState.trustingPeriod,
    clientState.maxClockDrift,
    header.TendermintSignedHeader,
  ))
}

Misbehaviour predicate

Function checkForMisbehaviour will check if an update contains evidence of Misbehaviour. If the ClientMessage is a header we check for implicit evidence of misbehaviour by checking if there already exists a conflicting consensus state in the store or if the header breaks time monotonicity.
function checkForMisbehaviour(clientMsg: clientMessage): boolean {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  switch typeof(clientMsg) {
    case Header:
      // fetch consensusstate at header height if it exists
      consensusState = provableStore.get("clients/{clientMsg.identifier}/consensusStates/{header.GetHeight()}")
      // if consensus state exists and conflicts with the header
      // then the header is evidence of misbehaviour
      if consensusState != nil && 
          !(
          consensusState.timestamp == header.timestamp &&
          consensusState.commitmentRoot == header.commitmentRoot &&
          consensusState.nextValidatorsHash == header.nextValidatorsHash
          ) {
        return true
      }

      // check for time monotonicity misbehaviour
      // if header is not monotonically increasing with respect to neighboring consensus states
      // then return true
      // NOTE: implementation must have ability to iterate ascending/descending by height
      prevConsState = getPreviousConsensusState(header.GetHeight())
      nextConsState = getNextConsensusState(header.GetHeight())
      if prevConsState.timestamp >= header.timestamp {
        return true
      }
      if nextConsState != nil && nextConsState.timestamp <= header.timestamp {
        return true
      }
    case Misbehaviour:
      if (misbehaviour.h1.height < misbehaviour.h2.height) {
        return false
      }
      // if heights are equal check that this is valid misbehaviour of a fork
      if (misbehaviour.h1.height === misbehaviour.h2.height && misbehaviour.h1.commitmentRoot !== misbehaviour.h2.commitmentRoot) {
        return true
      }
      // otherwise if heights are unequal check that this is valid misbehavior of BFT time violation
      if (misbehaviour.h1.timestamp <= misbehaviour.h2.timestamp) {
        return true
      }

      return false
  }
}

Update state

Function updateState will perform a regular update for the Tendermint client. It will add a consensus state to the client store. If the header is higher than the latest height on the clientState, then the clientState will be updated.
function updateState(clientMsg: clientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  header = Header(clientMessage)
  // only update the clientstate if the header height is higher
  // than clientState latest height
  if clientState.height < header.GetHeight() {
    // update latest height
    clientState.latestHeight = header.GetHeight()

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

  // create recorded consensus state, save it
  consensusState = ConsensusState{header.timestamp, header.nextValidatorsHash, header.commitmentRoot}
  provableStore.set("clients/{clientMsg.identifier}/consensusStates/{header.GetHeight()}", consensusState)

  // these may be stored as private metadata within the client in order to verify
  // that the delay period has passed in proof verification
  provableStore.set("clients/{clientMsg.identifier}/processedTimes/{header.GetHeight()}", currentTimestamp())
  provableStore.set("clients/{clientMsg.identifier}/processedHeights/{header.GetHeight()}", currentHeight())
}

Update state on misbehaviour

Function updateStateOnMisbehaviour will set the frozen height to a non-zero sentinel height to freeze the entire client.
function updateStateOnMisbehaviour(clientMsg: clientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  clientState.frozenHeight = Height{0, 1}
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}

Upgrades

The chain which this light client is tracking can elect to write a special pre-determined key in state to allow the light client to update its client state (e.g. with a new chain ID or revision) in preparation for an upgrade. As the client state change will be performed immediately, once the new client state information is written to the predetermined key, the client will no longer be able to follow blocks on the old chain, so it must upgrade promptly.
function upgradeClientState(
  clientState: ClientState,
  newClientState: ClientState,
  height: Height,
  proof: CommitmentProof
) {
  // assert trusting period has not yet passed
  assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod)
  // check that the revision has been incremented
  assert(newClientState.latestHeight.revisionNumber > clientState.latestHeight.revisionNumber)
  // check proof of updated client state in state at predetermined commitment prefix and key
  path = applyPrefix(clientState.upgradeCommitmentPrefix, clientState.upgradeKey)
  // check that the client is at a sufficient height
  assert(clientState.latestHeight >= height)
  // check that the client is unfrozen or frozen at a higher height
  assert(clientState.frozenHeight === null || clientState.frozenHeight > height)
  // fetch the previously verified commitment root & verify membership
  // Implementations may choose how to pass in the identifier
  // ibc-go provides the identifier-prefixed store to this method
  // so that all state reads are for the client in question
  consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}")
  // verify that the provided consensus state has been stored
  assert(verifyMembership(consensusState.commitmentRoot, proof, path, newClientState))
  // update client state
  clientState = newClientState
  provableStore.set("clients/{clientIdentifier}/clientState", clientState)
}

State verification functions

Tendermint client state verification functions check a Merkle proof against a previously validated commitment root. These functions utilise the proofSpecs with which the client was initialised.
function verifyMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: []byte
): Error {
  // check that the client is at a sufficient height
  assert(clientState.latestHeight >= height)
  // check that the client is unfrozen or frozen at a higher height
  assert(clientState.frozenHeight === null || clientState.frozenHeight > height)
  // assert that enough time has elapsed
  assert(currentTimestamp() >= processedTime + delayPeriodTime)
  // assert that enough blocks have elapsed
  assert(currentHeight() >= processedHeight + delayPeriodBlocks)
  // fetch the previously verified commitment root & verify membership
  // Implementations may choose how to pass in the identifier
  // ibc-go provides the identifier-prefixed store to this method
  // so that all state reads are for the client in question
  consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}")
  // verify that <path, value> has been stored
  if !verifyMembership(consensusState.commitmentRoot, proof, path, value) {
    return error
  }
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  height: Height,
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath
): Error {
  // check that the client is at a sufficient height
  assert(clientState.latestHeight >= height)
  // check that the client is unfrozen or frozen at a higher height
  assert(clientState.frozenHeight === null || clientState.frozenHeight > height)
  // assert that enough time has elapsed
  assert(currentTimestamp() >= processedTime + delayPeriodTime)
  // assert that enough blocks have elapsed
  assert(currentHeight() >= processedHeight + delayPeriodBlocks)
  // fetch the previously verified commitment root & verify membership
  // Implementations may choose how to pass in the identifier
  // ibc-go provides the identifier-prefixed store to this method
  // so that all state reads are for the client in question
  consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}")
  // verify that nothing has been stored at path
  if !verifyNonMembership(consensusState.commitmentRoot, proof, path) {
    return error
  }
  return nil
}

Properties & Invariants

Correctness guarantees as provided by the Tendermint light client algorithm.

Backwards Compatibility

Not applicable.

Forwards Compatibility

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

Example Implementations

History

December 10th, 2019 - Initial version December 19th, 2019 - Final first draft All content herein is licensed under Apache 2.0.