ics | title | stage | category | kind | implements | author | created | modified |
---|---|---|---|---|---|---|---|---|
7 |
Tendermint Client |
draft |
IBC/TAO |
instantiation |
2 |
Christopher Goes <cwgoes@tendermint.com> |
2019-12-10 |
2019-12-19 |
This specification document describes a client (verification algorithm) for a blockchain using Tendermint consensus.
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.
Functions & terms are as defined in ICS 2.
The Tendermint light client uses the generalised Merkle proof format as defined in ICS 8.
This specification must satisfy the client interface defined in ICS 2.
This specification depends on correct instantiation of the Tendermint consensus algorithm and light client algorithm.
The Tendermint client state tracks the current validator set, latest height, and a possible frozen height.
interface ClientState {
validatorSet: List<Pair<Address, uint64>>
latestHeight: uint64
frozenHeight: Maybe<uint64>
}
The Tendermint client tracks the validator set hash & commitment root for all previously verified consensus states (these can be pruned after awhile).
interface ConsensusState {
validatorSetHash: []byte
commitmentRoot: []byte
}
The Tendermint client headers include a height, the commitment root, the complete validator set, and the signatures by the validators who committed the block.
interface Header {
height: uint64
commitmentRoot: []byte
validatorSet: List<Pair<Address, uint64>>
signatures: []Signature
}
The Evidence
type is used for detecting misbehaviour and freezing the client - to prevent further packet flow - if applicable.
Tendermint client Evidence
consists of two headers at the same height both of which the light client would have considered valid.
interface Evidence {
fromValidatorSet: List<Pair<Address, uint64>>
fromHeight: uint64
h1: Header
h2: Header
}
Tendermint client initialisation requires a (subjectively chosen) latest consensus state, including the full validator set.
function initialize(consensusState: ConsensusState, validatorSet: List<Pair<Address, uint64>>, latestHeight: uint64): ClientState {
return ClientState{
validatorSet,
latestHeight,
pastHeaders: Map.singleton(latestHeight, consensusState)
}
}
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 checkValidityAndUpdateState(
clientState: ClientState,
header: Header) {
// assert that header is newer than any we know
assert(header.height < clientState.latestHeight)
// call the `verify` function
assert(verify(clientState.validatorSet, clientState.latestHeight, header))
// update latest height
clientState.latestHeight = header.height
// create recorded consensus state, save it
consensusState = ConsensusState{validatorSet.hash(), header.commitmentRoot}
set("consensusStates/{identifier}/{header.height}", consensusState)
// save the client
set("clients/{identifier}", clientState)
}
Tendermint client misbehaviour checking determines whether or not two conflicting headers at the same height would have convinced the light client.
function checkMisbehaviourAndUpdateState(
clientState: ClientState,
evidence: Evidence) {
// assert that the heights are the same
assert(h1.height === h2.height)
// assert that the commitments are different
assert(h1.commitmentRoot !== h2.commitmentRoot)
// fetch the previously verified commitment root & validator set hash
consensusState = get("consensusStates/{identifier}/{evidence.fromHeight}")
// check that the validator set matches
assert(consensusState.validatorSetHash === evidence.fromValidatorSet.hash())
// check if the light client "would have been fooled"
assert(
verify(evidence.fromValidatorSet, evidence.fromHeight, h1) &&
verify(evidence.fromValidatorSet, evidence.fromHeight, h2)
)
// set the frozen height
clientState.frozenHeight = min(h1.height, h2.height)
// save the client
set("clients/{identifier}", clientState)
}
Tendermint client state verification functions check a Merkle proof against a previously validated commitment root.
function verifyClientConsensusState(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
clientIdentifier: Identifier,
consensusState: ConsensusState) {
path = applyPrefix(prefix, "consensusStates/{clientIdentifier}")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that the provided consensus state has been stored
assert(root.verifyMembership(path, consensusState, proof))
}
function verifyConnectionState(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
connectionIdentifier: Identifier,
connectionEnd: ConnectionEnd) {
path = applyPrefix(prefix, "connection/{connectionIdentifier}")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that the provided connection end has been stored
assert(root.verifyMembership(path, connectionEnd, proof))
}
function verifyChannelState(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
channelEnd: ChannelEnd) {
path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that the provided channel end has been stored
assert(root.verifyMembership(path, channelEnd, proof))
}
function verifyPacketCommitment(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
sequence: uint64,
commitment: bytes) {
path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/packets/{sequence}")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that the provided commitment has been stored
assert(root.verifyMembership(path, commitment, proof))
}
function verifyPacketAcknowledgement(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
sequence: uint64,
acknowledgement: bytes) {
path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/acknowledgements/{sequence}")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that the provided acknowledgement has been stored
assert(root.verifyMembership(path, acknowledgement, proof))
}
function verifyPacketAcknowledgementAbsence(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
sequence: uint64) {
path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/acknowledgements/{sequence}")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that no acknowledgement has been stored
assert(root.verifyNonMembership(path, proof))
}
function verifyNextSequenceRecv(
clientState: ClientState,
height: uint64,
prefix: CommitmentPrefix,
proof: CommitmentProof,
portIdentifier: Identifier,
channelIdentifier: Identifier,
nextSequenceRecv: uint64) {
path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/nextSequenceRecv")
// 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
root = get("consensusStates/{identifier}/{height}")
// verify that the nextSequenceRecv is as claimed
assert(root.verifyMembership(path, nextSequenceRecv, proof))
}
Correctness guarantees as provided by the Tendermint light client algorithm.
Not applicable.
Not applicable. Alterations to the client verification algorithm will require a new client standard.
None yet.
None at present.
December 10th, 2019 - Initial version December 19th, 2019 - Final first draft
All content herein is licensed under Apache 2.0.