Light Client
A Sahyadri client that verifies state without downloading the chain — the practical face of "the present proves the past."
Overview
A light client is a Sahyadri participant that holds only:
- A recent block header it trusts as an anchor
- A rolling window of headers used to extend that trust forward as the chain grows
- State proofs for the accounts it cares about
It does not store the chain. It does not replay history. It does not download block bodies or the SMT. It verifies state against a root, using proofs, in O(log n) cryptographic operations per account.
Full Node vs Light Client
| Property | Full Node | Light Client |
|---|---|---|
| Storage | GBs, grows with chain length | MBs, bounded by header window + queried accounts |
| Bandwidth | Full blocks, full DAG | Headers + state proofs on demand |
| Sync time | Hours to days | Seconds to minutes |
| Verifies | Everything | Only what it queries |
| Trusts | Nothing but PoW + ghostDAG rules | A recent header + continued PoW on top of it |
| Runs on | Server, desktop | Mobile, browser, IoT |
Trust Model
The light client's trust is bounded and explicit. Nothing is trusted implicitly.
Header Chain Verification
Before a header can serve as a trust anchor, the client must check that it is a genuine extension of the chain it already trusts. The steps are:
- Locate parent. The new header must declare a parent that already exists in the client's trusted header window. A header whose parent is unknown is rejected.
- Check PoW. The header's proof-of-work must meet the target derived from its parent's difficulty window, following the same rules a full node applies.
- Check blue work. GHOSTDAG's blue work metric must strictly increase along the chain. A header that rolls back work is rejected.
- Check timestamp. The header's timestamp must be consistent with its parent's and within the client's tolerance.
These four checks are the full extent of what a light client trusts the network for. Everything below them — every balance, every account state — is checked against the header's account_commitment using SMT proofs, which are self-authenticating.
Sync Flow
Obtain an Anchor Header
Either a bundled checkpoint or a header chain verified back to a known ancestor. The client stores this header and its account_commitment as the initial trusted root.
Extend the Window
As new headers arrive, the client verifies each against its parent, checking PoW, blue work, and timestamp. Newly accepted headers are appended to a rolling window; the oldest are dropped once enough confirmations accumulate.
Pick a Target Header
For each account query, the client picks a header from its window — usually the deepest confirmed one — and treats that header's account_commitment as the target root.
Request State Proof
For each account it cares about, the client requests a proof from any peer via getStateProof(account, block_hash).
Verify Locally
The client recomputes the root from the proof. If it matches the header's account_commitment, the account state is authentic. Otherwise, the client drops the peer and tries another.
Cache and Reuse
Verified state is cached until the client advances its anchor. Subsequent queries against the same root reuse the same verification. Storage grows with the number of distinct accounts queried, not with chain length.
Storage Requirements
| Data | Size | Notes |
|---|---|---|
| Header ring buffer | ~1 MB | Last ~10,000 headers, pruned as the anchor advances |
| Per account cached | ~200 bytes | SPK, balance, flash count |
| Per account proof | ~1 KB transient | Not persisted after verification |
| Peer list + reputation | ~50 KB | Replaced periodically |
| 100 accounts, 1 year | < 5 MB | Independent of chain length |
| 10,000 accounts, 5 years | ~50 MB | Still bounded |
For comparison, a full node that has processed the same period of activity would store orders of magnitude more data. This is the "history debt" that light clients eliminate.
Code Example — Header Verification
The critical piece is accepting a new header only if it correctly extends a trusted one:
pub fn verify_header_chain(
cached: &HeaderRingBuffer,
new_header: &Header,
) -> Result<(), HeaderError> {
// 1. Parent must already be in the trusted window
let parent = cached
.get(&new_header.selected_parent)
.ok_or(HeaderError::UnknownParent)?;
// 2. PoW must meet the target derived from the parent's window
if !check_pow(new_header, &parent) {
return Err(HeaderError::InsufficientPow);
}
// 3. Blue work must strictly increase
if new_header.blue_work <= parent.blue_work {
return Err(HeaderError::BlueWorkNotMonotonic);
}
// 4. Timestamp must be sane relative to parent
if new_header.time < parent.time {
return Err(HeaderError::TimeRegression);
}
Ok(())
}Code Example — State Verification
Once the header chain is anchored, state is verified against the header's account_commitment:
use sahyadri_smt::{verify_inclusion, verify_exclusion, Proof, Terminal};
use sahyadri_hashes::Hash;
pub struct LightClient {
/// Most recent trusted header, verified as extending the anchor.
trusted_header: Hash,
/// account_commitment field of that header.
trusted_root: [u8; 32],
}
impl LightClient {
pub fn verify_account(
&self,
account_key: [u8; 32],
proof: &Proof,
) -> Option<AccountState> {
match &proof.terminal {
Terminal::Empty => {
if verify_exclusion(&self.trusted_root, &account_key, proof) {
Some(AccountState::default()) // absent = zero balance
} else {
None
}
}
Terminal::Leaf { value, .. } => {
if verify_inclusion(&self.trusted_root, &account_key, value, proof) {
// value is the content_hash of the account state.
// Fetch the full state from the state store via a
// second query, or accept the hash-only attestation
// if the client does not need the balance value.
None
} else {
None
}
}
}
}
}For browsers, the same logic compiles to WebAssembly via the Sahyadri WASM SDK. For embedded devices, the Rust core runs with no_std + alloc for a footprint under 100 KB.
Use Cases
Roadmap
| Phase | Deliverable |
|---|---|
| Now | SMT proofs (inclusion + exclusion) — implemented and tested |
| Phase 5b | getStateProof RPC endpoint |
| Phase 5c | Header-only sync protocol — rolling header stream |
| Phase 5d | WASM light client SDK (browser + mobile) |
| Phase 5e | Reference CLI light client |
| Phase 5f | Rust no_std light client for embedded devices |
| Phase 5g | Batch proof API — one RPC, N accounts |
Pruning & Storage
A Sahyadri node does not keep every historical state forever. Once a block falls outside the retention window, its SMT nodes and account state snapshots are permanently deleted from disk. This is what keeps a full node lightweight: storage grows with the current account set, not with the entire history of the chain.
Retention parameters (mainnet):
| Parameter | Value | Meaning |
|---|---|---|
| Finality depth | 43,200 blocks (~12 hours) | After this depth, a block cannot be reorged. |
| Pruning depth | 108,000 blocks (~30 hours) | After this depth, block data and SMT state are permanently pruned. |
Two kinds of nodes exist:
- Regular node — keeps only the retention window. Bounded disk. Suitable for mining, validation, wallets, and RPC.
- Archive node — retains the full history. Larger disk. Required for historical queries, explorers, and long-term audits.
State proofs are always available for blocks within the retention window. For blocks older than that, an archive node is the only source.
Garbage collection runs automatically once the pruning point advances — roughly every 30 hours on mainnet at the current block rate. A mark-and-sweep pass over the content-addressed SMT identifies every node and state reachable from a live root and deletes the rest. The pass takes well under a second even on a mature chain, and does not block block production.
Known Limitations
- Cannot produce blocks. A light client has no view of the full DAG, so it cannot mine or fully validate blocks. It only validates state.
- Cannot relay to others. A light client's network view is narrow. Full nodes are needed for network propagation.
- Trusts its initial anchor. The very first header a light client adopts comes from somewhere — a bundled checkpoint, a peer, or a bootstrap. This is a one-time trust assumption, but it is a real one.
- Does not see the mempool. Light clients cannot observe unconfirmed transactions or help with fee estimation. They see only what is in the headers they trust.
- Requires a proof-serving peer. If no peer will serve proofs, the light client is stuck. Any full node can serve, so this is a practical concern rather than a trust concern.
- Probabilistic finality. A trusted header can, in principle, be reorged out. The client should wait for a confirmation depth that matches the value at stake before treating state as final.
Summary
A light client is what makes Sahyadri usable on the devices most people actually carry. It turns the protocol's most abstract property — a pure-function, content-addressed commitment to all account state — into a concrete benefit: verify anything, anywhere, without storing or trusting the world.
This is the practical meaning of "the present proves the past." No node has to carry history to know that today's state is correct. The state itself carries its own proof.