Draft. No backwards compatibility. No flags. No replay.
Closes the light-client item from LP-200 §"Future work". Composes on
LP-201's Kademlia DHT (Layer C) for chunk retrieval and on the P3Q
precompile family (LP-218 + LP-220) for local cert verification.
Activated at the genesis of the new final Lux network:
2025-12-25 16:20 Pacific (unix 1766708400). Predates every light-
client release. The pre-Quasar Edition Lux network (2020–2025) had no
P3Q precompile and no Kademlia DHT and is a separate network out of
scope.
A Lux light client is a stateless verifier. It downloads block
headers, validator-set transitions, and state proofs from the Kademlia
DHT (LP-201 Layer C). It verifies QuasarCerts locally using the P3Q
precompile family (LP-218 P3Q-Pulsar at slot 0x012205, LP-220 P3Q-
Corona at slot 0x012206, future LP-221 P3Q-Magnetar at slot
0x012207). It does not store full state. It does not run consensus.
It does not validate transactions. It verifies a single state value at
a single height, with full cryptographic finality, in under 100 ms on a
1 Gbps LAN link.
The mechanism: a state-root commitment lives in every block header. A
state proof — Verkle (KZG, ~200 bytes per proof) or Merkle Patricia
(~1 KiB per proof) — proves a specific (key, value) pair under that
state root. The client fetches header + proof via DHT parallel chunk
reconstruction (LP-201 §"Layer C"), verifies the QuasarCert on the
header locally via the appropriate P3Q precompile leg, verifies the
proof against the state root, and is done.
Storage requirements scale linearly with retained history. The minimum
is one current block header and one current validator set — under
200 KiB combined. Clients that retain headers for N years on a busy
chain (60 blocks/sec, 32 KiB/header on a Pulsar-strict chain) pay
~60 GiB per year, which is the upper bound; most clients retain only a
sliding window plus checkpoint snapshots and pay under 100 MiB
indefinitely. The mathematical floor is one block header — anything
beyond is a UX choice.
The trust model is minimal. The genesis bootstrap validator set is
signed N-of-N at chain creation; thereafter validator-set transitions
are themselves QuasarCert-verified by the light client at the heights
where they occur. DHT peers are cross-validated by content hash — a
peer that returns chunks that do not match the requested sha256
content hash is rejected, no reputation system needed. The light client
never trusts a single peer for any single byte.
Three separable use cases drive this LP. Each is independently
sufficient.
1. Mobile and embedded wallets. A phone wallet that displays a user's
balance for an asset on a tenant EVM cannot run a full Z-Chain
node. It needs to verify that "address X has balance Y" with full
cryptographic finality. Today this is done by trusting an RPC
endpoint. With a light client, the wallet downloads the current
block header + state proof + QuasarCert and verifies locally. No
trusted RPC.
2. Cross-chain bridges. A bridge contract on Chain A needs to
verify that an event was committed on Chain B. The bridge runs a
light client of Chain B inside Chain A's EVM. Each state proof
from Chain B is verified by a P3Q precompile call on Chain A. No
trusted relayer.
3. In-browser inspector. A block explorer that lets a user
independently verify "this transaction is final on a tenant
EVM, parent L1 finality verified" without round-tripping to any
trusted indexer. The verification runs in the browser via wasm-
compiled Pulsar/Corona verifier cores.
In all three, the unifying requirement is: **fetch a small amount of
data, verify locally with no consensus participation, return a yes/no
answer about a state value.**
The light client trusts exactly two things:
Chain genesis fixes an initial validator set, signed N-of-N by the
initial signing keys. This signature is embedded in the genesis block
and is checked once at first sync. Thereafter, validator-set transitions
are governed on-chain (LP-204 Tier-1 validator rotation) and the light
client tracks them by verifying the QuasarCert that finalised each
transition.
The light client is therefore as secure as the chain it tracks, with
one exception: **a light client that joins after chain genesis without
verifying every intermediate validator-set transition is trusting that
the latest validator set is a legitimate descendant of the genesis
set.** This is solved by either (a) verifying every transition from
genesis (linear in chain age — feasible on a fast chain), or (b)
accepting a checkpoint from a trusted source at first sync (the
operator-policy "long-range attack" tradeoff).
LP-214 specifies option (a) as the default — the light client MUST
verify every validator-set-transition QuasarCert from the genesis block
forward. Option (b) is an opt-in for clients that cannot afford the
linear sync time; it is operator policy, not protocol.
Kademlia DHT (LP-201 Layer C) is content-addressed: every value is keyed
by sha256(value). A peer that returns a value whose hash does not
match the requested key is silently rejected. The light client fetches
each chunk in parallel from multiple peers (LP-201 §"Layer C"
parameters: bucket size k=20, concurrency α=3) and accepts the first
chunk whose hash matches the request. Collusion is detected by a single
honest peer returning the correct chunk; the DHT need not be majority-
honest, it need only be content-addressed.
The light client does NOT trust:
and are themselves QuasarCert-finalised)
A light client at steady state carries:
The 60 TiB number is the upper bound for a client that retains
every header from a busy chain (busy-tenant projection:
60 blocks/sec × 32 KiB each). No real client does this.
Three retention modes the light client SHOULD support:
The light client also retains, per active query, one state proof. State
proofs are pinned for the lifetime of the query and then garbage
collected. A client that issues 100 concurrent queries against a 60-byte
Verkle proof + 32-byte key + 32-byte value keeps ~12 KiB of proof data
hot.
For the typical mobile-wallet workload — verify your own balance at
each app foreground — the steady-state footprint is under 2 MiB.
Two schemes are specified. The chain picks one at genesis; mixed schemes
within a chain are not supported.
Verkle trees use KZG polynomial commitments. Properties:
(key, value) pair |Verkle is the lightweight choice. A PQ-augmented Verkle scheme is out
of scope for LP-214; if/when one is standardised, LP-215 (or similar)
adds it. For now: **a chain using Verkle has Verkle-classical state
proofs and Polaris-PQ block-cert proofs**. The cert protects the state
root; the state-proof verifier needs to be trusted only as far as the
KZG SRS is trusted, which is the same trust model as Ethereum's Verkle
deployment.
Merkle Patricia trees are the Ethereum-compatible scheme. Properties:
(key, value) pair (worst case 32 nibbles × 32-byte siblings = 1024 B + branch hashes) |Merkle is the conservative choice for EVM-compatible chains. Most
tenant EVMs use Merkle Patricia today. The Lux primary network may
migrate to Verkle at an activation marker yet TBD.
A chain declares its state-proof scheme in its genesis block:
GenesisBlock {
...
StateProofScheme uint8 // 0x01 = Merkle Patricia, 0x02 = Verkle
...
}
Light clients dispatch on this byte at first sync. There is no
backwards-compatibility transition path — a chain that switches schemes
mid-life forks. This is intentional; backwards-compatibility hurts more
than it helps for state-proof verification, which is hot-path on every
single query.
End-to-end verification of "value of key K at block height H":
1. Request block header at height H
client → DHT FindValue(contentHash = sha256("hdr/" || chainID || H))
← block-header chunks, parallel from α=3 peers, content-hash verified
2. Verify QuasarCert on the header
client → P3Q-Pulsar precompile @ 0x012205 (in-process, GPU if available)
← bool
For PQ-strict chains: also verify Corona leg via P3Q-Corona @ 0x012206
For PQ-heavy chains: also verify Magnetar leg via P3Q-Magnetar @ 0x012207
For PQ-off chains: verify BLS leg (classical, no P3Q needed)
3. Verify validator-set transition chain back to genesis
client → already-cached validator-set transitions
← (run once at first sync; thereafter only re-verify on new transitions)
4. Request state proof for key K under the state root in the header
client → DHT FindValue(contentHash = sha256("proof/" || chainID || H || K))
← state-proof bytes (Verkle ~200 B or Merkle ~1 KiB)
5. Verify proof against state root from step 1
client → in-process Verkle/Merkle verifier
← (value, bool)
6. Done. Return (value, verified=true) to caller.
Every step is independently complete. Steps 1-2 establish header
finality. Step 3 establishes validator-set legitimacy. Steps 4-5
establish state-value membership. No step braids into another.
The P3Q precompile cost varies by leg. Measured on Blackwell sm_120
(LP-203 GPU verify bench) and on M-series CPU:
Total cert verify on PQ-heavy mode per LP-217 (BLS + Corona + Pulsar +
Magnetar; was "Polaris" in LP-017) on CPU: ~9.2 ms. On Blackwell:
~1.1 ms.
For a wallet on a phone (CPU only), the dominant cost is the Corona
leg. A wallet that pins CertModeFloor = PQ-fast (LP-217) verifies
BLS + Pulsar only: ~6 ms on CPU. A wallet that pins
CertModeFloor = strict-PQ-fast (drops BLS) verifies Pulsar only:
~80 μs on CPU. Operator-policy choice; protocol does not constrain.
The catch-up latency budget for a cold-start light client on a 1 Gbps
LAN link to ~3 healthy DHT peers:
All four totals are under 100 ms. The 100 ms target is met with margin
on every supported configuration on both server-class and laptop-class
hardware. The mobile case (slower CPU, ~3-5× slowdown) brings PQ-fast
on CPU to ~45 ms, still under budget.
The wide-area network case is dominated by RTT, not by cryptography.
A WAN with 50 ms one-way latency adds ~50 ms × 3 (handshake + two DHT
lookups) = ~150 ms of network time, putting cold-start over the 100 ms
target. For WAN clients the budget is sub-200 ms, not sub-100 ms.
The 100 ms target is a LAN/edge target.
A single peer at 1 Gbps delivers a 1 MiB validator set in 8 ms. Ten
peers in parallel deliver chunks of the same set such that the *slowest*
chunk dominates — not the *sum* of chunks. The end-to-end fetch time
is bounded by the slowest of the parallel streams, which on a populated
DHT is the median peer's latency, not the worst peer's latency. This
is the LP-201 §"Layer C" content-addressed parallel fetch model. A
chunk that does not arrive (timeout) is silently replaced by another
peer's chunk; the failed peer is decremented in the routing table.
The light client thus benefits from DHT health automatically: more
peers = lower tail latency. Below ~5 peers the model degrades; below
3 the parallel-fetch optimisation is lost and the client falls back to
serial fetch from whatever peer is reachable. The LP-201 minimum DHT
size (10 peers for healthy operation) is assumed throughout.
LP-214 introduces three new schema IDs in the 0xF0..0xF2 range. This
range is allocated cleanly per the LP-200 schema-ID map: 0xC0..0xCB
chains-VM (LP-186), 0xD0..0xDF P2P (LP-201), 0xE0..0xE1 DAG
(LP-208), 0xE2..0xE7 cross-shard (LP-211), 0xE8..0xEF reserved
future, 0xF0..0xF2 light-client (this LP), 0xF3..0xFF reserved
future. No collision with any existing allocation.
> Note for the LP-208 author: the LP-208 spec file still lists
> DAGHeader at 0xF0 and DAGBody at 0xF1 — this is stale. The canonical
> session handoff at /tmp/SESSION-HANDOFF.md §"Schema ID Allocation"
> overrode LP-208 to 0xE0..0xE1 to avoid the LP-218 rollup-VM
> collision. LP-214 takes 0xF0..0xF2 per the canonical map. LP-208
> needs a follow-up edit to bring its file into line with the handoff.
0xF0 | LightClientBlockHeaderRequest | 0x50 | Light client → peer: fetch block header at given height (or by content hash) |0xF1 | LightClientStateProofRequest | 0x51 | Light client → peer: fetch state proof for (blockHeight, key) |0xF2 | LightClientCertProof | 0x52 | Peer → light client: fetched cert + state proof bundle (response wrapper) |Fixed-section layout:
0x50 |0x01 |Total fixed payload: 51 bytes.
Fixed-section layout:
0x51 |0x01 |0x01 Merkle Patricia, 0x02 Verkle |Total fixed payload: 83 bytes.
Server's response carrying header + cert + state proof in one envelope.
Fixed-section layout:
0x52 |0x01 |Total fixed payload: 36 bytes.
Variable tail: block-header bytes (LP-186 block-VM wire), QuasarCert
bytes (LP-182 QuasarCert wire), optional state-proof bytes (Merkle or
Verkle, per scheme), optional key-value bytes.
LP-214 composes; it does not braid.
ChunkRequest, 0xDE DHTFindValue, 0xDF DHTStore |CertTier field of response |CertMinTier per request | LP-214 §"Wire schemas" CertMinTier field |The light client does NOT compose with:
The light client is engineered for graceful degradation. The failure
modes are enumerated; for each, the recovery is specified.
A malicious peer returns a block header that is well-formed but whose
QuasarCert does not actually attest to the block content (e.g., the
peer crafts a header with a different state root and signs it with a
forged sig).
Recovery: The QuasarCert verify step (§"Verification path" step 2)
fails because the forged sig does not validate under the legitimate
validator set's group public key. The client rejects the chunk and
retries from another peer. No special protocol needed; cryptographic
verification IS the recovery.
A coordinated set of peers all return the same forged header (same
sha256, all peers in agreement). The content-hash check passes because
all peers agree. The QuasarCert check still catches it because the
forgery cannot produce a valid Pulsar / Corona / Magnetar signature
without compromising the actual validator set's private keys.
Recovery: Cert verify rejects. Client retries via different DHT
buckets (Kademlia routing-table diversity). If the entire DHT is
poisoned, the client falls back to mDNS LAN bootstrap peers (LP-201
Layer B); if those are also compromised, the client is operating in a
fully adversarial network and cannot proceed — this is the assumed
upper bound for any cryptographic protocol and not a failure mode
LP-214 attempts to recover.
An attacker presents a valid old block header with a valid old state
proof for a now-stale key (e.g., a token transfer that has since been
overridden by a later transfer).
Recovery: The client embeds BlockHeight in the
LightClientCertProof response and the caller must verify the proof
is at-or-after the height of interest. For "what is the current
balance" queries, the client requests the latest finalised header
(BlockHeight = 0 in the request schema means "latest"), and the
server's response carries the actual latest height. The client
verifies that the returned height is monotonically ≥ its previously-
seen height; a regression signals adversarial caching and the response
is rejected.
For "what was the balance at height H" queries, the client supplies a
specific height and the response is verified at that height — replay
of an old proof at the requested height is the correct answer, not an
attack.
An attacker presents a recent block header but skips the validator-set
transitions that occurred between the client's last-known height and
this header.
Recovery: The client always re-verifies the chain of validator-set
transitions before accepting a new header signed by a new validator
set. The header's ValidatorSetEpoch field (part of LP-182
QuasarCert) must match a validator set the client has independently
verified. A mismatch triggers a "fetch all missing validator-set
transitions" sub-protocol: the client requests every transition
QuasarCert from its last-known epoch up to the new epoch, verifies
each in sequence, and only then accepts the new header.
This is the only sub-protocol the light client runs. Everything
else is one-shot request/response.
A peer accepts the request but never returns the chunk.
Recovery: LP-201 §"Layer C" specifies α=3 concurrent peer queries
and a per-peer timeout. A timed-out peer is dropped from the active
query and another peer from the routing table is contacted. The
client's catch-up budget includes this redundancy — the published
sub-100 ms LAN figure assumes α=3 healthy peers, and degrades
gracefully (linearly in α) below that.
A consequence question: does the light client need its OWN cert mode?
For example, on a chain operating at PQ-strict, can a light client
choose to verify "PQ-fast only" if it only needs partial finality
guarantees for low-value queries?
**Decision: yes, the light client carries its own CertMinTier field,
independent of the chain's operating mode.** This is the
CertMinTier byte in LightClientBlockHeaderRequest (§"Wire
schemas").
The semantics:
with CertMinTier = PQ-strict), the server returns an error and
the client must either downgrade its requirement or wait.
This is consistent with LP-217 cert modes and LP-202 tier degradation
— the chain's cert tier is the ceiling, and the light client
chooses any floor at or below the ceiling. A wallet looking up its
balance for display can accept PQ-off (BLS-only) on a chain running
PQ-strict, getting ~5 ms response time and BLS-classical security. A
custody operation against the same chain pins `CertMinTier = PQ-
strict` and gets full PQ-heavy finality (per LP-217; was "Polaris" in
LP-017) at ~15 ms response time. Same chain, same data, different
security/latency tradeoffs at the verifier side.
The protocol-level point: **PQ-strict at the validator does not force
PQ-strict at the light client.** Operator-policy choice; the protocol
provides the mechanism, not the policy.
Measured under LP-201 assumptions (1 Gbps LAN, healthy DHT with 10+
peers, Pulsar-strict chain on Blackwell-class validators):
The warm-query case is the steady-state UX target. A wallet that has
recently fetched a header keeps the QuasarCert cached for the lifetime
of that header's epoch and only fetches new state proofs as the user
issues queries — ~5 ms per query on Blackwell, ~30 μs per query on
Merkle-Patricia + CPU.
The light client does NOT maintain a mempool, an execution state, a
fork-choice tree, or a peer-reputation system. It maintains:
via QuasarCert)
That is the whole client state. This is what makes "sub-100 ms cold
start" possible — the client has no warm-up phase to perform.
A chain declares its scheme at genesis and does not change. The
verifier code path is dispatched once per chain on first sync; there
is no runtime mode selection per query. Mixed schemes within a chain
would require dispatch overhead on every state proof and is rejected
by LP-214.
Some chains use compact-proof schemes (e.g., RLP-encoded proof nodes
with shared prefixes elided). LP-214 mandates the full proof —
every sibling, every prefix. The size overhead (a few hundred bytes
per proof) is dwarfed by network RTT; the simplification in client
verifier code (no prefix-reconstruction step) is worth it. Honest
tradeoff: ~30% larger proofs, ~50% simpler client code.
The long-range-attack class of vulnerability is solved by verifying
every validator-set transition from genesis. This costs linear-in-
chain-age time at first sync. On a chain with daily epochs, a 5-year-
old chain is ~1825 transitions × ~1.1 ms each = ~2 seconds of one-
time verify work. This is paid once at first sync; subsequent syncs
incremental from last-known epoch.
A "trusted checkpoint" opt-in is permitted for clients that cannot
afford the linear sync time (e.g., a freshly-installed phone wallet
joining a 10-year-old chain). The checkpoint is operator policy —
typically the chain operator publishes a recent validator-set hash
that wallets can pin at install time. LP-214 specifies the trust-from-
genesis path as the default; the checkpoint shortcut is an opt-in.
A full node serving light-client requests consumes bandwidth and CPU.
There is no protocol-level micropayment, no light-client-pays-full-
node scheme. The expectation is that full nodes serve light clients
as a public good (analogous to Ethereum's eth/68 light protocol). If
this proves operationally insufficient at scale, a future LP can add
an incentive layer; LP-214 does not.
The DHT model (LP-201) makes this less painful than it might be: the
full node's marginal cost per light-client query is dominated by one
hash lookup + one ZAP buffer send, not by any computation. Bandwidth
is the constraint, not CPU.
~/work/lux/lightclient/ (new package) | Pure-Go light client, headerly + state-proof verify |~/work/lux/lightclient/wasm/ | Browser-loadable wasm bundle for in-browser inspectors |~/work/lux/lightclient/ffi/ | C-callable interface for mobile wallets |~/work/lux/lightclient/verkle/ | Verkle proof verifier (reuses luxfi/kzg) |~/work/lux/lightclient/mpt/ | Merkle Patricia verifier (no shared code with full-node MPT) |~/work/lux/lightclient/p3q/ | Calls into the LP-218 / LP-220 / LP-221 verifier cores |~/work/lux/zap/v2/codegen/schemas/lp-214/ | ZAP v2 codegen declarations for 0xF0..0xF2 |The reference implementation lives in ~/work/lux/lightclient/. It is
a NEW package; there is no v1 light client to migrate. The
implementation MUST be byte-equality tested against a full-node-
computed reference: for any (chain, height, key) tuple, the light
client's verified value MUST equal the full node's stored value.
The light client conformance tests live at
~/work/lux/lightclient/conformance/ and exercise:
TestColdSync_Genesis | Light client cold-starts on a fresh chain (genesis only), verifies the genesis validator set |TestSync_ValidatorSetTransition | Light client tracks N validator-set transitions back-to-back, each via QuasarCert |TestStateProof_Verkle_Verify | Verkle proof for a known (key, value) verifies under a known state root |TestStateProof_Merkle_Verify | Merkle Patricia proof for a known (key, value) verifies under a known state root |TestQuasarCert_PQOff_Reject | Header with PQ-off cert rejected when client pins CertMinTier = PQ-fast |TestDHTPeerCollusion_ContentHashMismatch | Light client rejects a chunk whose sha256 does not match request hash |TestDHTPeerForgedCert | Light client rejects a chunk whose QuasarCert is forged (valid sha256 but invalid sig) |TestValidatorSetSkip_FetchMissingEpochs | Light client triggers epoch-fill sub-protocol when a header references an unknown validator set |TestColdStart_Latency_LAN | Cold-start time < 100 ms on a 1 Gbps LAN setup with α=3 DHT peers |TestWarmQuery_Latency_LAN | Steady-state per-query time < 10 ms on warm cache |All tests run in the standard go test ./... harness and are gated by
-tags=lightclient_conformance to keep CI cycle time low on routine
runs.
activates: 2025-12-25T16:20:00-08:00
activates-unix: 1766708400
The light client wire schemas (0xF0..0xF2) are accepted by the DHT and
by full-node responders from the genesis block of the new final Lux
network. There is no pre-activation light-client implementation; it is
NEW from genesis.
CertMinTier selection)Copyright and related rights waived via CC0.