Files
tendermint/rust-spec/lightclient/verification/verification.md
2020-09-29 14:05:44 +02:00

1163 lines
42 KiB
Markdown

# Light Client Verification
The light client implements a read operation of a
[header][TMBC-HEADER-link] from the [blockchain][TMBC-SEQ-link], by
communicating with full nodes. As some full nodes may be faulty, this
functionality must be implemented in a fault-tolerant way.
In the Tendermint blockchain, the validator set may change with every
new block. The staking and unbonding mechanism induces a [security
model][TMBC-FM-2THIRDS-link]: starting at time *Time* of the
[header][TMBC-HEADER-link],
more than two-thirds of the next validators of a new block are correct
for the duration of *TrustedPeriod*. The fault-tolerant read
operation is designed for this security model.
The challenge addressed here is that the light client might have a
block of height *h1* and needs to read the block of height *h2*
greater than *h1*. Checking all headers of heights from *h1* to *h2*
might be too costly (e.g., in terms of energy for mobile devices).
This specification tries to reduce the number of intermediate blocks
that need to be checked, by exploiting the guarantees provided by the
[security model][TMBC-FM-2THIRDS-link].
# Status
This document is thoroughly reviewed, and the protocol has been
formalized in TLA+ and model checked.
## Issues that need to be addressed
As it is part of the larger light node, its data structures and
functions interact with the fork dectection functionality of the light
client. As a result of the work on
[Pull Request 479](https://github.com/informalsystems/tendermint-rs/pull/479) we
established the need for an update in the data structures in [Issue 499](https://github.com/informalsystems/tendermint-rs/issues/499). This
will not change the verification logic, but it will record information
about verification that can be used in fork detection (in particular
in computing more efficiently the proof of fork).
# Outline
- [Part I](#part-i---tendermint-blockchain): Introduction of
relevant terms of the Tendermint
blockchain.
- [Part II](#part-ii---sequential-definition-of-the-verification-problem): Introduction
of the problem addressed by the Lightclient Verification protocol.
- [Verification Informal Problem
statement](#Verification-Informal-Problem-statement): For the general
audience, that is, engineers who want to get an overview over what
the component is doing from a bird's eye view.
- [Sequential Problem statement](#Sequential-Problem-statement):
Provides a mathematical definition of the problem statement in
its sequential form, that is, ignoring the distributed aspect of
the implementation of the blockchain.
- [Part III](#part-iii---light-client-as-distributed-system): Distributed
aspects of the light client, system assumptions and temporal
logic specifications.
- [Incentives](#incentives): how faulty full nodes may benefit from
misbehaving and how correct full nodes benefit from cooperating.
- [Computational Model](#Computational-Model):
timing and correctness assumptions.
- [Distributed Problem Statement](#Distributed-Problem-Statement):
temporal properties that formalize safety and liveness
properties in the distributed setting.
- [Part IV](#part-iv---light-client-verification-protocol):
Specification of the protocols.
- [Definitions](#Definitions): Describes inputs, outputs,
variables used by the protocol, auxiliary functions
- [Core Verification](#core-verification): gives an outline of the solution,
and details of the functions used (with preconditions,
postconditions, error conditions).
- [Liveness Scenarios](#liveness-scenarios): when the light
client makes progress depends heavily on the changes in the
validator sets of the blockchain. We discuss some typical scenarios.
- [Part V](#part-v---supporting-the-ibc-relayer): The above parts
focus on a common case where the last verified block has height *h1*
and the
requested height *h2* satisfies *h2 > h1*. For IBC, there are
scenarios where this might not be the case. In this part, we provide
some preliminaries for supporting this. As not all details of the
IBC requirements are clear by now, we do not provide a complete
specification at this point. We mark with "Open Question" points
that need to be addressed in order to finalize this specification.
It should be noted that the technically
most challenging case is the one specified in Part IV.
In this document we quite extensively use tags in order to be able to
reference assumptions, invariants, etc. in future communication. In
these tags we frequently use the following short forms:
- TMBC: Tendermint blockchain
- SEQ: for sequential specifications
- LCV: Lightclient Verification
- LIVE: liveness
- SAFE: safety
- FUNC: function
- INV: invariant
- A: assumption
# Part I - Tendermint Blockchain
## Header Fields necessary for the Light Client
#### **[TMBC-HEADER.1]**
A set of blockchain transactions is stored in a data structure called
*block*, which contains a field called *header*. (The data structure
*block* is defined [here][block]). As the header contains hashes to
the relevant fields of the block, for the purpose of this
specification, we will assume that the blockchain is a list of
headers, rather than a list of blocks.
#### **[TMBC-HASH-UNIQUENESS.1]**
We assume that every hash in the header identifies the data it hashes.
Therefore, in this specification, we do not distinguish between hashes and the
data they represent.
#### **[TMBC-HEADER-FIELDS.1]**
A header contains the following fields:
- `Height`: non-negative integer
- `Time`: time (integer)
- `LastBlockID`: Hashvalue
- `LastCommit` DomainCommit
- `Validators`: DomainVal
- `NextValidators`: DomainVal
- `Data`: DomainTX
- `AppState`: DomainApp
- `LastResults`: DomainRes
#### **[TMBC-SEQ.1]**
The Tendermint blockchain is a list *chain* of headers.
#### **[TMBC-VALIDATOR-PAIR.1]**
Given a full node, a
*validator pair* is a pair *(peerID, voting_power)*, where
- *peerID* is the PeerID (public key) of a full node,
- *voting_power* is an integer (representing the full node's
voting power in a certain consensus instance).
> In the Golang implementation the data type for *validator
pair* is called `Validator`
#### **[TMBC-VALIDATOR-SET.1]**
A *validator set* is a set of validator pairs. For a validator set
*vs*, we write *TotalVotingPower(vs)* for the sum of the voting powers
of its validator pairs.
#### **[TMBC-VOTE.1]**
A *vote* contains a `prevote` or `precommit` message sent and signed by
a validator node during the execution of [consensus][arXiv]. Each
message contains the following fields
- `Type`: prevote or precommit
- `Height`: positive integer
- `Round` a positive integer
- `BlockID` a Hashvalue of a block (not necessarily a block of the chain)
#### **[TMBC-COMMIT.1]**
A commit is a set of `precommit` message.
## Tendermint Failure Model
#### **[TMBC-AUTH-BYZ.1]**
We assume the authenticated Byzantine fault model in which no node (faulty or
correct) may break digital signatures, but otherwise, no additional
assumption is made about the internal behavior of faulty
nodes. That is, faulty nodes are only limited in that they cannot forge
messages.
#### **[TMBC-TIME-PARAMS.1]**
A Tendermint blockchain has the following configuration parameters:
- *unbondingPeriod*: a time duration.
- *trustingPeriod*: a time duration smaller than *unbondingPeriod*.
#### **[TMBC-CORRECT.1]**
We define a predicate *correctUntil(n, t)*, where *n* is a node and *t* is a
time point.
The predicate *correctUntil(n, t)* is true if and only if the node *n*
follows all the protocols (at least) until time *t*.
#### **[TMBC-FM-2THIRDS.1]**
If a block *h* is in the chain,
then there exists a subset *CorrV*
of *h.NextValidators*, such that:
- *TotalVotingPower(CorrV) > 2/3
TotalVotingPower(h.NextValidators)*; cf. [TMBC-VALIDATOR-SET.1]
- For every validator pair *(n,p)* in *CorrV*, it holds *correctUntil(n,
h.Time + trustingPeriod)*; cf. [TMBC-CORRECT.1]
> The definition of correct
> [**[TMBC-CORRECT.1]**][TMBC-CORRECT-link] refers to realtime, while it
> is used here with *Time* and *trustingPeriod*, which are "hardware
> times". We do not make a distinction here.
#### **[TMBC-CORR-FULL.1]**
Every correct full node locally stores a prefix of the
current list of headers from [**[TMBC-SEQ.1]**][TMBC-SEQ-link].
## What the Light Client Checks
> From [TMBC-FM-2THIRDS.1] we directly derive the following observation:
#### **[TMBC-VAL-CONTAINS-CORR.1]**
Given a (trusted) block *tb* of the blockchain, a given set of full nodes
*N* contains a correct node at a real-time *t*, if
- *t - trustingPeriod < tb.Time < t*
- the voting power in tb.NextValidators of nodes in *N* is more
than 1/3 of *TotalVotingPower(tb.NextValidators)*
> The following describes how a commit for a given block *b* must look
> like.
#### **[TMBC-SOUND-DISTR-POSS-COMMIT.1]**
For a block *b*, each element *pc* of *PossibleCommit(b)* satisfies:
- *pc* contains only votes (cf. [TMBC-VOTE.1])
by validators from *b.Validators*
- the sum of the voting powers in *pc* is greater than 2/3
*TotalVotingPower(b.Validators)*
- and there is an *r* such that each vote *v* in *pc* satisfies
- v.Type = precommit
- v.Height = b.Height
- v.Round = r
- v.blockID = hash(b)
> The following property comes from the validity of the [consensus][arXiv]: A
> correct validator node only sends `prevote` or `precommit`, if
> `BlockID` of the new (to-be-decided) block is equal to the hash of
> the last block.
#### **[TMBC-VAL-COMMIT.1]**
If for a block *b*, a commit *c*
- contains at least one validator pair *(v,p)* such that *v* is a
**correct** validator node, and
- is contained in *PossibleCommit(b)*
then the block *b* is on the blockchain.
## Context of this document
In this document we specify the light client verification component,
called *Core Verification*. The *Core Verification* communicates with
a full node. As full nodes may be faulty, it cannot trust the
received information, but the light client has to check whether the
header it receives coincides with the one generated by Tendermint
consensus.
The two
properties [[TMBC-VAL-CONTAINS-CORR.1]][TMBC-VAL-CONTAINS-CORR-link] and
[[TMBC-VAL-COMMIT]][TMBC-VAL-COMMIT-link] formalize the checks done
by this specification:
Given a trusted block *tb* and an untrusted block *ub* with a commit *cub*,
one has to check that *cub* is in *PossibleCommit(ub)*, and that *cub*
contains a correct node using *tb*.
# Part II - Sequential Definition of the Verification Problem
## Verification Informal Problem statement
Given a height *targetHeight* as an input, the *Verifier* eventually
stores a header *h* of height *targetHeight* locally. This header *h*
is generated by the Tendermint [blockchain][block]. In
particular, a header that was not generated by the blockchain should
never be stored.
## Sequential Problem statement
#### **[LCV-SEQ-LIVE.1]**
The *Verifier* gets as input a height *targetHeight*, and eventually stores the
header of height *targetHeight* of the blockchain.
#### **[LCV-SEQ-SAFE.1]**
The *Verifier* never stores a header which is not in the blockchain.
# Part III - Light Client as Distributed System
## Incentives
Faulty full nodes may benefit from lying to the light client, by making the
light client accept a block that deviates (e.g., contains additional
transactions) from the one generated by Tendermint consensus.
Users using the light client might be harmed by accepting a forged header.
The [fork detector][fork-detector] of the light client may help the
correct full nodes to understand whether their header is a good one.
Hence, in combination with the light client detector, the correct full
nodes have the incentive to respond. We can thus base liveness
arguments on the assumption that correct full nodes reliably talk to
the light client.
## Computational Model
#### **[LCV-A-PEER.1]**
The verifier communicates with a full node called *primary*. No assumption is made about the full node (it may be correct or faulty).
#### **[LCV-A-COMM.1]**
Communication between the light client and a correct full node is
reliable and bounded in time. Reliable communication means that
messages are not lost, not duplicated, and eventually delivered. There
is a (known) end-to-end delay *Delta*, such that if a message is sent
at time *t* then it is received and processes by time *t + Delta*.
This implies that we need a timeout of at least *2 Delta* for remote
procedure calls to ensure that the response of a correct peer arrives
before the timeout expires.
#### **[LCV-A-TFM.1]**
The Tendermint blockchain satisfies the Tendermint failure model [**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link].
#### **[LCV-A-VAL.1]**
The system satisfies [**[TMBC-AUTH-BYZ.1]**][TMBC-Auth-Byz-link] and
[**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link]. Thus, there is a
blockchain that satisfies the soundness requirements (that is, the
validation rules in [[block]]).
## Distributed Problem Statement
### Two Kinds of Termination
We do not assume that *primary* is correct. Under this assumption no
protocol can guarantee the combination of the sequential
properties. Thus, in the (unreliable) distributed setting, we consider
two kinds of termination (successful and failure) and we will specify
below under what (favorable) conditions *Core Verification* ensures to
terminate successfully, and satisfy the requirements of the sequential
problem statement:
#### **[LCV-DIST-TERM.1]**
*Core Verification* either *terminates
successfully* or it *terminates with failure*.
### Design choices
#### **[LCV-DIST-STORE.1]**
*Core Verification* has a local data structure called *LightStore* that
contains light blocks (that contain a header). For each light block we
record whether it is verified.
#### **[LCV-DIST-PRIMARY.1]**
*Core Verification* has a local variable *primary* that contains the PeerID of a full node.
#### **[LCV-DIST-INIT.1]**
*LightStore* is initialized with a header *trustedHeader* that was correctly
generated by the Tendermint consensus. We say *trustedHeader* is verified.
### Temporal Properties
#### **[LCV-DIST-SAFE.1]**
It is always the case that every verified header in *LightStore* was
generated by an instance of Tendermint consensus.
#### **[LCV-DIST-LIVE.1]**
From time to time, a new instance of *Core Verification* is called with a
height *targetHeight* greater than the height of any header in *LightStore*.
Each instance must eventually terminate.
- If
- the *primary* is correct (and locally has the block of
*targetHeight*), and
- *LightStore* always contains a verified header whose age is less than the
trusting period,
then *Core Verification* adds a verified header *hd* with height
*targetHeight* to *LightStore* and it **terminates successfully**
> These definitions imply that if the primary is faulty, a header may or
> may not be added to *LightStore*. In any case,
> [**[LCV-DIST-SAFE.1]**](#lcv-vc-inv) must hold.
> The invariant [**[LCV-DIST-SAFE.1]**](#lcv-dist-safe) and the liveness
> requirement [**[LCV-DIST-LIVE.1]**](#lcv-dist-life)
> allow that verified headers are added to *LightStore* whose
> height was not passed
> to the verifier (e.g., intermediate headers used in bisection; see below).
> Note that for liveness, initially having a *trustedHeader* within
> the *trustinPeriod* is not sufficient. However, as this
> specification will leave some freedom with respect to the strategy
> in which order to download intermediate headers, we do not give a
> more precise liveness specification here. After giving the
> specification of the protocol, we will discuss some liveness
> scenarios [below](#liveness-scenarios).
### Solving the sequential specification
This specification provides a partial solution to the sequential specification.
The *Verifier* solves the invariant of the sequential part
[**[LCV-DIST-SAFE.1]**](#lcv-vc-inv) => [**[LCV-SEQ-SAFE.1]**](#lcv-seq-inv)
In the case the primary is correct, and there is a recent header in *LightStore*, the verifier satisfies the liveness requirements.
*primary is correct*
⋀ always ∃ verified header in LightStore. *header.Time* > *now* - *trustingPeriod*
⋀ [**[LCV-A-Comm.1]**](#lcv-a-comm) ⋀ (
( [**[TMBC-CorrFull.1]**][TMBC-CorrFull-link] ⋀
[**[LCV-DIST-LIVE.1]**](#lcv-vc-live) )
⟹ [**[LCV-SEQ-LIVE.1]**](#lcv-seq-live)
)
# Part IV - Light Client Verification Protocol
We provide a specification for Light Client Verification. The local
code for verification is presented by a sequential function
`VerifyToTarget` to highlight the control flow of this functionality.
We note that if a different concurrency model is considered for
an implementation, the sequential flow of the function may be
implemented with mutexes, etc. However, the light client verification
is partitioned into three blocks that can be implemented and tested
independently:
- `FetchLightBlock` is called to download a light block (header) of a
given height from a peer.
- `ValidAndVerified` is a local code that checks the header.
- `Schedule` decides which height to try to verify next. We keep this
underspecified as different implementations (currently in Goland and
Rust) may implement different optimizations here. We just provide
necessary conditions on how the height may evolve.
<!-- > `ValidAndVerified` is the function that is sometimes called "Light -->
<!-- > Client" in the IBC context. -->
## Definitions
### Data Types
The core data structure of the protocol is the LightBlock.
#### **[LCV-DATA-LIGHTBLOCK.1]**
```go
type LightBlock struct {
Header Header
Commit Commit
Validators ValidatorSet
NextValidators ValidatorSet
Provider PeerID
}
```
#### **[LCV-DATA-LIGHTSTORE.1]**
LightBlocks are stored in a structure which stores all LightBlock from
initialization or received from peers.
```go
type LightStore struct {
...
}
```
Each LightBlock is in one of the following states:
```go
type VerifiedState int
const (
StateUnverified = iota + 1
StateVerified
StateFailed
StateTrusted
)
```
> Only the detector module sets a lightBlock state to `StateTrusted`
> and only if it was `StateVerified` before.
The LightStore exposes the following functions to query stored LightBlocks.
```go
func (ls LightStore) Get(height Height) (LightBlock, bool)
```
- Expected postcondition
- returns a LightBlock at a given height or false in the second argument if
the LightStore does not contain the specified LightBlock.
```go
func (ls LightStore) LatestVerified() LightBlock
```
- Expected postcondition
- returns the highest light block whose state is `StateVerified`
or `StateTrusted`
#### **[LCV-FUNC-UPDATE.1]**
```go
func (ls LightStore) Update(lightBlock LightBlock, verfiedState VerifiedState)
```
- Expected postcondition
- The state of the LightBlock is set to *verifiedState*.
> The following function is used only in the detector specification
> listed here for completeness.
```go
func (ls LightStore) LatestTrusted() LightBlock
```
- Expected postcondition
- returns the highest light block that has been verified and
checked by the detector.
### Inputs
- *lightStore*: stores light blocks that have been downloaded and that
passed verification. Initially it contains a light block with
*trustedHeader*.
- *primary*: peerID
- *targetHeight*: the height of the needed header
### Configuration Parameters
- *trustThreshold*: a float. Can be used if correctness should not be based on more voting power and 1/3.
- *trustingPeriod*: a time duration [**[TMBC-TIME_PARAMS.1]**][TMBC-TIME_PARAMS-link].
- *clockDrift*: a time duration. Correction parameter dealing with only approximately synchronized clocks.
### Variables
- *nextHeight*: initially *targetHeight*
> *nextHeight* should be thought of the "height of the next header we need
> to download and verify"
### Assumptions
#### **[LCV-A-INIT.1]**
- *trustedHeader* is from the blockchain
- *targetHeight > LightStore.LatestVerified.Header.Height*
### Invariants
#### **[LCV-INV-TP.1]**
It is always the case that *LightStore.LatestTrusted.Header.Time > now - trustingPeriod*.
> If the invariant is violated, the light client does not have a
> header it can trust. A trusted header must be obtained externally,
> its trust can only be based on social consensus.
### Used Remote Functions
We use the functions `commit` and `validators` that are provided
by the [RPC client for Tendermint][RPC].
```go
func Commit(height int64) (SignedHeader, error)
```
- Implementation remark
- RPC to full node *n*
- JSON sent:
```javascript
// POST /commit
{
"jsonrpc": "2.0",
"id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request
"method": "commit",
"params": {
"height": 1234
}
}
```
- Expected precondition
- header of `height` exists on blockchain
- Expected postcondition
- if *n* is correct: Returns the signed header of height `height`
from the blockchain if communication is timely (no timeout)
- if *n* is faulty: Returns a signed header with arbitrary content
- Error condition
- if *n* is correct: precondition violated or timeout
- if *n* is faulty: arbitrary error
----
```go
func Validators(height int64) (ValidatorSet, error)
```
- Implementation remark
- RPC to full node *n*
- JSON sent:
```javascript
// POST /validators
{
"jsonrpc": "2.0",
"id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request
"method": "validators",
"params": {
"height": 1234
}
}
```
- Expected precondition
- header of `height` exists on blockchain
- Expected postcondition
- if *n* is correct: Returns the validator set of height `height`
from the blockchain if communication is timely (no timeout)
- if *n* is faulty: Returns arbitrary validator set
- Error condition
- if *n* is correct: precondition violated or timeout
- if *n* is faulty: arbitrary error
----
### Communicating Function
#### **[LCV-FUNC-FETCH.1]**
```go
func FetchLightBlock(peer PeerID, height Height) LightBlock
```
- Implementation remark
- RPC to peer at *PeerID*
- calls `Commit` for *height* and `Validators` for *height* and *height+1*
- Expected precondition
- `height` is less than or equal to height of the peer **[LCV-IO-PRE-HEIGHT.1]**
- Expected postcondition:
- if *node* is correct:
- Returns the LightBlock *lb* of height `height`
that is consistent with the blockchain
- *lb.provider = peer* **[LCV-IO-POST-PROVIDER.1]**
- *lb.Header* is a header consistent with the blockchain
- *lb.Validators* is the validator set of the blockchain at height *nextHeight*
- *lb.NextValidators* is the validator set of the blockchain at height *nextHeight + 1*
- if *node* is faulty: Returns a LightBlock with arbitrary content
[**[TMBC-AUTH-BYZ.1]**][TMBC-Auth-Byz-link]
- Error condition
- if *n* is correct: precondition violated
- if *n* is faulty: arbitrary error
- if *lb.provider != peer*
- times out after 2 Delta (by assumption *n* is faulty)
----
## Core Verification
### Outline
The `VerifyToTarget` is the main function and uses the following functions.
- `FetchLightBlock` is called to download the next light block. It is
the only function that communicates with other nodes
- `ValidAndVerified` checks whether header is valid and checks if a
new lightBlock should be trusted
based on a previously verified lightBlock.
- `Schedule` decides which height to try to verify next
In the following description of `VerifyToTarget` we do not deal with error
handling. If any of the above function returns an error, VerifyToTarget just
passes the error on.
#### **[LCV-FUNC-MAIN.1]**
```go
func VerifyToTarget(primary PeerID, lightStore LightStore,
targetHeight Height) (LightStore, Result) {
nextHeight := targetHeight
for lightStore.LatestVerified.height < targetHeight {
// Get next LightBlock for verification
current, found := lightStore.Get(nextHeight)
if !found {
current = FetchLightBlock(primary, nextHeight)
lightStore.Update(current, StateUnverified)
}
// Verify
verdict = ValidAndVerified(lightStore.LatestVerified, current)
// Decide whether/how to continue
if verdict == OK {
lightStore.Update(current, StateVerified)
}
else if verdict == CANNOT_VERIFY {
// do nothing
// the light block current passed validation, but the validator
// set is too different to verify it. We keep the state of
// current at StateUnverified. For a later iteration, Schedule
// might decide to try verification of that light block again.
}
else {
// verdict is some error code
lightStore.Update(current, StateFailed)
// possibly remove all LightBlocks from primary
return (lightStore, ResultFailure)
}
nextHeight = Schedule(lightStore, nextHeight, targetHeight)
}
return (lightStore, ResultSuccess)
}
```
- Expected precondition
- *lightStore* contains a LightBlock within the *trustingPeriod* **[LCV-PRE-TP.1]**
- *targetHeight* is greater than the height of all the LightBlocks in *lightStore*
- Expected postcondition:
- returns *lightStore* that contains a LightBlock that corresponds to a block
of the blockchain of height *targetHeight*
(that is, the LightBlock has been added to *lightStore*) **[LCV-POST-LS.1]**
- Error conditions
- if the precondition is violated
- if `ValidAndVerified` or `FetchLightBlock` report an error
- if [**[LCV-INV-TP.1]**](#LCV-INV-TP.1) is violated
### Details of the Functions
#### **[LCV-FUNC-VALID.1]**
```go
func ValidAndVerified(trusted LightBlock, untrusted LightBlock) Result
```
- Expected precondition:
- *untrusted* is valid, that is, satisfies the soundness [checks][block]
- *untrusted* is **well-formed**, that is,
- *untrusted.Header.Time < now + clockDrift*
- *untrusted.Validators = hash(untrusted.Header.Validators)*
- *untrusted.NextValidators = hash(untrusted.Header.NextValidators)*
- *trusted.Header.Time > now - trustingPeriod*
- *trusted.Commit* is a commit for the header
*trusted.Header*, i.e., it contains
the correct hash of the header, and +2/3 of signatures
- the `Height` and `Time` of `trusted` are smaller than the Height and
`Time` of `untrusted`, respectively
- the *untrusted.Header* is well-formed (passes the tests from
[[block]]), and in particular
- if the untrusted header `unstrusted.Header` is the immediate
successor of `trusted.Header`, then it holds that
- *trusted.Header.NextValidators =
untrusted.Header.Validators*, and
moreover,
- *untrusted.Header.Commit*
- contains signatures by more than two-thirds of the validators
- contains no signature from nodes that are not in *trusted.Header.NextValidators*
- Expected postcondition:
- Returns `OK`:
- if *untrusted* is the immediate successor of *trusted*, or otherwise,
- if the signatures of a set of validators that have more than
*max(1/3,trustThreshold)* of voting power in
*trusted.Nextvalidators* is contained in
*untrusted.Commit* (that is, header passes the tests
[**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link]
and [**[TMBC-VAL-COMMIT.1]**][TMBC-VAL-COMMIT-link])
- Returns `CANNOT_VERIFY` if:
- *untrusted* is *not* the immediate successor of
*trusted*
and the *max(1/3,trustThreshold)* threshold is not reached
(that is, if
[**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link]
fails and header is does not violate the soundness
checks [[block]]).
- Error condition:
- if precondition violated
----
#### **[LCV-FUNC-SCHEDULE.1]**
```go
func Schedule(lightStore, nextHeight, targetHeight) Height
```
- Implementation remark: If picks the next height to be verified.
We keep the precise choice of the next header under-specified. It is
subject to performance optimizations that do not influence the correctness
- Expected postcondition: **[LCV-SCHEDULE-POST.1]**
Return *H* s.t.
1. if *lightStore.LatestVerified.Height = nextHeight* and
*lightStore.LatestVerified < targetHeight* then
*nextHeight < H <= targetHeight*
2. if *lightStore.LatestVerified.Height < nextHeight* and
*lightStore.LatestVerified.Height < targetHeight* then
*lightStore.LatestVerified.Height < H < nextHeight*
3. if *lightStore.LatestVerified.Height = targetHeight* then
*H = targetHeight*
> Case i. captures the case where the light block at height *nextHeight*
> has been verified, and we can choose a height closer to the *targetHeight*.
> As we get the *lightStore* as parameter, the choice of the next height can
> depend on the *lightStore*, e.g., we can pick a height for which we have
> already downloaded a light block.
> In Case ii. the header of *nextHeight* could not be verified, and we need to pick a smaller height.
> In Case iii. is a special case when we have verified the *targetHeight*.
### Solving the distributed specification
*trustedStore* is implemented by the light blocks in lightStore that
have the state *StateVerified*.
#### Argument for [**[LCV-DIST-SAFE.1]**](#lcv-dist-safe)
- `ValidAndVerified` implements the soundness checks and the checks
[**[TMBC-VAL-CONTAINS-CORR.1]**][TMBC-VAL-CONTAINS-CORR-link] and
[**[TMBC-VAL-COMMIT.1]**][TMBC-VAL-COMMIT-link] under
the assumption [**[TMBC-FM-2THIRDS.1]**][TMBC-FM-2THIRDS-link]
- Only if `ValidAndVerified` returns with `OK`, the state of a light block is
set to *StateVerified*.
#### Argument for [**[LCV-DIST-LIVE.1]**](#lcv-dist-life)
- If *primary* is correct,
- `FetchLightBlock` will always return a light block consistent
with the blockchain
- `ValidAndVerified` either verifies the header using the trusting
period or falls back to sequential
verification
- If [**[LCV-INV-TP.1]**](#LCV-INV-TP.1) holds, eventually every
header will be verified and core verification **terminates successfully**.
- successful termination depends on the age of *lightStore.LatestVerified*
(for instance, initially on the age of *trustedHeader*) and the
changes of the validator sets on the blockchain.
We will give some examples [below](#liveness-scenarios).
- If *primary* is faulty,
- it either provides headers that pass all the tests, and we
return with the header
- it provides one header that fails a test, core verification
**terminates with failure**.
- it times out and core verification
**terminates with failure**.
## Liveness Scenarios
The liveness argument above assumes [**[LCV-INV-TP.1]**](#LCV-INV-TP.1)
which requires that there is a header that does not expire before the
target height is reached. Here we discuss scenarios to ensure this.
Let *startHeader* be *LightStore.LatestVerified* when core
verification is called (*trustedHeader*) and *startTime* be the time
core verification is invoked.
In order to ensure liveness, *LightStore* always needs to contain a
verified (or initially trusted) header whose time is within the
trusting period. To ensure this, core verification needs to add new
headers to *LightStore* and verify them, before all headers in
*LightStore* expire.
#### Many changes in validator set
Let's consider `Schedule` implements
bisection, that is, it halves the distance.
Assume the case where the validator set changes completely in each
block. Then the
method in this specification needs to
sequentially verify all headers. That is, for
- *W = log_2 (targetHeight - startHeader.Height)*,
*W* headers need to be downloaded and checked before the
header of height *startHeader.Height + 1* is added to *LightStore*.
- Let *Comp*
be the local computation time needed to check headers and signatures
for one header.
- Then we need in the worst case *Comp + 2 Delta* to download and
check one header.
- Then the first time a verified header could be added to *LightStore* is
startTime + W * (Comp + 2 Delta)
- [TP.1] However, it can only be added if we still have a header in
*LightStore*,
which is not
expired, that is only the case if
- startHeader.Time > startTime + WCG * (Comp + 2 Delta) -
trustingPeriod,
- that is, if core verification is started at
startTime < startHeader.Time + trustingPeriod - WCG * (Comp + 2 Delta)
- one may then do an inductive argument from this point on, depending
on the implementation of `Schedule`. We may have to account for the
headers that are already
downloaded, but they are checked against the new *LightStore.LatestVerified*.
> We observe that
> the worst case time it needs to verify the header of height
> *targetHeight* depends mainly on how frequent the validator set on the
> blockchain changes. That core verification terminates successfully
> crucially depends on the check [TP.1], that is, that the headers in
> *LightStore* do not expire in the time needed to download more
> headers, which depends on the creation time of the headers in
> *LightStore*. That is, termination of core verification is highly
> depending on the data stored in the blockchain.
> The current light client core verification protocol exploits that, in
> practice, changes in the validator set are rare. For instance,
> consider the following scenario.
#### No change in validator set
If on the blockchain the validator set of the block at height
*targetHeight* is equal to *startHeader.NextValidators*:
- there is one round trip in `FetchLightBlock` to download the light
block
of height
*targetHeight*, and *Comp* to check it.
- as the validator sets are equal, `Verify` returns `OK`, if
*startHeader.Time > now - trustingPeriod*.
- that is, if *startTime < startHeader.Header.Time + trustingPeriod -
2 Delta - Comp*, then core verification terminates successfully
# Part V - Supporting the IBC Relayer
The above specification focuses on the most common case, which also
constitutes the most challenging task: using the Tendermint [security
model][TMBC-FM-2THIRDS-link] to verify light blocks without
downloading all intermediate blocks. To focus on this challenge, above
we have restricted ourselves to the case where *targetHeight* is
greater than the height of any trusted header. This simplified
presentation of the algorithm as initially
`lightStore.LatestVerified()` is less than *targetHeight*, and in the
process of verification `lightStore.LatestVerified()` increases until
*targetHeight* is reached.
For [IBC][ibc-rs] it might be that some "older" header is
needed, that is, *targetHeight < lightStore.LatestVerified()*. In this section we present a preliminary design, and we mark some
remaining open questions.
If *targetHeight < lightStore.LatestVerified()* our design separates
the following cases:
- A previous instance of `VerifyToTarget` has already downloaded the
light block of *targetHeight*. There are two cases
- the light block has been verified
- the light block has not been verified yet
- No light block of *targetHeight* had been downloaded before. There
are two cases:
- there exists a verified light block of height less than *targetHeight*
- otherwise. In this case we need to do "backwards verification"
using the hash of the previous block in the `LastBlockID` field
of a header.
**Open Question:** what are the security assumptions for backward
verification. Should we check that the light block we verify from
(and/or the checked light block) is within the trusting period?
The design just presents the above case
distinction as a function, and defines some auxiliary functions in the
same way the protocol was presented in
[Part IV](#part-iv---light-client-verification-protocol).
```go
func (ls LightStore) LatestPrevious(height Height) (LightBlock, bool)
```
- Expected postcondition
- returns a light block *lb* that satisfies:
- *lb* is in lightStore
- *lb* is verified and not expired
- *lb.Header.Height < height*
- for all *b* in lightStore s.t. *b* is verified and not expired it
holds *lb.Header.Height >= b.Header.Height*
- *false* in the second argument if
the LightStore does not contain such an *lb*.
```go
func (ls LightStore) MinVerified() (LightBlock, bool)
```
- Expected postcondition
- returns a light block *lb* that satisfies:
- *lb* is in lightStore
- *lb* is verified **Open Question:** replace by trusted?
- *lb.Header.Height* is minimal in the lightStore
- **Open Question:** according to this, it might be expired (outside the
trusting period). This approach appears safe. Are there reasons we
should not do that?
- *false* in the second argument if
the LightStore does not contain such an *lb*.
If a height that is smaller than the smallest height in the lightstore
is required, we check the hashes backwards. This is done with the
following function:
#### **[LCV-FUNC-BACKWARDS.1]**
```go
func Backwards (primary PeerID, lightStore LightStore, targetHeight Height)
(LightStore, Result) {
lb,res = lightStore.MinVerified()
if res = false {
return (lightStore, ResultFailure)
}
latest := lb.Header
for i := lb.Header.height - 1; i >= targetHeight; i-- {
// here we download height-by-height. We might first download all
// headers down to targetHeight and then check them.
current := FetchLightBlock(primary,i)
if (hash(current) != latest.Header.LastBlockId) {
return (lightStore, ResultFailure)
}
else {
lightStore.Update(current, StateVerified)
// **Open Question:** Do we need a new state type for
// backwards verified light blocks?
}
latest = current
}
return (lightStore, ResultSuccess)
}
```
The following function just decided based on the required height which
method should be used.
#### **[LCV-FUNC-IBCMAIN.1]**
```go
func Main (primary PeerID, lightStore LightStore, targetHeight Height)
(LightStore, Result) {
b1, r1 = lightStore.Get(targetHeight)
if r1 = true and b1.State = StateVerified {
// block already there
return (lightStore, ResultSuccess)
}
if targetHeight > lightStore.LatestVerified.height {
// case of Part IV
return VerifyToTarget(primary, lightStore, targetHeight)
}
else {
b2, r2 = lightStore.LatestPrevious(targetHeight);
if r2 = true {
// make auxiliary lightStore auxLS to call VerifyToTarget.
// VerifyToTarget uses LatestVerified of the given lightStore
// For that we need:
// auxLS.LatestVerified = lightStore.LatestPrevious(targetHeight)
auxLS.Init;
auxLS.Update(b2,StateVerified);
if r1 = true {
// we need to verify a previously downloaded light block.
// we add it to the auxiliary store so that VerifyToTarget
// does not download it again
auxLS.Update(b1,b1.State);
}
auxLS, res2 = VerifyToTarget(primary, auxLS, targetHeight)
// move all lightblocks from auxLS to lightStore,
// maintain state
// we do that whether VerifyToTarget was successful or not
for i, s range auxLS {
lighStore.Update(s,s.State)
}
return (lightStore, res2)
}
else {
return Backwards(primary, lightStore, targetHeight)
}
}
}
```
<!-- - Expected postcondition: -->
<!-- - if targetHeight > lightStore.LatestVerified.height then -->
<!-- return VerifyToTarget(primary, lightStore, targetHeight) -->
<!-- - if targetHeight = lightStore.LatestVerified.height then -->
<!-- return (lightStore, ResultSuccess) -->
<!-- - if targetHeight < lightStore.LatestVerified.height -->
<!-- - let b2 be in lightStore -->
<!-- - that is verified and not expired -->
<!-- - b2.Header.Height < targetHeight -->
<!-- - for all b in lightStore s.t. b is verified and not expired it -->
<!-- holds b2.Header.Height >= b.Header.Height -->
<!-- - if b2 does not exists -->
<!-- return Backwards(primary, lightStore, targetHeight) -->
<!-- - if b2 exists -->
<!-- - make auxiliary light store auxLS containing only b2 -->
<!-- VerifyToTarget(primary, auxLS, targetHeight) -->
<!-- - if b2 -->
# References
[[block]] Specification of the block data structure.
[[RPC]] RPC client for Tendermint
[[fork-detector]] The specification of the light client fork detector.
[[fullnode]] Specification of the full node API
[[ibc-rs]] Rust implementation of IBC modules and relayer.
[[lightclient]] The light client ADR [77d2651 on Dec 27, 2019].
[RPC]: https://docs.tendermint.com/master/rpc/
[block]: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md
[TMBC-HEADER-link]: #tmbc-header1
[TMBC-SEQ-link]: #tmbc-seq1
[TMBC-CorrFull-link]: #tmbc-corr-full1
[TMBC-Auth-Byz-link]: #tmbc-auth-byz1
[TMBC-TIME_PARAMS-link]: #tmbc-time-params1
[TMBC-FM-2THIRDS-link]: #tmbc-fm-2thirds1
[TMBC-VAL-CONTAINS-CORR-link]: #tmbc-val-contains-corr1
[TMBC-VAL-COMMIT-link]: #tmbc-val-commit1
[TMBC-SOUND-DISTR-POSS-COMMIT-link]: #tmbc-sound-distr-poss-commit1
[lightclient]: https://github.com/interchainio/tendermint-rs/blob/e2cb9aca0b95430fca2eac154edddc9588038982/docs/architecture/adr-002-lite-client.md
[fork-detector]: https://github.com/tendermint/spec/blob/master/rust-spec/lightclient/detection/detection_001_reviewed.md
<!-- [fullnode]: https://github.com/tendermint/spec/blob/master/spec/blockchain/fullnode.md -->
[ibc-rs]:https://github.com/informalsystems/ibc-rs
<!-- [FN-LuckyCase-link]: https://github.com/tendermint/spec/blob/master/spec/blockchain/fullnode.md#fn-luckycase -->
[blockchain-validator-set]: https://github.com/tendermint/spec/blob/master/spec/blockchain/blockchain.md#data-structures
[fullnode-data-structures]: https://github.com/tendermint/spec/blob/master/spec/core/data_structures.md
<!-- [FN-ManifestFaulty-link]: https://github.com/tendermint/spec/blob/master/spec/blockchain/fullnode.md#fn-manifestfaulty -->
[arXiv]: https://arxiv.org/abs/1807.04938