Skip to content

Light Clients

This document traces the complete light client flow from initialization through state verification, including finality proofs, validator set transitions, state proofs, and historical block verification.

Light clients verify blockchain state without executing transactions. They rely on:

  1. Finality proofs - BLS threshold signatures proving block finalization
  2. Merkle proofs - Cryptographic proofs for state values
  3. MMR proofs - Historical block inclusion proofs
  4. DAS - Data availability sampling (optional)
Initialize with trusted context --> Verify finality proofs -->
Track validator transitions --> Query state with proofs --> Verify historical blocks
pub struct LightClientState {
pub context: LightClientContext, // Current trusted validator set
pub pending_update: Option<PendingUpdate>, // Announced validator set change
pub current_epoch: u64,
pub latest_height: u64,
}
pub struct LightClientContext {
pub validator_set: ValidatorSet,
pub aggregate_key_proof: ValidatorProof, // Merkle proof for aggregate key
pub version: u32,
}
let context = LightClientContext {
validator_set: trusted_validator_set,
aggregate_key_proof: build_aggregate_key_proof(&validator_set),
version: validator_set_version,
};
let state = LightClientState::new(context, initial_epoch, initial_height);
pub fn verify_finality_proof(
proof: &FinalityProof,
ctx: &LightClientContext,
) -> Result<(), LightClientError> {
// Step 1: Validator set commitment
let root = ctx.commitment_root();
if root != proof.header.validator_set_id.id {
return Err(LightClientError::ValidatorSetIdMismatch);
}
// Step 2: Aggregate key Merkle proof
if ctx.aggregate_key_proof.leaf_index != 0 {
return Err(LightClientError::InvalidMembershipProof);
}
if !ctx.aggregate_key_proof.verify(&root) {
return Err(LightClientError::InvalidMembershipProof);
}
// Step 3: BLS signature verification
if !proof.verify(&ctx.validator_set.aggregate_bls_pubkey) {
return Err(LightClientError::InvalidSignature);
}
Ok(())
}
pub struct FinalityProof {
pub header: BlockHeader,
pub epoch: u64,
pub view: u64,
pub parent_view: u64,
pub key_version: u32,
pub certificate: FinalityCertificateBytes, // Threshold BLS signature
}
[Root]
/ \
[Internal] [Internal]
/ \ / \
[AggKey Leaf] [Val 0] [Val 1] [Val 2]
pub fn commitment_root(&self) -> Hash {
let mut leaves = Vec::with_capacity(self.validators.len() + 1);
// Leaf 0: aggregate BLS key
leaves.push(self.aggregate_key_leaf_hash());
// Leaves 1..n: individual validators
for i in 0..self.validators.len() {
leaves.push(self.validator_leaf_hash(i)?);
}
merkle_root(&leaves)
}

When a block announces a validator set change:

pub fn process_finality_proof(&mut self, proof: &FinalityProof) -> Result<()> {
// Verify against current context
verify_finality_proof(proof, &self.context)?;
// Check for validator set change announcement
if proof.header.next_validator_set_id != proof.header.validator_set_id {
self.pending_update = Some(PendingUpdate {
next_validator_set_id: proof.header.next_validator_set_id.clone(),
announced_at_epoch: proof.header.epoch,
announced_at_height: proof.header.height,
});
}
self.current_epoch = proof.header.epoch;
self.latest_height = proof.header.height;
Ok(())
}
Proof TypePurpose
SlotProofStorage slot existence/non-existence
PositionProofMerkle position in state tree
NonMembershipProofKey doesn’t exist (left/right neighbors)
ContractQmdbProofFull contract state proof
  1. Validate QMDB ops sequence
  2. Recompute state root from ops
  3. Verify bounded proof
  4. Verify slot proof (if present)
pub struct FinalizedMMR {
leaves: Vec<FinalizedEntry>,
height_index: BTreeMap<u64, u64>, // height -> position
}
pub struct FinalizedMmrProof {
pub entry: FinalizedEntry,
pub leaf_position: u64,
pub leaf_count: u64,
pub siblings: Vec<([u8; 32], bool)>, // (hash, is_left_of_path)
}
pub fn verify(&self, expected_root: &[u8; 32]) -> bool {
let mut current = leaf_hash(&self.entry);
for (sibling, is_left) in &self.siblings {
current = if *is_left {
sha256(concat![b"mmr_internal_v1", sibling, &current])
} else {
sha256(concat![b"mmr_internal_v1", &current, sibling])
};
}
current == *expected_root
}
MethodPurpose
NodeRpcV1.finality_proofGet finality proof for a height
NodeRpcV1.light_client_contextGet validator set context
NodeRpcV1.state_proofGet state proof for address/slot
NodeRpcV1.finalized_history_rootGet current MMR root
NodeRpcV1.finalized_history_proofGet MMR inclusion proof
Terminal window
# Verify a block is in the finalized history
node verify --height 100
# Verify with a specific RPC endpoint
node verify --height 100 --rpc-url http://localhost:3030
# Output as JSON (for scripting)
node verify --height 100 --format json
InvariantCheck
Validator Setcommitment_root() == header.validator_set_id.id
Aggregate KeyLeaf index 0, correct hash, valid Merkle path
BLS SignatureCertificate verifies with aggregate key
Epoch BoundaryTransitions only at epoch > announced_epoch
Version MonotonicNew version >= announced version
QMDB OrderingPositions 0 to last_loc in sequence
Leaf DigestsPreimage hash matches proof leaf
MMR PathSiblings form valid path to root