The light client implements a read operation of a header from the blockchain, 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: starting at time Time of the header, 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.
As it is part of the larger light node, its data structures and functions interact with the attack dectection functionality of the light client. As a result of the work on
attack detection for light nodes
attack detection for IBC and relayer requirements
light client supervisor (also in Rust proposal)
adaptations to the semantics and functions exposed by the LightStore needed to be made. In contrast to version 001 we specify the following:
VerifyToTarget
and Backwards
are called with a single lightblock
as root of trust in contrast to passing the complete lightstore.
During verification, we record for each lightblock which other lightblock can be used to verify it in one step. This is needed to generate verification traces that are needed for IBC.
Part I: Introduction of relevant terms of the Tendermint blockchain.
Part II: Introduction of the problem addressed by the Lightclient Verification protocol.
Part III: Distributed aspects of the light client, system assumptions and temporal logic specifications.
Incentives: how faulty full nodes may benefit from misbehaving and how correct full nodes benefit from cooperating.
Computational Model: timing and correctness assumptions.
Distributed Problem Statement: temporal properties that formalize safety and liveness properties in the distributed setting.
Part IV: Specification of the protocols.
Definitions: Describes inputs, outputs, variables used by the protocol, auxiliary functions
Core Verification: gives an outline of the solution, and details of the functions used (with preconditions, postconditions, error conditions).
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: 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:
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). 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.
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.
A header contains the following fields:
Height
: non-negative integerTime
: time (non-negative integer)LastBlockID
: HashvalueLastCommit
DomainCommitValidators
: DomainValNextValidators
: DomainValData
: DomainTXAppState
: DomainAppLastResults
: DomainResThe Tendermint blockchain is a list chain of headers.
Given a full node, a validator pair is a pair (peerID, voting_power), where
In the Golang implementation the data type for validator pair is called
Validator
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.
A vote contains a prevote
or precommit
message sent and signed by
a validator node during the execution of consensus. Each
message contains the following fields
Type
: prevote or precommitHeight
: positive integerRound
a positive integerBlockID
a Hashvalue of a block (not necessarily a block of the chain)A commit is a set of precommit
message.
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.
A Tendermint blockchain has the following configuration parameters:
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.
If a block h is in the chain, then there exists a subset CorrV of h.NextValidators, such that:
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.
Every correct full node locally stores a prefix of the current list of headers from **[TMBC-SEQ.1]**.
From [TMBC-FM-2THIRDS.1] we directly derive the following observation:
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
The following describes how a commit for a given block b must look like.
For a block b, each element pc of PossibleCommit(b) satisfies:
The following property comes from the validity of the consensus: A correct validator node only sends
prevote
orprecommit
, ifBlockID
of the new (to-be-decided) block is equal to the hash of the last block.
If for a block b, a commit c
then the block b is on the blockchain.
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] and [TMBC-VAL-COMMIT] 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.
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. In particular, a header that was not generated by the blockchain should never be stored.
The Verifier gets as input a height targetHeight, and eventually stores the header of height targetHeight of the blockchain.
The Verifier never stores a header which is not in the blockchain.
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 attack 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.
The verifier communicates with a full node called primary. No assumption is made about the full node (it may be correct or faulty).
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.
The Tendermint blockchain satisfies the Tendermint failure model **[TMBC-FM-2THIRDS.1]**.
The system satisfies [TMBC-AUTH-BYZ.1]** and [TMBC-FM-2THIRDS.1]**. Thus, there is a blockchain that satisfies the soundness requirements (that is, the validation rules in [block]).
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:
Core Verification either terminates successfully or it terminates with failure.
Core Verification returns a data structure called LightStore that contains light blocks (that contain a header).
Core Verification is called with
It is always the case that every header in LightStore was generated by an instance of Tendermint consensus.
If a new instance of Core Verification is called with a height targetHeight greater than root.Header.Height it must must eventually terminate.
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.2]** must hold. The invariant [LCV-DIST-SAFE.2]** and the liveness requirement **[LCV-DIST-LIVE.2]** 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 root 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.
This specification provides a partial solution to the sequential specification. The Verifier solves the invariant of the sequential part
[LCV-DIST-SAFE.2]** => [LCV-SEQ-SAFE.1]**
In the case the primary is correct, and root is a recent header in LightStore, the verifier satisfies the liveness requirements.
⋀ primary is correct
⋀ root.header.Time > now - trustingPeriod
⋀ [LCV-A-Comm.1]** ⋀ (
( [TMBC-CorrFull.1]** ⋀
[LCV-DIST-LIVE.2]** )
⟹ [LCV-SEQ-LIVE.1]**
)
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.The core data structure of the protocol is the LightBlock.
type LightBlock struct {
Header Header
Commit Commit
Validators ValidatorSet
}
LightBlocks are stored in a structure which stores all LightBlock from initialization or received from peers.
type LightStore struct {
...
}
For each lightblock in a lightstore we record in a field verification-root
of
type Height.
verification-root
records the height of a lightblock that can be used to verify the lightblock in one step
At all times, if a lightblock b in a lightstore has b.verification-root = h, then
The LightStore exposes the following functions to query stored LightBlocks.
Each LightBlock is in one of the following states:
type VerifiedState int
const (
StateUnverified = iota + 1
StateVerified
StateFailed
StateTrusted
)
func (ls LightStore) Get(height Height) (LightBlock, bool)
func (ls LightStore) Latest() LightBlock
func (ls LightStore) Add(newBlock)
func (ls LightStore) store_chain(newLS LightStore)
newLS
to the lightStore.func (ls LightStore) LatestVerified() LightBlock
StateVerified
func (ls LightStore) FilterVerified() LightStore
StateVerified
func (ls LightStore) Update(lightBlock LightBlock, verfiedState
VerifiedState, root-height Height)
func (ls LightStore) TraceTo(lightBlock LightBlock) (LightBlock, LightStore)
root
from the lightstore with a height
less than lightBlock
root
to lightBlock
(including lightBlock
)nextHeight should be thought of the "height of the next header we need to download and verify"
root is from the blockchain
targetHeight > root.Header.Height
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.
We use the convention that root is assumed to be verified.
We use the functions commit
and validators
that are provided
by the RPC client for Tendermint.
func Commit(height int64) (SignedHeader, error)
// POST /commit
{
"jsonrpc": "2.0",
"id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request
"method": "commit",
"params": {
"height": 1234
}
}
height
exists on blockchainheight
from the blockchain if communication is timely (no timeout)----;
func Validators(height int64) (ValidatorSet, error)
// POST /validators
{
"jsonrpc": "2.0",
"id": "ccc84631-dfdb-4adc-b88c-5291ea3c2cfb", // UUID v4, unique per request
"method": "validators",
"params": {
"height": 1234
}
}
height
exists on blockchainheight
from the blockchain if communication is timely (no timeout)----;
func FetchLightBlock(peer PeerID, height Height) LightBlock
Commit
for height and Validators
for height and height+1height
is less than or equal to height of the peer [LCV-IO-PRE-HEIGHT.1]height
that is consistent with the blockchain----;
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 nodesValidAndVerified
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 nextIn 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.
func VerifyToTarget(primary PeerID, root LightBlock,
targetHeight Height) (LightStore, Result) {
lightStore = new LightStore;
lightStore.Update(root, StateVerified, root.verifiedBy);
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, nil)
}
// Verify
verdict = ValidAndVerified(lightStore.LatestVerified, current)
// Decide whether/how to continue
if verdict == SUCCESS {
lightStore.Update(current, StateVerified, lightStore.LatestVerified.Height)
}
else if verdict == NOT_ENOUGH_TRUST {
// 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, nil)
return (nil, ResultFailure)
}
nextHeight = Schedule(lightStore, nextHeight, targetHeight)
}
return (lightStore.FilterVerified, ResultSuccess)
}
ValidAndVerified
or FetchLightBlock
report an errorfunc ValidAndVerified(trusted LightBlock, untrusted LightBlock) Result
Height
and Time
of trusted
are smaller than the Height and
Time
of untrusted
, respectivelyunstrusted.Header
is the immediate
successor of trusted.Header
, then it holds that
SUCCESS
:
NOT_ENOUGH_TRUST
if:
----;
func Schedule(lightStore, nextHeight, targetHeight) Height
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.
Analogous to [001_published]
Analogous to [001_published]
The above specification focuses on the most common case, which also
constitutes the most challenging task: using the Tendermint security
model 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 there are two additional challenges:
it might be that some "older" header is needed, that is,
targetHeight < lightStore.LatestVerified(). The
supervisor checks whether it is in this
case by calling LatestPrevious
and MinVerified
and if so it calls
Backwards
. All these functions are specified below.
In order to submit proof of a light client attack, a relayer may
need to submit a verification trace. This it is important to
compute such a trace efficiently. That it can be done is based on
the invariant [LCV-INV-LS-ROOT.2] that needs
to be maintained by the light client. In particular
VerifyToTarget
and Backwards
need to take care of setting
verification-root
.
func (ls LightStore) LatestPrevious(height Height) (LightBlock, bool)
----;
func (ls LightStore) Lowest() (LightBlock)
----;
func (ls LightStore) MinVerified() (LightBlock, bool)
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:
func Backwards (primary PeerID, root LightBlock, targetHeight Height)
(LightStore, Result) {
lb := root;
lightStore := new LightStore;
lightStore.Update(lb, StateTrusted, lb.verifiedBy)
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 (nil, ResultFailure)
}
else {
// latest and current are linked together by LastBlockId
// therefore it is not relevant which we verified first
// for consistency, we store latest was veried using
// current so that the verifiedBy is always pointing down
// the chain
lightStore.Update(current, StateTrusted, nil)
lightStore.Update(latest, StateTrusted, current.Header.Height)
}
latest = current
}
return (lightStore, ResultSuccess)
}
[block] Specification of the block data structure.
[RPC] RPC client for Tendermint
[attack-detector] The specification of the light client attack 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].