Skip to content

[runtime, tesseract]: Kaia consensus client - #1098

Draft
seunlanlege wants to merge 1 commit into
mainfrom
seun/kaia-consensus-client
Draft

[runtime, tesseract]: Kaia consensus client#1098
seunlanlege wants to merge 1 commit into
mainfrom
seun/kaia-consensus-client

Conversation

@seunlanlege

Copy link
Copy Markdown
Member

Draft — parked pending Kaia's Permissionless hardfork (see Why this is a draft below).

Adds a light client for Kaia (ex-Klaytn, mainnet chain id 8217, Kairos 1001), an EVM chain finalized instantly by Istanbul BFT.

How Kaia finality works

~33 known companies (the Governance Council) take turns proposing blocks and signing off on each other's. A block only exists once roughly two-thirds have committed to it, so there are no reorgs and no confirmations — 1s blocks, final on arrival.

Every header carries its own finality artifact in extra_data: the proposer's secp256k1 seal plus the committee's committed seals. All plain ecrecover, no BLS. Verification mirrors the chain's own import check in blockchain/block_validator.go:

  • the recovered author must be a council member
  • 2f + 1 distinct council members must have committed, f = ceil(min(|council|, committee_size)/3) - 1

Mainnet today: 33 council members, blocks carry 22 seals, 21 required. State commitments are standard MPT, so state proofs reuse EvmStateMachine unchanged.

What's here

crate role
modules/consensus/kaia/verifier no_std verifier — header RLP, Istanbul extra, seal recovery, council tracking
modules/consensus/kaia/prover update assembly over the kaia_* RPC namespace
modules/ismp/clients/kaia ConsensusClient impl (KAIA)
tesseract/consensus/kaia relayer host, config type = "kaia"

Plus registration in both runtimes, the tesseract consensus config, the fisherman's exhaustive match, the EVM registry, and relayer docs.

Things worth a reviewer's attention

The proposer seal is a double hash. Istanbul's GetSignatureAddress keccaks its payload, so the seal signs keccak256(sig_hash), not sig_hash. Recovering with the single hash yields a plausible-looking wrong address. Pinned by a fixture test.

Kaia headers are not Ethereum headers. No uncles, difficulty, gas limit or nonce; they add rewardbase, block_score, timestamp_fos, governance and vote. The geth-primitives header cannot reproduce Kaia block hashes, hence the separate type.

Validator votes have three encodings and the chain rejects none of them. Besides a single address and concatenated addresses, any other length is canonicalized via BytesToAddress — the last 20 bytes, left-padded — with no hex decoding. Mainnet blocks 75038594 and 90897408 carried the 42-byte ASCII text of an address, and the address that yields (0x6332386239...) is in the canonical council to this day. A parser that rejects these silently diverges from the chain. Both blocks are pinned as fixtures.

Governance parameters are read from live state, not genesis. istanbul.committeesize is 22 in the genesis config and 50 on-chain today; the governing node moved too. The client tracks both from ratified epoch-block governance.

Known limitations

Council freshness is a trust assumption. Updates skip blocks — Kaia produces 86,400 a day, so following every one would cost ~122 MB/day of ancestry — and no header proves the blocks before it carried no votes. Whoever assembles updates is trusted to include every vote-bearing header.

Drift in the dangerous direction is caught rather than trusted: a header declaring a validator the client doesn't know proves a skipped addvalidator (which would understate the quorum), and is rejected. A skipped removevalidator is not detectable from headers.

The quorum denominator is conservative. The chain derives f from qualified validators (council minus those below the 5M KAIA minimum stake); that's staking state, invisible to a header-only client, so the council size stands in. It's never smaller, so the client can only demand more seals than the chain — but blocks carry ~ceil(2N/3), so with 33 council / 22 seals / 21 required the margin is one. Roughly three demotions would stall the client until its state is refreshed.

Why this is a draft

Kaia's Permissionless hardfork retires most of this. Post-fork the chain enforces that a header's declared validator list matches state (kaiax/valset/impl/consensus.go — the check exists today but returns early), which turns the list from an unchecked hint into a consensus-validated claim. A quorum signature then is a quorum attestation of the roster, so the client reads it off the header: no vote parsing, no epoch tracking, no ancestry, and the quorum denominator becomes exactly right since qualified = committee post-fork. Both limitations above dissolve, and the verifier gets smaller.

It is unscheduledPermissionlessCompatibleBlock is nil with a TODO on both mainnet and Kairos as of kaiachain/kaia@d6eb00a, and the latest release schedules only Osaka. If it activates before an upgrade the client fails closed: seals recover to non-council addresses and every header is rejected, so it stalls rather than accepting anything unsound. Kairos getting a fork block is the signal to start the rewrite.

Testing

37 unit and fixture tests. The fixtures are captured mainnet responses, so the RLP layout, block hash, both seal preimages and the historical vote blocks are pinned offline — silent drift fails the build.

cargo test -p kaia-verifier

A live suite verifies real mainnet finality end to end (bootstrap a trusted state from RPC, then verify sequential and batched updates):

cargo test -p kaia-prover -- --ignored

One caveat found while writing these: kaia_getBlockWithConsensusInfoByNumber's committers field is not a reliable oracle — it returns different sets for the same finalized block across calls (a stale signature-address cache). An independent eth_keys recovery agreed with this implementation, not with the node. The tests assert against the seals themselves.

Not included

Contracts aren't deployed on Kaia yet, so there's no EvmHosts entry, chain.ts entry, or indexer wiring. Also outstanding: testsuite-runtime registration, a CI job, and integration tests.

Adds a light client for Kaia (kaiachain/kaia, ex-Klaytn), an EVM chain
finalized instantly by Istanbul BFT.

Every Kaia header carries its own finality artifact in extra_data: the
proposer's secp256k1 seal plus the committee's committed seals, all plain
ecrecover. Verification mirrors the chain's own import check in
blockchain/block_validator.go — the author must be a council member, and
2f+1 distinct council members must have committed, where
f = ceil(min(|council|, committee_size)/3) - 1. Kaia uses standard MPT
state commitments, so state proofs reuse EvmStateMachine unchanged.

The council is tracked from governance.addvalidator / removevalidator
votes carried in header vote fields, and the committee size / governing
node from governance ratified at epoch blocks. Vote values follow Kaia's
canonicalizer exactly, including the form where the value is the ASCII
text of an address rather than its bytes — mainnet blocks 75038594 and
90897408 did that, and the address it yields is in the canonical council.

Council freshness is a trust assumption on whoever assembles updates:
updates skip blocks (Kaia produces 86,400 a day) and no header proves the
blocks before it carried no votes. Drift in the dangerous direction is
caught rather than trusted — a header declaring a validator the client
does not know proves a skipped addvalidator, and is rejected. Kaia's
Permissionless hardfork makes the header's validator list consensus
enforced, which retires this machinery entirely; it is unscheduled on
both mainnet and Kairos, and the client fails closed if it activates
before an upgrade.

- modules/consensus/kaia/verifier: no_std verifier. Kaia header RLP (no
  uncles, difficulty, gas limit or nonce; adds rewardbase, block_score,
  timestamp_fos, governance and vote), Istanbul extra parsing, the
  filtered-header hashes, and seal recovery. Note the proposer seal signs
  keccak256(sig_hash) — Istanbul hashes the payload a second time inside
  its signing helper.
- modules/consensus/kaia/prover: assembles updates over the kaia_* RPC
  namespace, which the eth_* block queries cannot serve since they omit
  the governance header fields. Reconstructed headers are checked against
  the node's reported block hash before use.
- modules/ismp/clients/kaia: the ConsensusClient impl.
- tesseract/consensus/kaia: relayer host, config type = "kaia".

Tests pin the RLP layout, both seal preimages and the historical vote
blocks against captured mainnet data offline; a live suite runs with
cargo test -p kaia-prover -- --ignored.
@seunlanlege
seunlanlege force-pushed the seun/kaia-consensus-client branch from 2455799 to 7c3ddce Compare August 4, 2026 09:19
@seunlanlege seunlanlege changed the title [runtime]: Kaia consensus client [runtime, tesseract]: Kaia consensus client Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant