概要

本规范文档描述了一种用于单机的客户端(验证算法),该单机具有一个可更新的公钥,并实现了 ICS 2 接口。

动机

单机,例如手机、浏览器或笔记本电脑等设备,可能希望与其他支持 IBC 的机器和复制账本进行交互,而它们可以通过统一的客户端接口来实现这一点。 单机客户端大致类似于“隐式账户”,可用来替代账本上的“常规交易”,从而使所有交易都通过 IBC 的统一接口进行。

定义

函数与术语的定义见 ICS 2。 getCommitmentPrefix 的定义见 ICS 24。 removePrefix 的定义见 ICS 23。

期望属性

本规范必须满足 ICS 2 中定义的客户端接口。 在概念上,我们假设存在一个“宇宙中的签名大表”——即产生的签名是公开的——并据此纳入重放保护。

技术规范

本规范包含了 ICS 2 定义的全部函数的实现。

客户端状态

单机的 ClientState 由序列号以及一个表示客户端是否被冻结的布尔值组成。
interface ClientState {
  sequence: uint64
  frozen: boolean
  consensusState: ConsensusState
}

共识状态

单机的 ConsensusState 由当前公钥、当前区分符和时间戳组成。 区分符是一个任意字符串,在创建客户端时选定,用于允许同一个公钥在不同的单机客户端之间重复使用(可能位于不同链上),而不被视为恶意行为。
interface ConsensusState {
  publicKey: PublicKey
  diversifier: string
  timestamp: uint64
}

高度

单机的 Height 只是一个 uint64,并使用通常的比较操作。

头部

只有当单机希望更新公钥或区分符时,才应提供 Header。
interface Header {
  sequence: uint64 // deprecated
  timestamp: uint64
  signature: Signature
  newPublicKey: PublicKey
  newDiversifier: string
}
Header 实现了 ClientMessage 接口。

签名验证

单机公钥必须对以下结构体进行签名:
interface SignBytes {
  sequence: uint64
  timestamp: uint64  
  diversifier: string
  path: []byte
  data: []byte
}

恶意行为

单机的 Misbehaviour 由一个序列号以及该序列号下针对不同消息的两个签名组成。
interface SignatureAndData {
  sig: Signature
  path: []byte
  data: []byte
  timestamp: Timestamp
}

interface Misbehaviour {
  sequence: uint64
  signatureOne: SignatureAndData
  signatureTwo: SignatureAndData
}
Misbehaviour 实现了 ClientMessage 接口。

签名

签名通过客户端状态验证函数中的 Proof 字段提供。它们包含数据和时间戳,这两者也必须被签名覆盖。
interface Signature {
  data: []byte
  timestamp: uint64
}

客户端初始化

单机客户端的 initialise 函数会使用初始共识状态启动一个未冻结的客户端。
function initialise(identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) {
  assert(clientState.consensusState === consensusState)

  provableStore.set("clients/{identifier}/clientState", clientState)
  provableStore.set("clients/{identifier}/consensusStates/{height}", consensusState)
}
单机客户端的 latestClientHeight 函数返回最新序列号。
function latestClientHeight(clientState: ClientState): uint64 {
  return clientState.sequence
}

有效性判定

单机客户端的 verifyClientMessage 函数会检查:当前已注册的公钥是否在期望的序列号上对客户端消息进行了签名,并且客户端消息中包含当前区分符。如果客户端消息是更新,则它必须使用当前序列号;如果客户端消息是恶意行为,则它必须使用该恶意行为对应的序列号。
function verifyClientMessage(clientMsg: ClientMessage) {
  switch typeof(ClientMessage) {
    case Header:
      verifyHeader(clientMessage)
    // misbehaviour only supported for current public key and diversifier on solomachine
    case Misbehaviour:
      verifyMisbehaviour(clientMessage)
  }
}

function verifyHeader(header: header) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  assert(header.timestamp >= clientstate.consensusState.timestamp)
  headerData = {
    newPubKey: header.newPubKey,
    newDiversifier: header.newDiversifier,
  }
  signBytes = SignBytes(
    sequence: clientState.sequence,
    timestamp: header.timestamp,
    diversifier: clientState.consensusState.diversifier,
    path: []byte{"solomachine:header"},
    value: marshal(headerData)
  )
  assert(checkSignature(cs.consensusState.publicKey, signBytes, header.signature))
}

function verifyMisbehaviour(misbehaviour: Misbehaviour) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  s1 = misbehaviour.signatureOne
  s2 = misbehaviour.signatureTwo
  pubkey = clientState.consensusState.publicKey
  diversifier = clientState.consensusState.diversifier
  // assert that the signatures validate and that they are different
  sigBytes1 = SignBytes(
    sequence: misbehaviour.sequence,
    timestamp: s1.timestamp,
    diversifier: diversifier,
    path: s1.path,
    data: s1.data
  )
  sigBytes2 = SignBytes(
    sequence: misbehaviour.sequence,
    timestamp: s2.timestamp,
    diversifier: diversifier,
    path: s2.path,
    data: s2.data
  )
  // either the path or data must be different in order for the misbehaviour to be valid
  assert(s1.path != s2.path || s1.data != s2.data)
  assert(checkSignature(pubkey, sigBytes1, misbehaviour.signatureOne.signature))
  assert(checkSignature(pubkey, sigBytes2, misbehaviour.signatureTwo.signature))
}

恶意行为判定

由于恶意行为会在 verifyClientMessage 中进行检查,因此如果客户端消息的类型为 Misbehaviour,则返回 true:
function checkForMisbehaviour(clientMessage: ClientMessage): bool {
  switch typeof(ClientMessage) {
  case Misbehaviour:
    return true
  }
  return false
}

更新函数

函数 updateState 使用提供的客户端消息头部来更新单机的 ConsensusState 值:
function updateState(clientMessage: ClientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  header = Header(clientMessage)
  clientState.consensusState.publicKey = header.newPubKey
  clientState.consensusState.diversifier = header.newDiversifier
  clientState.consensusState.timestamp = header.timestamp
  clientState.sequence++
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}
函数 updateStateOnMisbehaviour 在接收到有效恶意行为后更新状态:
function updateStateOnMisbehaviour(clientMessage: ClientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  // freeze the client
  clientState.frozen = true
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}

状态验证函数

所有单机客户端状态验证函数都只是检查一个签名,而该签名必须由单机提供。
function verifyMembership(
  clientState: ClientState,
  // provided height is unnecessary for solomachine
  // since clientState maintains the expected sequence
  height: uint64,
  // delayPeriod is unsupported on solomachines
  // thus these fields are ignored
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: []byte
): Error {
  // the expected sequence used in the signature
  abortTransactionUnless(!clientState.frozen)
  abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp)

  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since solomachines do not use multi-stores, the prefix needs 
  // to be removed from the path to retrieve the correct key in the
  // solomachine store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)
  signBytes = SignBytes(
    sequence: clientState.sequence,
    timestamp: proof.timestamp,
    diversifier: clientState.consensusState.diversifier,
    path: unprefixedPath,
    data: value,
  )
  proven = checkSignature(clientState.consensusState.publicKey, signBytes, proof.sig)
  if !proven {
    return error
  }

  // increment sequence on each verification to provide
  // replay protection
  clientState.sequence++
  clientState.consensusState.timestamp = proof.timestamp
  // unlike other clients, we must set the client state here because we
  // mutate the clientState (increment sequence and set timestamp)
  // thus the verification methods are stateful for the solomachine
  // in order to prevent replay attacks
  provableStore.set("clients/{identifier}/clientState", clientState)
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  // provided height is unnecessary for solomachine
  // since clientState maintains the expected sequence
  height: uint64,
  // delayPeriod is unsupported on solomachines
  // thus these fields are ignored
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath
): Error {
  abortTransactionUnless(!clientState.frozen)
  abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp)

  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since solomachines do not use multi-stores, the prefix needs 
  // to be removed from the path to retrieve the correct key in the
  // solomachine store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)
  signBytes = SignBytes(
    sequence: clientState.sequence,
    timestamp: proof.timestamp,
    diversifier: clientState.consensusState.diversifier,
    path: unprefixedPath,
    data: nil,
  )
  proven = checkSignature(clientState.consensusState.publicKey, signBytes, proof.sig)
  if !proven {
    return error
  }

  // increment sequence on each verification to provide
  // replay protection
  clientState.sequence++
  clientState.consensusState.timestamp = proof.timestamp
  // unlike other clients, we must set the client state here because we
  // mutate the clientState (increment sequence and set timestamp)
  // thus the verification methods are stateful for the solomachine
  // in order to prevent replay attacks
  provableStore.set("clients/{identifier}/clientState", clientState)
  return nil
}

属性与不变量

实例化了 ICS 2 中定义的接口。

向后兼容性

不适用。

向前兼容性

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

示例实现

历史

2019 年 12 月 9 日 - 初始版本 2019 年 12 月 17 日 - 第一版最终草稿 2022 年 8 月 15 日 - 在 #813 中进行变更,以与 02-client-refactor 保持一致 2022 年 9 月 14 日 - 进行变更,以与 #4429 中的更改保持一致

版权

此处所有内容均根据 Apache 2.0 许可发布。

Synopsis

This specification document describes a client (verification algorithm) for a solo machine with a single updateable public key which implements the ICS 2 interface.

Motivation

Solo machines — which might be devices such as phones, browsers, or laptops — might like to interface with other machines & replicated ledgers which speak IBC, and they can do so through the uniform client interface. Solo machine clients are roughly analogous to “implicit accounts” and can be used in lieu of “regular transactions” on a ledger, allowing all transactions to work through the unified interface of IBC.

Definitions

Functions & terms are as defined in ICS 2. getCommitmentPrefix is as defined in ICS 24. removePrefix is as defined in ICS 23.

Desired properties

This specification must satisfy the client interface defined in ICS 2. Conceptually, we assume “big table of signatures in the universe” - that signatures produced are public - and incorporate replay protection accordingly.

Technical specification

This specification contains implementations for all of the functions defined by ICS 2.

Client state

The ClientState of a solo machine consists of the sequence number and a boolean indicating whether or not the client is frozen.
interface ClientState {
  sequence: uint64
  frozen: boolean
  consensusState: ConsensusState
}

Consensus state

The ConsensusState of a solo machine consists of the current public key, current diversifier, and timestamp. The diversifier is an arbitrary string, chosen when the client is created, designed to allow the same public key to be re-used across different solo machine clients (potentially on different chains) without being considered misbehaviour.
interface ConsensusState {
  publicKey: PublicKey
  diversifier: string
  timestamp: uint64
}

Height

The Height of a solo machine is just a uint64, with the usual comparison operations.

Headers

Headers must only be provided by a solo machine when the machine wishes to update the public key or diversifier.
interface Header {
  sequence: uint64 // deprecated
  timestamp: uint64
  signature: Signature
  newPublicKey: PublicKey
  newDiversifier: string
}
Header implements the ClientMessage interface.

Signature verification

The solomachine public key must sign over the following struct:
interface SignBytes {
  sequence: uint64
  timestamp: uint64  
  diversifier: string
  path: []byte
  data: []byte
}

Misbehaviour

Misbehaviour for solo machines consists of a sequence and two signatures over different messages at that sequence.
interface SignatureAndData {
  sig: Signature
  path: []byte
  data: []byte
  timestamp: Timestamp
}

interface Misbehaviour {
  sequence: uint64
  signatureOne: SignatureAndData
  signatureTwo: SignatureAndData
}
Misbehaviour implements the ClientMessage interface.

Signatures

Signatures are provided in the Proof field of client state verification functions. They include data & a timestamp, which must also be signed over.
interface Signature {
  data: []byte
  timestamp: uint64
}

Client initialisation

The solo machine client initialise function starts an unfrozen client with the initial consensus state.
function initialise(identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) {
  assert(clientState.consensusState === consensusState)

  provableStore.set("clients/{identifier}/clientState", clientState)
  provableStore.set("clients/{identifier}/consensusStates/{height}", consensusState)
}
The solo machine client latestClientHeight function returns the latest sequence.
function latestClientHeight(clientState: ClientState): uint64 {
  return clientState.sequence
}

Validity predicate

The solo machine client verifyClientMessage function checks that the currently registered public key signed over the client message at the expected sequence with the current diversifier included in the client message. If the client message is an update, then it must be the current sequence. If the client message is misbehaviour then it must be the sequence of the misbehaviour.
function verifyClientMessage(clientMsg: ClientMessage) {
  switch typeof(ClientMessage) {
    case Header:
      verifyHeader(clientMessage)
    // misbehaviour only supported for current public key and diversifier on solomachine
    case Misbehaviour:
      verifyMisbehaviour(clientMessage)
  }
}

function verifyHeader(header: header) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  assert(header.timestamp >= clientstate.consensusState.timestamp)
  headerData = {
    newPubKey: header.newPubKey,
    newDiversifier: header.newDiversifier,
  }
  signBytes = SignBytes(
    sequence: clientState.sequence,
    timestamp: header.timestamp,
    diversifier: clientState.consensusState.diversifier,
    path: []byte{"solomachine:header"},
    value: marshal(headerData)
  )
  assert(checkSignature(cs.consensusState.publicKey, signBytes, header.signature))
}

function verifyMisbehaviour(misbehaviour: Misbehaviour) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  s1 = misbehaviour.signatureOne
  s2 = misbehaviour.signatureTwo
  pubkey = clientState.consensusState.publicKey
  diversifier = clientState.consensusState.diversifier
  // assert that the signatures validate and that they are different
  sigBytes1 = SignBytes(
    sequence: misbehaviour.sequence,
    timestamp: s1.timestamp,
    diversifier: diversifier,
    path: s1.path,
    data: s1.data
  )
  sigBytes2 = SignBytes(
    sequence: misbehaviour.sequence,
    timestamp: s2.timestamp,
    diversifier: diversifier,
    path: s2.path,
    data: s2.data
  )
  // either the path or data must be different in order for the misbehaviour to be valid
  assert(s1.path != s2.path || s1.data != s2.data)
  assert(checkSignature(pubkey, sigBytes1, misbehaviour.signatureOne.signature))
  assert(checkSignature(pubkey, sigBytes2, misbehaviour.signatureTwo.signature))
}

Misbehaviour predicate

Since misbehaviour is checked in verifyClientMessage, if the client message is of type Misbehaviour then we return true:
function checkForMisbehaviour(clientMessage: ClientMessage): bool {
  switch typeof(ClientMessage) {
  case Misbehaviour:
    return true
  }
  return false
}

Update functions

Function updateState updates the solo machine ConsensusState values using the provided client message header:
function updateState(clientMessage: ClientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  header = Header(clientMessage)
  clientState.consensusState.publicKey = header.newPubKey
  clientState.consensusState.diversifier = header.newDiversifier
  clientState.consensusState.timestamp = header.timestamp
  clientState.sequence++
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}
Function updateStateOnMisbehaviour updates the function after receiving valid misbehaviour:
function updateStateOnMisbehaviour(clientMessage: ClientMessage) {
  clientState = provableStore.get("clients/{clientMsg.identifier}/clientState")
  // freeze the client
  clientState.frozen = true
  provableStore.set("clients/{clientMsg.identifier}/clientState", clientState)
}

State verification functions

All solo machine client state verification functions simply check a signature, which must be provided by the solo machine.
function verifyMembership(
  clientState: ClientState,
  // provided height is unnecessary for solomachine
  // since clientState maintains the expected sequence
  height: uint64,
  // delayPeriod is unsupported on solomachines
  // thus these fields are ignored
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath,
  value: []byte
): Error {
  // the expected sequence used in the signature
  abortTransactionUnless(!clientState.frozen)
  abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp)

  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since solomachines do not use multi-stores, the prefix needs 
  // to be removed from the path to retrieve the correct key in the
  // solomachine store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)
  signBytes = SignBytes(
    sequence: clientState.sequence,
    timestamp: proof.timestamp,
    diversifier: clientState.consensusState.diversifier,
    path: unprefixedPath,
    data: value,
  )
  proven = checkSignature(clientState.consensusState.publicKey, signBytes, proof.sig)
  if !proven {
    return error
  }

  // increment sequence on each verification to provide
  // replay protection
  clientState.sequence++
  clientState.consensusState.timestamp = proof.timestamp
  // unlike other clients, we must set the client state here because we
  // mutate the clientState (increment sequence and set timestamp)
  // thus the verification methods are stateful for the solomachine
  // in order to prevent replay attacks
  provableStore.set("clients/{identifier}/clientState", clientState)
  return nil
}

function verifyNonMembership(
  clientState: ClientState,
  // provided height is unnecessary for solomachine
  // since clientState maintains the expected sequence
  height: uint64,
  // delayPeriod is unsupported on solomachines
  // thus these fields are ignored
  delayTimePeriod: uint64,
  delayBlockPeriod: uint64,
  proof: CommitmentProof,
  path: CommitmentPath
): Error {
  abortTransactionUnless(!clientState.frozen)
  abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp)

  // path is prefixed with the store prefix of the commitment proof
  // e.g. in ibc-go implementation this is "ibc"
  // since solomachines do not use multi-stores, the prefix needs 
  // to be removed from the path to retrieve the correct key in the
  // solomachine store
  unprefixedPath = removePrefix(getCommitmentPrefix(), path)
  signBytes = SignBytes(
    sequence: clientState.sequence,
    timestamp: proof.timestamp,
    diversifier: clientState.consensusState.diversifier,
    path: unprefixedPath,
    data: nil,
  )
  proven = checkSignature(clientState.consensusState.publicKey, signBytes, proof.sig)
  if !proven {
    return error
  }

  // increment sequence on each verification to provide
  // replay protection
  clientState.sequence++
  clientState.consensusState.timestamp = proof.timestamp
  // unlike other clients, we must set the client state here because we
  // mutate the clientState (increment sequence and set timestamp)
  // thus the verification methods are stateful for the solomachine
  // in order to prevent replay attacks
  provableStore.set("clients/{identifier}/clientState", clientState)
  return nil
}

Properties & invariants

Instantiates the interface defined in ICS 2.

Backwards compatibility

Not applicable.

Forwards compatibility

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

Example implementations

History

December 9th, 2019 - Initial version December 17th, 2019 - Final first draft August 15th, 2022 - Changes to align with 02-client-refactor in #813 September 14th, 2022 - Changes to align with changes in #4429 All content herein is licensed under Apache 2.0.