How the Light Client Works
The source code for the light client contract can be found on GitHub.
The LightClient
contract validates the HotShot state through cryptographic proofs (SNARK proofs). Rollups can use the new finalizedState
from the LightClient
contract to confirm the validity and finality of transactions that have been bundled and processed.
Light Client State
This LightClientState
includes information such as:
the Merkle root of finalized block commitments,
blockCommRoot
the fee ledger commitment,
feeLedgerComm
the active stake table commitment,
stakeTable*Comm
the latest view number,
viewNum
, and block height,blockHeight
, of the finalized HotShot chain
For the next release, a fixed stake table is used so stake table commitments will not be updated.
The most important part of the LightClientState
is the blockCommRoot
, which is the root of an appendonly Merkle tree of all blocks finalized by HotShot. This root is public, allowing other contracts to use succinct Merkle proofs to verify the inclusion of a certain block commitment at a certain height and to update the root when new blocks are appended.
At any given time, the LightClientState
contains Espresso block commitments from height 0 up to (but not including) the current block height, blockHeight
. All commitments for heights greater or equal to blockHeight
are not present—the corresponding leaf is empty in the tree.
Rollups and the Light Client Contract
Rollup contracts on L1 must use the blockCommRoot
when validating a state transition to ensure that the rollup block claimed to have been executed is indeed the next block in the canonical sequence.
The proposer of a rollup state transition must provide a proof, relative to blockCommRoot
, showing that the Espresso state commitment at a specified height is consistent with the rollup block commitment.
For rollups to integrate with Espresso, they need to modify their contract on L1 to prove that their state is derived from the Espresso state. For instance, consider a scenario where the Espresso prover pushes a new Espresso state commitment to the light client contract every 1 minute, and the rollup prover submits a new rollup block commitment every 10 minutes to their rollup contract. The rollup prover needs to prove that the block commitment it publishes to the rollup contract corresponds to all its rollup transactions contained in the Espresso blocks during the 10minute period.
Each Espresso block commitment also commits to a list of rollup transactions (among other metadata) which facilitates lightweight proofs with transaction granularity for arbitrarily old rollup blocks. This approach allows for the verification of these rollups blocks on Espresso's chain and enables the light client contract to operate with a constant amount of storage, irrespective of the HotShot chain's length.
Data Availability
Since the actual block commitments (let alone the full blocks) are not stored onchain, it is important to understand the data availability properties ensuring that clients can always retrieve an old block, block commitment, or a block's Merkle proof.
Clients can fetch a Merkle proof for any block from an archival query service and authenticate it against the block root in the light client state. Failing that, they can fetch the individual blocks from HotShot DA and extract the proof themselves.
ZK Proofs, SNARK Proofs, and ZK Circuits
A circuit defines the computation to be proven. Zeroknowledge proof (ZKP) systems can generate cryptographic proofs attesting to the validity of statements described by the circuit without revealing underlying witness. In our context, we rely on SNARK proofs (a special kind of ZKP), whose succinct nature is especially valuable for verifying computation in smart contracts where gas costs are a critical concern. Irrespective of the number of signatures/consensus votes, the size and verification cost of the proof remain constant.
In a zeroknowledge protocol there are two main roles: (i) the prover and (ii) the verifier.
The prover uses a combination of secret inputs (HotShot nodes' Schnorr signatures), also called witnesses, public inputs (the LightClientState
), and a circuit description in order to generate a SNARK proof.
The verifier uses the public inputs and the SNARK proof to verify that the rules defined by the ZK circuit are satisfied. In this case, the LightClient
contract acts as the verifier for this ZK proof via the verifyProof
method which is invoked within the newFinalizedState
function.
Finally, both the prover and verifier use some public parameters. These public parameters are derived from the circuit and a structured reference string (SRS) that requires a trusted setup to be generated but can be reused for other circuits.
The Light Client Circuit
Let the following circuit $\mathcal{C}_{\sf qc}$ over prime field $\mathbb{F}_p$, the corresponding Jubjub curve group $\mathbb{G}=\langle g\rangle$ whose scalar field is $\mathbb{F}_r$ and base field is exactly $\mathbb{F}_p$, so that each group element $\in \mathbb{F}_p \times\mathbb{F}_p$.
Public input:
stake table commitment: $\mathsf{cm}_\mathcal{T} \in \mathbb{F}_p$
A stake table entry now composes of a triple $(bls\_ver\_key, schnorr\_ver\_key, stake\_amount)$. BLS verification key is under
ark_bn254::Fq
, Schnorr verification key is underark_bn254::Fr
, and the stake amount is within the range ofark_bn254::Fr
. The commitment should be computed in the following way: first serialize all BLS keys into elements ofark_bn254::Fr
, follows by a list of Schnorr keys and then the stake amount. The commitment is the rescue hash of this list.
quorum Threshold: $T \in \mathbb{F}_p$
attested new finalized hotshot state: $m:=(v, h, \mathsf{root_{cm}, cm_{ledger}, cm_{stake\_table}}) \in \mathbb{F}_p^5$
the merkle tree for block commitments can use any hash function (e.g. SHA2) and best if there is an injective mapping between the root value to a $\mathbb{F}_p$ element.
Secret witness:
signers indicator vector: $\vec{v}_S$
stake table vector (consists of public key, weight pair): $\mathcal{T}=[(pk_i \in\mathbb{G}, w_i \in \mathbb{F}_p)]_{i\in[n]}$
list of schnorr signatures: $\{\sigma_i = (s_i, R_i) \in \mathbb{F}_r \times \mathbb{G}\}_{i\in [n]}$
Relation:
the input signers indicator vector $\vec{v}_S$ is a bit vector
correct stake table commitment: $\mathsf{cm}_\mathcal{T}= \mathtt{commit}(\mathcal{T})$
we use Rescuebased commitment, thus all operations are native
accumulated weighted sum exceeds threshold: $\sum_{i\in S}{w_i} > T$
assumption: there’s no overflow!! NOTE: outside the circuit, the client software that’s in charge of stake table management needs to check the accumulated sum does not exceed modulus $p$ AT ALL TIMES! ⚠️
signature verification (on each): $\mathsf{Vfy}(pk_i, m, \sigma_i) = 1$ for all $\forall i\in [n]$.
$c = H(R, m, ..)$: rescuebased hash to get challenge
$x = g^s$: a fixedbase scalar mul
internally, involves bitdecomposition of $s$ and elliptic curve addition based on the bits.
$y = R + pk^c$: a variablebase scalar multiplication + an elliptic curve addition.
$x\overset{?}{=}y$: point equality check
Updating and Verifying LightClientState
The LightClientState
is updated by any state prover that submits valid updates to the LightClient
contract via the newFinalizedState
method which sets the latest finalizedState
.
We assume an altruistic, honest prover for now and leave the design of prover market to future work.
For replicas:
upon receiving new QC from the leader, generate a Schnorr signature over the updated finalized HotShot state, and send it over to the CDN and store it locally for a while.
the local storage for the Schnorr signatures (for each block) can be a sliding window of a fixed size where older signatures got pruned. The window size can be set based on the expected interval for onchain update plus some buffer accounting for temporarily failing prover.
For the altruistic prover:
will continuously listen passively for the HotShot state changes, e.g. when a new block has been decided
periodically requests the Schnorr signatures from the DA layer by sending a request on a specific view $v$. This will be the signature for the new finalized state from the consensus nodes
For convenience of signature collection, prover will first fetch from CDN (we refer to as the relay server) for the list of Schnorr signatures, if failed, then ask each individual replica (note: not small DA committee, or VID, but each node individually).
Once a sufficient amount of valid signatures is collected, some provers can then generate a SNARK proof which is submitted alongside the new finalized state to the light client contract.
For the next release, only a permissioned prover doing the computations will call this function.
Replica nodes update the snapshot of the stake table at the beginning of an epoch and this snapshot is used to define the set of stakers for the next epoch. The light client state must be updated at least once per epoch.
For the next release, we are not using epochs so numBlockPerEpoch
is set to type(uint32).max
during deployment.
HotShot state authentication via Schnorr signatures
When a set of HotShot nodes reach consensus and the finalized HotShot state has been updated, they each sign a Schnorr signature on this updated HotShot state. These signatures assert that the signer agrees with the state of each proposed block. The signatures are stored locally on the DA layer, and to save space, older signatures are pruned using a sliding window mechanism. The window size can be set based on the expected interval for onchain update plus some buffer accounting for a temporarily failing prover.
When a prover (an entity that confirms the truth of a claim) retrieves these signatures, a SNARK proof is then generated. This proof, is used by the LightClient
contract to efficiently verify these Schnorr signatures. The state of the sequencer contract can be updated only if a correct SNARK proof is provided. This is a critical step that ensures the validity and security of state updates in Espresso's consensus protocol.
The proof of the Schnorr signatures is sent to the newFinalizedState
function of the LightClient
contract.
Verifying the Signatures and Light Client State
The LightClient
contract also does the work of verifying the proof that is sent by the prover on L1. The verifyProof
method accepts the proof and a set of public inputs (the LightClientState
) to check whether the proof correctly verifies the new state being submitted.
Verifying a SNARK proof requires a constant amount of space and computation, no matter how many HotShot node signatures are involved. This is unlike verifying the signatures directly, which would require space and computation proportional to the number of signers.
The proof itself contains the HotShot state, the stake table info and the list of Schnorr signatures of the HotShot nodes that formed a Quorum and came to consensus on that state.
This verifyProof
method is executed when the newFinalizedState
method is called so that the new state is accepted only if the proof succeeds.
Public Write Methods
newFinalizedState
This method updates the latest finalized light client state. It is updated per epoch. An update for the last block for every epoch has to be submitted before any newer state can be accepted since the stake table commitments of that block become the snapshots used for vote verifications later on.
in the next launch, only a permissioned prover doing the computations will call this function
Parameters
Name  Type  Description 


 new light client state 

 Plonk proof 
computeStakeTableComm
Given the light client state, compute the short commitment of the stake table
Light Client Contract UML
Light Client Contract Interaction Diagram
Last updated