The next release of Tendermint introduces a new implementation of the Tendermint light client, and in this three-part article, we’ll tell you everything you need to know about it.
In Part 1, we’ll talk about what the light client is and why we need it. Part 2 will guide us through the core principles and algorithms used. In the final Part 3, we’ll touch upon the Go implementation.
Part 1: What is the light client
A light client is a lightweight (hence the name) alternative to a full node. Full nodes often are resource-heavy because they execute transactions and verify results (and do a lot of other stuff). Light clients, on the opposite, have low resource requirements since they only verify results (without executing transactions). Full nodes often store a lot of data (blocks, transaction results, etc.). Light clients only store a few latest headers.
A light client is a client, which connects to a full node, requests new block headers, and verifies that those headers can be trusted.
The concept of light clients was introduced in the Bitcoin white paper. In chapter 8, Satoshi describes a “simplified payment verification” method. The SPV (“light”) client connects to a full node and downloads new headers. Those headers can be trusted as long as they belong to the longest chain.
If you want to familiarize yourself more with the term and use-cases, check out this great article by Parity Technologies.
The existence of light clients forms the basis of safe and efficient state synchronization for new network nodes and inter-blockchain communication (or IBC, in short; where a light client of one chain instance runs in another chain’s state machine). To learn more about IBC, go to https://cosmos.network/ibc.
Part 2: Core principles and algorithms
2.1 Weak subjectivity
Now, how do we make sure that the first header we obtain from a full node can be trusted? Remember, we don’t have any prior knowledge except the consensus algorithm and genesis info (genesis.json file and genesis block #1).
We could fetch the genesis header and try to sync to the latest state from it, but that would be:
- Theoretically unsafe because it is costless for an attacker to buy up voting keys that are no longer bonded and fork the network at some point in its prior history (aka a long-range attack). Remember that when a new validator joins a Proof-Of-Stake network, it must put
Xamount of tokens into a special account (stake/bond them). If it does something bad, its stake will be slashed. When it decides to get its tokens out, it signals the network desire to unbond. After a certain amount of time (unbonding window), it gets the tokens back.
- Very slow (especially when validator set changes are frequent).
Instead, we get a header that is no older than one unbonding window ** from a trusted source and say that we trust it. This is called “weak subjectivity”.
** — minus a configurable evidence submission synchrony bound
The first header we get from a full node must be within the unbonding window because we want to be able to punish a validator if it gives us a maliciously constructed header. Otherwise, if there was no unbonding period, a validator (or a group of them) could construct a bad header and unbond immediately after.
A trusted source can be a validator, a friend, or a secure website. A more user-friendly solution with trust tradeoffs is to establish an HTTPS based protocol with a default endpoint that populates this information. Also, an on-chain registry of roots-of-trust (e.g., on the Cosmos Hub) seems likely in the future. (Please comment on GH#4422 if you have ideas on how to strengthen the security model here.)
Also, if you want to dive in deep, read Vitalik’s post at Proof of Stake: How I Learned to Love Weak Subjectivity.
When a new light client connects to the network or when a light client that has been offline for longer than the unbonding period connects to the network, it must supply the following options:
There are two methods to sync the light client from the trusted header to the latest state: sequential and skipping verifications. Let’s look at both of them in detail.
2.2 Sequential Verification
As you may have guessed, sequential verification verifies headers sequentially, or in order. Starting from the trusted header (
height: H), it downloads the next header (
height: H+1) and applies the Tendermint validation rules. All the way up until it reaches the latest header (
Let’s say we have four validators: A, B, C, and D, all with an equal stake — 25% of the total supply. Header #2 can be trusted if it’s signed by 2/3+ of the total supply (~66%). A, B and C account for 75% of the total supply, therefore header #2 is OK.
Note that the validator set can change 100% between two blocks (#2 and #3 above).
As long as
header2.NextValidatorSetHash == header3.ValidatorSetHash and 2/3+ of the new validator set signed header #3, it can be trusted.
Despite its simplicity, verifying headers sequentially is slow (due to signature verification) and requires downloading all intermediate headers.
2.3 Skipping Verification
Skipping verification is a less bandwidth and compute intensive mechanism that, in the most optimistic case, requires a light client to only download two block headers to come into synchronization. It’s the default method in the Go light client.
The algorithm proceeds in the following fashion: the light client fetches the latest header. If the validators from the trusted header account for more than 1/3+ of the stake in the latest header, the header is marked as verified ***.
*** — sequential verification is always used for adjacent headers
If this fails, then following bisection algorithm is executed:
The light client tries to download the block at the midpoint block between the latest height and the trusted height and attempts that same algorithm as above. In the case of failure, recursively perform the midpoint verification until success, then start over with an updated validator set and a trusted header.
Tendermint failure model assumes that there are no more than 1/3 of malicious validators at any point. By requiring 1/3+ of the stake coming from the trusted validators, the light client ensures that there’s at least one correct validator present in the new header.
If the Tendermint failure model does not hold and, during any point, malicious validators have 1/3 + of the total stake, they can try to fool light clients, which are using skipping verification. That’s why the light client additionally cross-checks each new header with witnesses. When somebody is trying to fool the light client, who has at least one correct witness, the attack will be detected and reported to all connected full nodes.
2.4 Fork Detection
When the header received from a witness does not match the new header and both of them have 1/3 + of the trusted validators, there are two possible options: either somebody is trying to attack this light client or there’s an actual fork on the main chain.
The light client then bundles two conflicting headers, sends them to all connected full nodes and returns an error.
See fork accountability spec for the list of attacks on a light client.
2.5 Backwards verification
When someone requests a header, which is older than the earliest trusted header, the light client performs backwards verification. Starting from the latest trusted header (
height: H), it downloads the previous header (
height: H-1) and checks that
trustedHeader.LastBlockID.Hash == prevHeader.Hash.
Say we started with header #2, but now header #1 is needed to verify some past transaction. The light client will fetch header #1 via RPC, check its hash matches with header #2
LastBlockID and save it to the trusted store.
In cases where an older header is found in the trusted store (e.g. requested header — #5, header in the trusted store — #1, latest header — #10), the light client performs skipping verification from the closest header in the store to the requested header.
Part 3. Using the Light Client
While the above algorithms may sound tricky, all the complexity is hidden and handled by the implementation.
3.1 Creating a light client
To create a new light client, you’ll need five things:
- chain ID;
- trust options — height & hash of the trusted header; one way to obtain the trusted height & hash right now is to query multiple nodes and compare results https://docs.tendermint.com/master/tendermint-core/light-client-protocol.html#where-to-obtain-trusted-height-hash (Please comment of GH#4422 if you have other ideas)
- primary — full node, which the most communication will be happening with;
- witnesses, which will be used for cross-checking new headers;
- trusted store — a permanent storage for trusted headers.
The options parameter allows you to tweak the light client. For example, you can switch to sequential verification by providing
SequentialVerification(). Or you can set a logger (
Logger(l)). Or set how many headers to store in the trusted store (
PruningSize(10)). Check out Option docs for more information.
NewClient will fetch the header at height
trustOptions.Height, check and save it to the trusted store.
3.2 Syncing to the latest state / Verifying headers
If you want to update the light client to the latest state, you can call
If you want to update the light client to the specific height, call
If you have a new header already and want to verify it, call
VerifyHeader(newHeader, newVals, time.Now())
If either a witness or the primary is not responding, it will be removed after a certain number of attempts (see MaxRetryAttempts option).
3.3 Light client HTTP(S) proxy
Tendermint comes with a built-in
tendermint light command, which can be used to run a light client proxy server, verifying Tendermint RPC. All calls that can be tracked back to a block header by a proof will be verified before passing them back to the caller. Other than that, it will present the same interface as a full Tendermint node.
$ tendermint light supernova -p tcp://18.104.22.168:26657 -w tcp://22.214.171.124:26657,tcp://126.96.36.199:26657 --height=10 --hash=37E9A6DD3FA25E83B22C18835401E8E56088D0D7ABC6FD99FCDC920DD76C1C57
For additional options, run
tendermint light --help .
Let us know what your experience has been so far in the chat! Are there any issues or potential improvements we should be aware of? Please leave a comment if we’ve missed anything. Thanks!
Appendix A: History of the light client implementations in Tendermint
The very first version of the light client was developed by Ethan Frey back in 2018. It had a bisection algorithm that tried to use a binary search to find the minimum number of block headers where the validator set voting power changes are less than 1/3. While it gave our users something working, it was theoretically unsafe.
For this reason, the work on the second version began in 2019, starting with a specification. The effort was primarily led by the team of researchers at Informal Systems. Go implementation was later developed by Anton Kaliaev and Callum Waters.
Appendix B: Security of the light clients
In the Bitcoin white paper, Satoshi writes:
As such, the verification is reliable as long as honest nodes control the network, but is more vulnerable if the network is overpowered by an attacker. While network nodes can verify transactions for themselves, the simplified method can be fooled by an attacker’s fabricated transactions for as long as the attacker can continue to overpower the network. One strategy to protect against this would be to accept alerts from network nodes when they detect an invalid block, prompting the user’s software to download the full block and alerted transactions to confirm the inconsistency.
The “Fraud and Data Availability Proofs: Maximising Light Client Security and Scaling Blockchains with Dishonest Majorities” white paper, which was published in Sept. 2018, describes how one can implement “alerts” (fraud proofs) to weaken the honest majority assumption and improve the security of light clients.
The Tendermint Light Client currently does not have fraud proofs (or data availability proofs for that matter). It relies solely on having at least one honest witness, which will tell it when/if the primary is cheating. A proposal (GH#4873) is already there to monitor the network for fraud proofs.
I want to thank Zaki Manian, Ethan Buchman, Zarko Milosevic, Josef Widder, Anca Zamfir for their contribution! Fantastic work everyone! 🙌
Thank you Erik Grinaker, Aleksandr Bezobchuk and Tess Rinearson for reviewing this article!