For more context and terminology about the architecture of Espresso, please read README.md.
At a high level ZK rollups produce blocks and periodically settle their new state on layer 1 (e.g. Ethereum), after applying all the transactions of such blocks to their current state. The time between two consecutive updates can vary, yet it is high enough in order to amortize gas costs for the rollup and thus offer low fees to end users. ZK rollups have the option to participate in Espresso's marketplace for rollup sequencers. This marketplace can produce blocks simultaneously for multiple rollups. In this context each rollup assigns itself an identifier called a namespace. Each rollup uses its namespace identifier to extract its own rollup-specific transactions from the Espresso ledger, along with a succinct proof of inclusion of these transactions in the Espresso ledger. Participation in the Espresso marketplace has many benefits for rollups, which we discuss elsewhere . In this document we focus on how a zk rollup might integrate with the Espresso marketplace.
Similarly to their optimistic alternatives, ZK rollups need to instantiate their derivation pipeline in order to integrate with Espresso. There are a number of alternatives for such integration, depending on specific constraints the rollup may have, such as using a particular data availability layer in addition to Tiramisu, or the need to update their state on the L1 at a faster pace than the Espresso light client contract does.
As described in the sequence diagram, ZK rollups relying on Espresso blocks as their source of transactions have to prove their state update is consistent with the Espresso state. This can be achieved in two ways:
- The rollup relies on the Espresso light client contract to fetch the Espresso state updates.
- The rollup verifies some value equivalent to the Espresso finality gadget inside its circuit.
Moreover, in case the Espresso consensus loses liveness, and thus the corresponding finality gadget is not available, the rollup can fall back to a backup sequencer. In order to reliably detect that the Espresso consensus is not making progress, the rollup contract will call an escape hatch function part of the Espresso light client contract.
For both alternatives we describe:
- the high level structure of the circuit as well as the rollup L1 contract and how they are related.
- how rollups use the escape hatch function of the Espresso light client contract to source the transactions from the backup sequencer.
For rollups relying exclusively on a centralized sequencer (such as most current production deployments today), the circuit only checks the correct update of the zkVM as depicted in Figure 1. Naturally this same circuit can be used when the escape hatch is activated, as in this case the backup sequencer is in control of the rollup operator. When the escape hatch is not activated, because the Espresso consensus is making progress, the circuit depicted in Figure 1 needs to be extended with additional gadgets in order to guarantee the transactions are fetched from the Espresso ledger instead of some trusted / local source.
Figure 1: Circuit used when the rollup falls back to using its default (centralized) sequencer. The public inputs
are the current state of the rollup cm_state_vm i
, the new rollup state after update cm_state_vm i+1
and a
commitment to the transactions applied to the state, cm_txs_rollup
. The private input (in bold) corresponds to the
list of transactions.
Each zk rollup already has its own contract on the L1 (Ethereum) that allows the rollup to settle its state to the L1
via verification of a snark proof. The abstract version of this contract is sketched below. In addition to contract
variables and a constructor, it contains a function isEscapeHatchActivated
which allows to detect whether the Espresso
consensus protocol is live or not. In case liveness is lost, the rollup can update its state without reading from the
Espresso ledger by calling the function updateStateDefaultSequencingMode
. Note that the Espresso state is read from
the Espresso light client contract which is referenced by the rollup contract via the variable lcContract
.
// Abstract rollup contract
contract RollupContract {
VMState previousVMState;
uint256 lastEspressoBlockNumber;
LightClient lcContract;
bytes[] vkRollup; // This verification key corresponds to the circuit depicted in Figure 1.
bytes[] vkEspresso; // This verification key corresponds to the circuits depicted in Figure 2 or 3.
uint256 escapeHatchThreshold; // Number of L1 blocks the Espresso light client contract is allowed to lag behind in order to consider the Espresso consensus is still live.
constructor(address EspressoLightClientAddress,...) public {
lcContract = LightClient(EspressoLightClientAddress);
...
}
/// Detects if the escape hatch is activated or not.
function isEscapeHatchActivated() private returns (bool) {
if (lcContract.getFinalizedState().blockHeight > lastEspressoBlockNumber){
return false;
} else {
return lcContract.lagOverEscapeHatchThreshold(block.number, escapeHatchThreshold);
}
}
/// Updates the state of the rollup if the Espresso finality gadget loses liveness.
function updateStateBackupSequencingMode(commTxsRollup, newVMState,snarkProof) virtual {
bytes[] publicInputs = [
previousVMState,
newVMState,
commTxsRollup,
];
SnarkVerify(
publicInputs,
snarkProof,
vkRollup);
previousVMState = newVMState;
}
// Update the rollup state using the Espresso ledger as input.
function updateStateFromEspresso(
currentEspressoState,
blockNumberEspresso,
commTxsRollup,
newVMState,
snarkProof) virtual {
if (blockNumberEspresso <= lastBlockNumberEspresso) {
revert();
}
bytes[] publicInputs = [
previousVMState,
newVMState,
commTxsRollup,
currentEspressoState,
lastEspressoBlockNumber,
blockNumberEspresso
];
SnarkVerify(
publicInputs,
snarkProof,
vkEspresso
);
lastBlockNumberEspresso = blockNumberEspresso;
previousVMState = newVMState;
}
// Main function to update the rollup state. Specific to the type of integration (see below)
function updateRollupState(...
){
...
}
}
Integration 1: Rollup contract fetches Espresso block commitment from the Espresso light client contract
For this integration, Espresso consensus verification is delegated to the Espresso light client contract. In practice the rollup contract will be given a Merkle root of all Espresso block commitments up to now1 and feed it to the circuit. Still additional gadgets need to be introduced in order to implement the derivation pipeline logic consisting at a high level of:
- Collecting all the Espresso blocks since last update, along with proofs of membership for their commitments in the given Merkle root.
- Filtering each of these Espresso blocks to gather all the transactions belonging to the rollups.
- Establishing some equivalence between the commitment to the rollup transactions used in the zkVM and those same transactions obtained after namespace filtering.
Figure 2: Rollup circuit with additional gadgets Espresso Derivation. Private inputs of the circuit are written in uppercase and bold font.
The circuit depicted in Figure 2 operates as follows:
- The Espresso Derivation gadget receives as public inputs a
blk_cm_root
which is the Merkle root of all finalized Espresso block commitments up to now, and a commitment of transactionscm_txs_rollup
that is used by zkVM gadget. With witnesses of all transactions to the rollupROLLUP_TXS
and proofs that they are complete and correctly filtered from finalized Espresso blocks since last updateTXS_NAMESPACE_PROOFS
, it checks that:- Namespace proofs
TXS_NAMESPACE_PROOFS
are valid, saying thatROLLUP_TXS
are complete and correctly filtered from Espresso blockscommited in a Merkle tree with rootblk_cm_root
since last update. - Given
cm_txs_rollup
commits to the filtered transactionsROLLUP_TXS
.
- Namespace proofs
- The zkVM gadget is the original gadget of the rollup circuit that proves a correct transition from state
cm_state_vm i
to the next statecm_state_vm i+1
when applying the transactions represented by the commitment valuecm_txs_rollup
. - These three gadgets above return a boolean: true if the verification succeeds and false otherwise.
- For the circuit to accept, all these gadget outputs must be true, and thus we add an AND gate.
Please read our doc for more detailed description along with a reference implementation.2
The pseudocode of the rollup contract below shows that in the case we rely on the Espresso light client contract to
fetch the Espresso state, the only inputs to the function updateRollupState
are newVMState
, commTxsRollup
and
snarkProof
.
/// Uses the Espresso light client contract to fetch the last state.
contract RollupContract1 is RollupContract {
function updateRollupState(
newVMState,
commTxsRollup,
snarkProof){
// Escape hatch is activated, switch to default sequencing mode
if (isEscapeHatchActivated()){
this.updateStateBackupSequencingMode(commTxsRollup,newVMState,snarkProof);
} else { // No escape hatch, use the state of Espresso consensus
lightClientState = lcContract.getFinalizedState();
currentEspressoState = lightClientState.blockCommRoot;
blockNumberEspresso = lightClientState.blockHeight;
this.updateStateFromEspresso(currentEspressoState, blockNumberEspresso, commTxsRollup, newVMState, snarkProof);
}
}
}
Figure 3: Rollup circuit with additional gadgets Espresso Consensus and Espresso Derivation. Private inputs of the circuit are written in uppercase and bold font.
The circuit depicted in Figure 3 operates as follows:
- The Espresso Consensus gadget checks the Merkle root of Espresso block commitments
blk_cm_root
using the multi-signatureSTATE_SIGS
obtained by the HotShot replicas. To achieve this goal, it is required to obtain the stake table from the previous HotShot epochSTAKE_TABLE_ENTRIES
as witness, containing the list of public keys with their respective stake. It can be linked to some old blocks from last epoch committed in theblk_cm_root
via the witnessSTAKE_TABLE_OPENINGS
. Finally, note that the firstblk_cm_root
value needs to be read from the light client contract. Afterward, no dependency on the light client contract is needed. - The other gadgets are the same as in Integration 1.
The pseudocode of the rollup contract below shows that in the case we do not rely on the Espresso light client contract
to fetch the Espresso state, the function updateRollupState
requires additional inputs (compared to Integration 1)
which are currentEspressoState
and blockNumberEspresso
.
/// Does not use the Espresso Light client contract for fetching the Espresso state
contract RollupContract2 is RollupContract {
function updateRollupState(
currentEspressoState,
blockNumberEspresso,
newVMState,
commTxsRollup,
snarkProof){
// Escape hatch is activated, switch to default sequencing mode
if (isEscapeHatchActivated()){
this.updateStateBackupSequencingMode(commTxsRollup,newVMState,snarkProof);
} else { // No escape hatch, use the state of Espresso consensus
this.updateStateFromEspresso(currentEspressoState, blockNumberEspresso, commTxsRollup, newVMState, snarkProof);
}
}
}