Awesome
tendermint-sol
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 headercommitment_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
)
- vanilla (
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 heights | mode | segment | Gas (init) | gas (h2) | gas (h3) | gas (h4) |
---|---|---|---|---|---|---|
8619996-8619999 | adjacent | all | 359531 | 16400033 | 16380490 | 16404617 |
8619996-8619999 | adjacent | no-precompile | 373215 | 16293500 | 16273960 | 16297936 |
8619996-8619999 | adjacent | no-check-validity | 373215 | 12634904 | 12616984 | 12638759 |
8619996-8619999 | adjacent | unmarshal-header | 359531 | 12258109 | 12286480 | 12308085 |
8619996-8619999 | adjacent | early-return | 373215 | 479499 | 524934 | 525989 |
-- | -- | -- | -- | -- | -- | -- |
8619996-8619999 | non-adjacent | all | 373215 | -- | 26734466 | 23511707 |
8619996-8619999 | non-adjacent | no-precompile | 359531 | -- | 26577975 | 23394015 |
8619996-8619999 | non-adjacent | no-check-validity | 359531 | -- | 19386277 | 19407358 |
8619996-8619999 | non-adjacent | unmarshal-header | 373215 | -- | 18992290 | 19059787 |
8619996-8619999 | non-adjacent | early-return | 359531 | -- | 640815 | 688152 |
height | mode | base cost | serialization cost | check-validity cost | precompile cost | total | gas limit | gas usage |
---|---|---|---|---|---|---|---|---|
8619997 | adjacent | 479499 | 11778610 | 3765129 | 106533 | 16400033 | 20M | 82.00 % |
-- | -- | 2.923 % | 71.820 % | 22.958 % | 0.6495 % | 100 % | -- | -- |
8619998 | non-adjacent | 524934 | 18467356 | 7348189 | 156491 | 26734466 | 20M | 133.67 % |
-- | -- | 1.963 % | 69.07 % | 27.485 % | 0.585 % | 100 % | -- | -- |
Optimized Client (branch: optimized)
header heights | mode | segment | Gas (init) | gas (h2) | gas (h3) | gas (h4) |
---|---|---|---|---|---|---|
8619996-8619999 | adjacent | all | 373191 | 12657290 | 12638571 | 12662331 |
8619996-8619999 | adjacent | no-precompile | 359507 | 12560073 | 12541355 | 12565112 |
8619996-8619999 | adjacent | no-check-validity | 373191 | 9627130 | 9609975 | 9631564 |
8619996-8619999 | adjacent | unmarshal-header | 373191 | 9364934 | 9347650 | 9369069 |
8619996-8619999 | adjacent | early-return | 359507 | 418250 | 463841 | 464895 |
-- | -- | -- | -- | -- | -- | -- |
8619996-8619999 | non-adjacent | all | 359507 | -- | 17976391 | 15550856 |
8619996-8619999 | non-adjacent | no-precompile | 373191 | -- | 17843584 | 15450647 |
8619996-8619999 | non-adjacent | no-check-validity | 359507 | -- | 12442497 | 12463389 |
8619996-8619999 | non-adjacent | unmarshal-header | 359507 | -- | 12175649 | 12196539 |
8619996-8619999 | non-adjacent | early-return | 373191 | -- | 518520 | 565852 |
height | mode | base cost | serialization cost | check-validity cost | precompile cost | total | gas limit | gas usage |
---|---|---|---|---|---|---|---|---|
8619997 | adjacent | 418250 | 8946684 | 3030160 | 97217 | 12657290 | 20M | 63.28 % |
-- | -- | 3.30 % | 70.68 % | 23.94 % | 0.768 % | 100 % | -- | -- |
8619998 | non-adjacent | 518520 | 11657129 | 5533894 | 132807 | 17976391 | 20M | 89.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