Skip to content

zkCasper

Special thanks to Alistair Stewart, Oana Ciobotaru and Sergey Vasilyev (authors of the “Accountable Light Client Systems for PoS Blockchains” paper) for the helpful discussions and feedback that were instrumental in making this protocol possible.

We present a protocol for efficiently verifying the Ethereum Beacon chain's Casper FFG consensus proofs using a SNARK-based approach. With this scheme, computationally constrained environments, such as on-chain or off-chain consensus clients, can securely follow the Casper FFG protocol and benefit from the crypto-economic security provided by the over 17 million ETH ($34 billion at the time of writing) staked on the Beacon chain. This protocol offers full node-level security that is orders of magnitude more secure than the sync committee, and is fully Byzantine fault-tolerant.

Motivation

The sync committee was introduced to the beacon chain in the Altair hard-fork and it consists of a randomly selected subset of 512 validators from the full validator set. The motivation for this committee was consensus proofs that could be verified cheaply. Unfortunately, this protocol introduces new security assumptions that are completely orthogonal to the security of the beacon chain. More specifically, it has much lower crypto-economic security, as well as a lack of slashing for byzantine behavior, which is critical to the safety of POS consensus.

Therefore, bridges and off-chain consensus clients that rely on the beacon chain consensus proofs must trust that the sync committee will not collude to perform eclipse or data withholding attacks, even when there are no consequences for such actions. We find this blind-faith security model to be completely unacceptable. Consequently, we have opted for the more ambitious approach of directly verifying the Casper FFG consensus proofs.

Preliminaries

We let ee be a bilinear pairing function such that e:G1×G2GTe : \mathbb{G}_1 \times \mathbb{G}_2 \rightarrow \mathbb{G}_T. All groups have some prime-order pp. Let g1\textmd{g}_1 and g2\textmd{g}_2 be the generators for G1\mathbb{G}_1 and G2\mathbb{G}_2 respectively.

Next we define the hash function H1:MG1H_1: \mathcal{M} \rightarrow \mathbb{G}_1. This function simply takes an arbitrary length message and maps it to an element of the G1\mathbb{G}_1 group.

BLS Signatures

BLS signatures[1]^{[1]} enable consensus proofs that are very efficient to verify as it supports both public key and signature aggregation. So a verifier only needs to verify a single aggregate signature rather than nn signatures.

KeyGen():KeyGen(): Choose a random sFps \leftarrow \mathbb{F}_p and output pk=g2spk = \textmd{g}_2^s and sk=ssk = s

Sign(sk,m):Sign(sk, m): outputs σ=H1(m)sk\sigma = H_1(m)^{sk}. This signature is a single group element in G1\mathbb{G}_1.

AggregateSignature(σ1,,σn):AggregateSignature(\sigma_1, \dots, \sigma_n) : This reduces a set of signatures σ1,σn\sigma_1, \dots\sigma_n to a single group element σ~=i=0n1σi\tilde\sigma = \sum^{n-1}_{i=0}\sigma_i. Outputs σ~\tilde\sigma .

AggregateKeys(pki,,pkn):AggregateKeys(pk_i, \dots, pk_n) : This reduces a set of public keys pk1,,pknpk_1, \dots, pk_n to a single group element apk=i=0n1pkiapk = \sum^{n-1}_{i=0}pk_i. Outputs apkapk.

Verify(pk,m,σ):Verify(pk, m, \sigma): Checks the equality of the pairing:

e(H1(m)sk,g2)=e(H1(m),pk)e(H_1(m)^{sk}, \textmd{g}_2) = e(H_1(m), pk)

This works because our pairing is bilinear:

e(H1(m),g2)sk=e(H1(m),g2)ske(H_1(m), \textmd{g}_2)^{sk} = e(H_1(m), \textmd{g}_2)^{sk}

This also extends to aggregate signatures:

e(g1,σ~)=e(apk,H1(m))\begin{equation} e(\textmd{g}_1, \tilde\sigma ) = e(apk, H_1(m)) \tag{1} \end{equation}

Homomorphic KZG Commitments

We’ve previously reviewed the KZG commitment scheme here. One great feature of KZG commitments is that they are homomorphic. What this means is that we can update the values in a commitment to some polynomial without needing the full polynomial. Recall that a KZG commitment CC to any set VV is of the form:

gϕ(s)=i=0n(gsi)ϕi\textmd{g}^{\phi(s)} = \sum^{n}_{i=0} (\textmd{g}^{s^i})^{\phi_i}

Where ss is the secret value, n=V1n = |V| - 1, and ϕ(x)\phi(x) is the polynomial that interpolates all the coordinates (xi,vi)(x_i, v_i) derived from the Lagrange basis:

ϕ(x)=i[0,n)nviLi(x)=i[0,n)nvi(j = 1, i  jn(xxjxixj))\begin{split} \phi(x) &= \sum_{i \in [0, n)}^n v_i \cdot \mathcal{L}_i(x)\\ &= \sum_{i \in [0, n)}^n v_i \cdot (\prod\limits_{j \space = \space 1, \space \\ { i \space\ne \space j}}^{n} (\frac{x-x_j}{x_i - x_j})) \end{split}

Notice that we can rewrite our commitment CC as:

gϕ(s)=i[0,n)(gLi(s))vi\textmd{g}^{\phi(s)} = \sum_{i \in [0, n)} (\textmd{g}^{\mathcal{L}_i(s)})^{v_i}

So that updating a value in this commitment from viviv_i \rightarrow v_i^\prime can be seen as

gϕ(s)=gϕ(s)+(gLi(s))δi \begin{equation} \textmd{g}^{\phi ^\prime (s)} = \textmd{g}^{\phi(s)} + (\textmd{g}^{\mathcal{L}_i(s)})^{\delta_i} \tag{2} \end{equation}

where δi\delta_i is given as:

δi=vivi\delta_i = v^\prime_i - v_i\\

This works because:

ϕ(xi)=ϕ(xi)+Li(xi)δi=vi+δi=vi+vivi=vi \begin{split} \phi^\prime(x_i) &= \phi(x_i) + \mathcal{L}_i(x_i) \cdot \delta_i \\ &= v_i + \delta_i \\ &= v_i + v^\prime_i - v_i\\ &= v^\prime_i \end{split}

Unfortunately for the verifier, naively using equation (2)(2) to update it’s commitment requires computing the Lagrange base Li(x)\mathcal{L}_i(x) which has a runtime complexity of O(2(deg(ϕ)1))O(2(deg(\phi)-1)). This complexity comes from evaluating the terms for both the numerator and denominator. An optimization that can be made here is to have the prover compute and provide the value gLi(s)\textmd{g}^{\mathcal{L}_i(s)} instead, which the verifier can use to compute the update. But how can the verifier trust the correctness of this value? i.e Li(xi)=1\mathcal{L}_i(x_i) = 1. This is where KZG proofs come in. First we define the polynomial L(x)L(x) as the sum of all Lagrange bases in ϕ(x)\phi(x):

L(x)=i=0n1Li(x)L(x) = \sum^{n-1}_{i = 0} \mathcal{L}_i(x)

This allows us create a KZG commitment to this polynomial gL(s)\textmd{g}^{L(s)} as part of the KZG set up ceremony. Since we know that L(xi)Li(xi)=0L(x_i) - \mathcal{L}_i(x_i) = 0, thus the prover can can compute a KZG proof for the xix_i-th coordinate using the quotient.

ψ(x)=L(x)Li(xi)(xxi)\psi(x) = \frac{L(x) - \mathcal{L}_i(x_i)}{(x - x_i)}

so that the verifier can verify gLi(s)\textmd{g}^{\mathcal{L}_i(s)} by using the pairing check:

e(gL(s)gLi(s),g)=e(gψ(s),gsgxi)e(\frac{\textmd{g}^{L(s)}}{\textmd{g}^{\mathcal{L}_i(s)}}, \textmd{g}) = e(\textmd{g}^{\psi(s)}, \frac{\textmd{g}^s}{\textmd{g}^{x_i}})

But what if the prover wants to update multiple points in the commitment? Then they’ll have to submit the terms (gLi(s),gδi) iI(\textmd{g}^{\mathcal{L}_i(s)}, \textmd{g}^{\delta_i}) \space \forall i \in I where II is the set of all points to be updated. So updating our commitments becomes

gϕ(s)=gϕ(s)+iII(gLi(s))δi \begin{equation} \textmd{g}^{\phi ^\prime (s)} = \textmd{g}^{\phi(s)} + \sum^{|I|}_{i \in I}(\textmd{g}^{\mathcal{L}_i(s)})^{\delta_i} \tag{3} \end{equation}

What about verifying these terms? This is possible with KZG multi-proofs[3]^{[3]}. Dankrad's article on KZG commitments outlines that to verify batch KZG proofs, the verifier needs to compute some Lagrange bases themselves. However, the complexity for the bases is now reduced to O(2(I1))O(2(I - 1)). In our case, the prover has already supplied those Lagrange bases. Let's define I(x)I(x) as follows:

I(x)=iIILi(x)(4) I(x) = \sum^{|I|}_{i \in I} \mathcal{L}_i(x) \tag{4}

Using the polynomial in (4)(4), We can come to the following conclusions:

iI, L(xi)I(xi)=0L(x)I(x)=ψ(x)iII(xxi)L(x)I(x)=ψ(x)ZI(x)\begin{align*} \forall i \in I, \space L(x_i) - I(x_i) &= 0 \\ L(x) - I(x) &= \psi(x) \cdot \prod^{|I|}_{i \in I} (x - x_i) \\ L(x) - I(x) &= \psi(x) \cdot Z_I(x) \end{align*}

Since the prover provides the individual terms gLi(s) iI\textmd{g}^{\mathcal{L}_i(s)} \space \forall i \in I, the verifier can use them to compute:

gI(s)=iIIgLi(s)\textmd{g}^{I(s)} = \sum^{|I|}_{i \in I}\textmd{g}^{\mathcal{L}_i(s)}

Finally the prover provides the KZG multi-proof gψ(s)\textmd{g}^{\psi(s)} which is still a single group element, but allows us to verify multiple points using the pairing check:

e(gL(s)gI(s),g)=e(gψ(s),gZI(s))(5)e(\frac{\textmd{g}^{L(s)}}{\textmd{g}^{I(s)}}, \textmd{g}) = e(\textmd{g}^{\psi(s)}, \textmd{g}^{Z_I(s)}) \tag{5}

Casper FFG

The Casper FFG consensus protocol defines the finality rule for the Ethereum beacon chain[4]^{[4]}. It does this by introducing what it refers to as "source" and "target" checkpoints. These checkpoints are 32 slots apart and correspond to the epoch boundaries of the beacon chain. This means that Casper FFG finalizes whole epochs, rather than arbitrary block sequences.

An epoch according to the protocol goes through three stages: unjustified, justified, and finalized. The genesis block is a special case, as it is already finalized by the protocol rules. Moreover, an epoch (source) can only be considered finalized if there exists a direct descendant epoch (target) that has been justified by a supermajority of the authority set.

Blocks b1 & b2 are finalized, while b3 is only justified.

Blocks b1 & b2 are finalized, while b3 is only justified.

As stated in my article on the sync committee, there are simply too many authorities in the Ethereum beacon chain (Currently 560k and rising). Passing around a epoch checkpoint to sign would degrade the network. To solve this issue, the authorities are split up into committees with a maximum count of 2048 per committee. These attestation committees produce signed Casper FFG votes. The Casper FFG protocol uses BLS signature which allows signatures from individual committee members to be aggregated into a single signature per committee.

Attestation messages are published in the beacon chain blocks and contains: Casper FFG votes, a bitlist of the validators in the committee who signed the attestation and a BLS signature over the AttestationData. The BLS signatures for the attestation messages use the G1\mathbb{G}_1 group for public keys, while it’s signatures are in the G2\mathbb{G}_2 group. A consensus client observing the attestation messages can conclude that some epoch is justified (and it’s parent finalized) if they collect enough messages from a supermajority of the validator set confirming the epoch.

struct Attestation {
    aggregation_bits: Bitlist<MAX_VALIDATORS_PER_COMMITTEE>
    data: AttestationData
    signature: BlsSignature
}
 
struct AttestationData {
    slot: u64
    index: u64
    // LMD GHOST vote
    beacon_block_root: H256
    // FFG vote
    source: Checkpoint
    target: Checkpoint
}

Validators can join the validator set by locking up 32 ETH. The beacon chain adds their BLS public key as well as other protocol metadata (such as their activation_epoch, exit_epoch and a boolean flag that tracks if they’ve been slashed ) to the “validator registry” an ssz list object on the [BeaconState](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate) . Once added to the validator set, their initial activation_epoch and exit_epoch is set to a constant FAR_FUTURE_EPOCH (2641)(2^{64}-1) [source]. This triggers the epoch transition function to schedule them for activation for the next epoch once the current epoch ends. [source]. It's worth noting that the beacon chain does NOT remove any validators from its registry. Instead, it updates their activation_epoch, exit_epoch, or slashed values.

If a validator is found to have either proposed two competing beacon blocks for the same height [source] or signed an attestation that violates casper FFG's rules [source]. Then their validator.slashed will be updated to true immediately, meaning they can no longer propose blocks or sign attestations. Validators may choose to voluntarily exit the active set after some minimum period, after which their exit_epoch will be changed from FAR_FUTURE_EPOCH to some near future epoch. [source]. Hence, all inactive validators will satisfy the condition exit_epoch < current_epoch < activation_epoch or slashed = true.

It is critical to note that both the BeaconState and the "validator registry" list are SSZ objects that can be merkleized. As a result, it is possible to obtain SSZ merkle multi-proofs of validator state changes, such as deposits (new deposits), exits, and slashings, which can be verified against the BeaconState root. This will be important later on.

Aggregate Public Key Proofs

The Accountable Light Client Systems for PoS Blockchains[5]^{[5]} paper by Web3 Foundation researchers presents a SNARK that can verify whether the constituent public keys in an aggregate BLS public key exist as a subset of a list of potential signers. Using this SNARK the verifier only needs to maintain a KZG commitment to the BLS public keys of all potential signers.

More formally, Given a set of potential signers {pki}i\{pk_i\} \forall i \in TT. The verifier holds a commitment CC to the list of public keys in TT. The prover can then send a bitlist bb which represents a subset SS of the signers in TT, an aggregate BLS signature σ\sigma, aggregate public key apk=i=0Spkiapk = \sum_{i = 0}^{|S|} pk_i and a succint proof π\pi that apk=i=0Tbipkiapk = \sum_{i = 0}^{|T|} b_i \cdot pk_i.

After verifying the aggregated public key SNARK proof π\pi, the verifier can simply perform the naive aggregate BLS signature verification. This SNARK construction removes the requirement for the verifier to know the individual public keys in TT making it perfect for truly light clients.

The SNARK circuit itself simply performs elliptic curve affine additions of the BLS public keys and constrains these additions using a custom PLONK gate.

Screenshot 2023-05-10 at 11.52.59 AM.png

The SNARK requires a pair of pairing-friendly elliptic curves: one for the BLS signature (the inner curve) and one for the SNARK itself (the outer curve). The paper recommends using the curves BLS12-377 and BW6-761. However, for the BLS12-381 keys used in the Casper FFG protocol, we must use the BW6-767 curve for the outer curve to avoid non-native field arithmetic that would significantly increase the number of constraints and, consequently, proving times. The protocol for aggregated public key proofs is formally defined below:

APK.Setup(t,s):APK.Setup(t, s): This outputs the proving and verification keys srspk,srsvk\langle srs_{pk}, srs_{vk} \rangle using the powers of ss and a SNARK preprocessing algorithm, which can be used to commit and prove a maximum number of tt signers.

APK.Commit(srspk,T):APK.Commit(srs_{pk}, T): Given a set of public keys T={pki}i=1tT = \{pk_i\}_{i = 1}^{t}, outputs a commitment to the public keys CC.

APK.Prove(srspk,C,{pki}i=1S,b):APK.Prove(srs_{pk}, C, \{pk_i\}^{|S|}_{i = 1}, b): Computes the SNARK proof for the aggregation of the given public keys and a bitlist b={biti}i=1tb = \{bit_i\}^t_{i = 1} that indicates their positions in the original set TT. Outputs πapk\pi_{apk} .

APK.Verify(srsvk,C,apk,πapk,b):APK.Verify(srs_{vk}, C, apk, \pi_{apk}, b): Given the commitment, aggregated public key, SNARK proof and bitlist. This verifies that apk=i=0Sbipkiapk = \sum_{i = 0}^{|S|} b_i \cdot pk_i. Outputs 1/01/0.

Protocol

With the preliminaries out of the way, let's take a look the zkCasper protocol. Our approach is to begin by establishing a trusted commitment to the list of all validators in the Beacon chain. However, the validators in the Beacon chain will not sign this commitment as the statuses of the individual validators in the set changes. Instead, they sign the block roots of epoch boundaries. Fortunately, these block roots contain a merkle commitment to all the validators in the validator registry. Therefore, the verifier can leverage the homomorphic property of KZG commitments in order to update their commitment after verifying the merkle proofs of the validator set changes. In this way, the verifier can securely track all validator set changes.

Armed with this commitment, the verifier only needs to know the aggregate of public keys that signed a committee's attestation messages. Using the apk SNARK, the verifier can confirm that the aggregate of these aggregate public keys, along with a bitlist and a SNARK proof, corresponds to the commitment it has to the public keys of the full validator set. This enables the verifier verify as many attestation messages at once in order to take advantage of the performance benefits of equation (1)(1).

An issue that arises is that the beacon chain uses a combination of Casper FFG and LMD Ghost[6]^{[6]}. A consequence of this decision is that signed attestations do not need to reach super-majority participation before they are published in beacon chain blocks. This means that there may be multiple signed attestation messages from a committee with overlapping participants. The aggregate of these overlapping signers is unfortunately incompatible with the apk SNARK’s constraints. However this just means we cannot verify a supermajority of the beacon chain’s attestations in one go, we can instead prove nn batches of attestations that have no overlapping committee signatures. Where nn is the maximum number of times a single committee produced an attestation.

The protocol is formally defined below:

Setup(s,t,V):Setup(s, t, V): Performs the SNARK set up, srspk,srsvkAPK.Setup(t,s)\langle srs_{pk}, srs_{vk} \rangle \rarr APK.Setup(t, s). Computes the commitment C=APK.Commit(srspk,V)C = APK.Commit(srs_{pk}, V) where VV is the set of all validator public keys. Computes the update key srsuk=gL(s)srs_{uk} = g^{L(s)}. Outputs srsuk,srspk,srsvk\langle srs_{uk}, srs_{pk}, srs_{vk} \rangle .

ProveAttestations(srspk,C,A):ProveAttestations(srs_{pk}, C, A): Given a set of nn Casper FFG committee attestations with non-overlapping signatures A=(mi,σ~i,({pkj}j=1P)i)i[1,n)A = (m_i, \tilde\sigma_i, (\{pk_j\}^{|P|}_{j=1})_i) \forall i \in [1, n) where PP is the set of participating public keys in each committee attestation. The prover computes the bitlist bb for the participating public keys of all the individual validators, then finally they compute the apk proof for the aggregation of these public keys, and outputs A,πapk,b\langle A, \pi_{apk}, b \rangle. Where:

πapk=APK.Prove(srspk,C,i=1Pnpki,b)\pi_{apk} = APK.Prove(srs_{pk}, C, \sum^{|P| \cdot n}_{i=1} pk_i , b)

VerifyAttestations(srsvk,C,A,S,πapk,b):VerifyAttestations(srs_{vk}, C, A, S, \pi_{apk}, b): Where, A=(mi,σ~i,apki)i[1,n)A = (m_i, \tilde\sigma_i, apk_i) \forall i \in [1, n), mi=Es,Etm_i = \langle \mathcal{E}_s, \mathcal{E}_t \rangle. First the verifier computes b=dbb = d \oplus b, where dd is the bitlist of disabled validators, next they verify the apk proof for the participating public keys:

APK.verify(srsvk,C,i=0napki,πapk,b){true,false} APK.verify(srs_{vk}, C, \sum^n_{i=0} apk_i , \pi_{apk}, b) \in \{true, false\}

finally they verify the BLS signatures for each committee’s attestations using equation (1)(1)

e(g1,σ~i)=e(apki,H1(mi))e(\textmd{g}_1, \tilde\sigma_i ) = e(apk_i, H_1(m_i))

If all signature verifications pass, the verifier updates the bitlist for all the signers seen so far by computing S=SbS = S \lor b. Outputs 1/01/0. A source epoch boundary Es\mathcal{E}_s should be considered final if Hamming(S)23(Cd)+1Hamming(S) \ge \frac{2}{3}(|C| - |d|) + 1.

ProveValidatorUpdates(C,I,srsuk):ProveValidatorUpdates(C, I, srs_{uk}): The prover computes the merkle multi-proof πmerkle\pi_{merkle} for all validators viIv_i \in I whose status (joined, exit_epoch, activated_epoch, slashed) have changed with respect to the latest finalized epoch block root Es\mathcal{E}_s. Next they compute the KZG proof πkzg=gψ(s)\pi_{kzg} = g^{\psi(s)} for the points that pass through I(x)I(x) defined in (4)(4). Outputs I,πkzg,πmerkle\langle I, \pi_{kzg}, \pi_{merkle} \rangle.

UpdateValidatorSet(srsuk,C,I,πmerkle,πkzg,Es):UpdateValidatorSet(srs_{uk}, C, I, \pi_{merkle}, \pi_{kzg}, \mathcal{E}_s) : First the verifier verifies the ssz merkle proof for the validator statuses via ssz.Verify(Es,πmerkle,{vi iI})ssz.Verify(\mathcal{E}_s, \pi_{merkle}, \{v_i \space \forall i \in I\}), where viv_i represents the validator struct in the beacon state for the block root of the finalized epoch Ef\mathcal{E}_f. If the Merkle verifications pass, the verifier then proceeds to verify I(x),πkzgI(x), \pi_{kzg} using the equation in (5)(5). If the KZG proof is valid, then the verifier can update their commitment CC with the set of all new validators in II using equation (3)(3). Finally they compute d=(dr)ad = (d \lor r) \oplus a , where r,aIr, a \sub I, rr is the bitlist of all validators who have been disabled (exited, slashed) and aa is the bitlist of all validators who have been activated. Outputs the updated commitment CC^{\prime}.

References

[1]^{[1]} Dan Boneh; Ben Lynn & Hovav Shacham. "Short Signatures from the Weil Pairing".

[2]^{[2]} Dan Boneh, Manu Drijvers, and Gregory Neven. "Compact Multi-Signatures for Smaller Blockchains"

[3]^{[3]} Dankrad Feist, Kate Polynomial Commitments

[4]^{[4]} Vitalik Buterin and Virgil Griffith. "Casper the Friendly Finality Gadget"

[5]^{[5]} Oana Ciobotaru, Fatemeh Shirazi, Alistair Stewart , and Sergey Vasilyev. "Accountable Light Client Systems for PoS Blockchains"

[6]^{[6]} Combining Ghost and Casper