tendermint-sol

December 16, 2021 ยท View on GitHub

Solidity implementation of IBC (Inter-Blockchain Communication Protocol) compatible Tendermint Light Client intended to run on Celo EVM (but not limited to it).

Features:

  • supports both adjacent/non-adjacent (sequential/skipping) verification modes
  • supports Secp256k1 (via ecrecover) and Ed25519 (Celo EVM precompile) curves
  • supports ics23 Merkle proofs
  • implements IBC interface via yui-ibc-solidity

The Light Client comes in two branches:

  • main - the code is a very close copy of the ibc-go light client
  • optimized - the code has been sufficiently optimized to fit the Celo block gas limit (20M) while keeping all functionalities

Light client in the nutshell

The light client ingests the block headers coming from the full node and verifies them. Once the verification succeeds, the light client will update its ConsensusState with:

  • next_validator_set_hash - hash of the next validator set stored in the verified block header
  • commitment_root (e.g., app hash Merkle root) - also stored in the block header.

We can verify the inclusion/exclusion in the Merkle tree with a valid proof and commitment root. For example, you can check if a transaction has been committed to transactions Merkle tree. But, how can you trust the commitment root? How do you know it has not been forged?

Both values are members of the block header, so we need to check whether a header is valid. The verification requires:

  • block header
  • commit signatures
  • validator set (validator voting power and public key)
  • trusted validator set (non-adjacent verification)

The core verification is quite simple. We build the Canonical structures with provided data, serialize them (with protobuf) and check via the cryptographic function (e.g., ed25519) if it matches the given signature. The validator set hash is checked against the ConsensusState prior signature verification to ensure the trust to validator set.

The light client offers two verification modes:

  • adjcacent / sequential - block heights are sequential e.g., n, n+1, n+2, ...
  • non-adjacent / skipping - block heights aren't sequential e.g., n, n+1, ..., n+6, n+7

Sequential mode is obvious, but when would one use the non-adjacent method?

Syncing up headers after some time (e.g., relayer was down) might be expensive because the light client must process all missing headers up to the latest one. With the non-adjacent mode, we can quick-sync to the latest height, but it requires a trusted_validator_set to be passed on additionally.

At the time of writing, the Cosmos Hub validator set contains 150 validators, so:

  • adjacent mode - requires 150 validator entries and 150 commit signatures
  • non-adjacent mode - requires 150 validator and 150 trusted validator entries + 150 commit signatures

To learn more about the light client theory, see this article

Performance analysis

The benchmark aims to gauge the gas usage across the Tendermint Light Client contract and help out to identify potential optimization areas.

There are a few segments/tests outlined:

  • all - test runs as is, no code is modified
  • no-precompile - the call to Ed25519 precompile is commented out. (all - no-precompile = gas spent on precompile)
  • no-check-validity - the checkValidity call is commented out. This is the starting point for LC core logic.
  • unmarshal-header - unmarshal the header in the CheckHeaderAndUpdateState and return.
  • early-return - the CheckHeaderAndUpdateState method returns as quickly as possible (no deserialization, storage etc)

Some of the segments can also be measured via unittests (see test/.*js).

Setup

  • celo blockchain node (v1.3.2)
  • block headers relayed from CosmosHub public node
  • TM Light Client compiled with 0.8.2 solidity compiler
  • code checked out at:
    • vanilla (8434ff68a7a90b1670a64ab36c7cdfc43a5ce1ad)
    • optimized (0a8acb90a8ef834e596538997859d6ee883dba97)

Running tests

The Rust Demo program relays four headers from the Tendermint RPC node (e.g., cosmos hub) and calls light client code, particularly CreateClient and CheckHeaderAndUpdateState. In the non-adjacent mode, the second header is being skipped.

cd test/demo

# adjacent mode
cargo run  -- --max-headers 4 --celo-gas-price 500000000 --celo-usd-price 5.20 --tendermint-url "https://rpc.atomscan.com" --gas 40000000 --celo-url http://localhost:8545 --from-height 8619996

# non-adjacent mode
cargo run  -- --max-headers 4 --celo-gas-price 500000000 --celo-usd-price 5.20 --tendermint-url "https://rpc.atomscan.com" --gas 40000000 --celo-url http://localhost:8545 --from-height 8619996 --non-adjacent-mode

Vanilla Client (branch: main)

header heightsmodesegmentGas (init)gas (h2)gas (h3)gas (h4)
8619996-8619999adjacentall359531164000331638049016404617
8619996-8619999adjacentno-precompile373215162935001627396016297936
8619996-8619999adjacentno-check-validity373215126349041261698412638759
8619996-8619999adjacentunmarshal-header359531122581091228648012308085
8619996-8619999adjacentearly-return373215479499524934525989
--------------
8619996-8619999non-adjacentall373215--2673446623511707
8619996-8619999non-adjacentno-precompile359531--2657797523394015
8619996-8619999non-adjacentno-check-validity359531--1938627719407358
8619996-8619999non-adjacentunmarshal-header373215--1899229019059787
8619996-8619999non-adjacentearly-return359531--640815688152

heightmodebase costserialization costcheck-validity costprecompile costtotalgas limitgas usage
8619997adjacent4794991177861037651291065331640003320M82.00 %
----2.923 %71.820 %22.958 %0.6495 %100 %----
8619998non-adjacent5249341846735673481891564912673446620M133.67 %
----1.963 %69.07 %27.485 %0.585 %100 %----

Optimized Client (branch: optimized)

header heightsmodesegmentGas (init)gas (h2)gas (h3)gas (h4)
8619996-8619999adjacentall373191126572901263857112662331
8619996-8619999adjacentno-precompile359507125600731254135512565112
8619996-8619999adjacentno-check-validity373191962713096099759631564
8619996-8619999adjacentunmarshal-header373191936493493476509369069
8619996-8619999adjacentearly-return359507418250463841464895
--------------
8619996-8619999non-adjacentall359507--1797639115550856
8619996-8619999non-adjacentno-precompile373191--1784358415450647
8619996-8619999non-adjacentno-check-validity359507--1244249712463389
8619996-8619999non-adjacentunmarshal-header359507--1217564912196539
8619996-8619999non-adjacentearly-return373191--518520565852

heightmodebase costserialization costcheck-validity costprecompile costtotalgas limitgas usage
8619997adjacent41825089466843030160972171265729020M63.28 %
----3.30 %70.68 %23.94 %0.768 %100 %----
8619998non-adjacent5185201165712955338941328071797639120M89.88 %
----2.88 %64.846 %30.784 %0.738 %100 %----

Results overview

By looking at the results, it's clear that:

  • protobuf deserialization costs are high. Umarshalling takes up to 70% in optimized client
  • core logic (check-validity) takes less than 30%
  • the signature verification via precompile is very cheap (compared to the rest)

The optimized branch removes unused fields from proto/TendermintLight.proto and flattens some structures such as PublicKey to reduce deserialization costs. As shown, the gas usage in non-adjacent mode was lowered from 26734466 to 17976391 (89.88% of max allowed gas).

The Light Client contract fits into Celo Blockchain, but running it may be expensive.

Potential optimizations:

  • serialization - the input data doesn't need to be protobuf serialized, so:
    • further protobuf structure unification/nesting removal
    • alternative (simpler) serialization format may be evaluated e.g., RLP
    • custom serialization - for example |validator_pub_key|voting_power| can be stored as one byte array
    • try out another protobuf compiler - maps, nested enums are not supported
  • removal of non-adjacent mode - if anticipated?

NOTE: gas limit (20M) is the maximum allowed gas per block on Celo blockchain mainnet (2021-12-16)

Quick Start

git clone https://github.com/ChorusOne/tendermint-sol.git
cd tendermint-sol && git checkout optimized

export NETWORK=celo

# deploy with truffle
make deploy

# run demo program (local celo node must be running)
cd test/demo
cargo run  -- --max-headers 4 --tendermint-url "https://rpc.atomscan.com" --gas 20000000 --celo-url http://localhost:8545 --from-height 8619996

Demo

asciicast