A light client is a program that, with storing minimal information for a chain, can tell you that block H is valid, given that the light client has seen an earlier block H-1. In some cases, it can verify that H is valid, based on blocks further back (H-n). Often light clients are used for fast-syncing chains while still partially checking validity, as well as in highly secure cross-chain messaging.
Light clients generally only store a subset of the chain, such as headers, which contain information like various Merkle roots (e.g., state root, validator root) used for verifying data integrity and inclusion. These Merkle roots allow light clients to verify specific data, such as account balances or transaction validity, without needing the full blockchain. This lightweight nature makes them ideal for devices with limited resources or for users who do not need full access to the entire blockchain data. Light clients rely on full nodes for retrieving blockchain data and verifying transactions [1].
Abstractly, a light client is a pure update function, which given the current trusted state, and a batch of new headers and proofs, will be able to confirm if a proposed next state is valid.
More formally, the update function in pseudo BNF notation can be described using:
HEADER ::= bytes
PROOF ::= bytes
BATCH ::= <HEADER> + <PROOF>
STATE ::= <HEADER>
UPDATE_FN ::= (<BATCH>, <STATE>) -> <STATE>
Where the update function accepts a batch and the current state, and if the batch is valid, alters its internally stored next state.
In our current Berachain implementation, the function is:
function updateClient(
// clientId is used to identify the instance, since a single contract
// monitors many chains.
string calldata clientId,
// A clientMessageBytes is a <BATCH>
bytes calldata clientMessageBytes
)
Note that we do not pass the state as an argument, since that is stored internally in the contract.
The updateClient
function is continuously called by the relayer, whenever packets need to be processed.
sequenceDiagram
Chain A->>Relayer: fetch <BATCH>
Relayer->>Chain B: updateClient(chain B, <BATCH>)
Relayer->>Chain B: recvPacket