Gnosis Chain is a canary chain for eth2. Gnosis would like to implement a ZK-based light client for the beacon chain consensus (on Gnosis Chain), which can be validated either by an off-chain user or an EVM smart contract (for use in bridges). Since beacon chain uses BLS12-381, there are currently no Eth precompiles and using ZK helps overcome high gas costs.
Implementing a ZK-based light client would involve generating a SNARK of attestations of a sync committee. This consists of 512 validators rotating every ~27 hours (256 epochs), (see here), so the latency requirements are minimal. (Although if the light client followed updates more closely it would have to process more headers and signatures during that 27 hour period.)
Sync committees are chosen ever 256 epochs (~27 hours) consisting of 512 validators. The sync committee signs every block.
The BeaconBlockState
contains block_roots
and state_roots
for the past SLOTS_PER_HISTORICAL_ROOT = 256 * 32 = 8192
slots, which is the sync committee period.
Say light client has finalized block header in slot N
. This is in sync committee period X = N // 8192
. From the current header we know the sync committess in periods [X, X + 1]
.
M
.M // 8192
needs to be in [X, X + 1]
so that we can validate the attested header using sync committee signatures.N'
in [M - 8192, M - 1]
.N'
the updated light client stored block header.N' // 8192 = X + 1
, then we update the stored sync committees in periods [X + 1, X + 2]
.This means a new finalized block needs to be realized within a single period = 256
epochs = 8192
slots.
Using official light client spec.
LightClientUpdate
containers and must update its tracked BeaconBlockHeader
according to this information. This is received either via p2p network or client-server setup.LightClientStore
The crux of the light client update is validate_light_client_update
, which must validate an instance update
of LightClientUpdate
.
Slightly confusingly, LightClientUpdate
contains attested_header
and finalized_header
fields. (The best guess reasoning is that attested_header
is newer than finalized_header
?) If the update is valid, update.finalized_header
becomes the new snapshot header.
The main steps of the validation are:
update.finalized_header
in the state of update.attested_header
(BeaconState
has a finalized_checkpoint
field).
floorlog2(FINALIZED_ROOT_INDEX)
.next_sync_committee
in the state of update.finalized_header
.
floorlog2(NEXT_SYNC_COMMITTEE_INDEX)
.update.attested_header
The reasoning is:
update.attested_header
because we verified it was signed (attested) by some fraction of the sync committee.update.finalized_header
since it's in the state of update.attested_header
update.finalized_header
The update validation can also be run without update.finalized_header
where Step 1 is skipped and Step 2 is done with update.attested_header
instead. This is to keep a running "best valid update", which is the current verified update.attested_header
with the most sync committee participants. This best valid update is used in case a sync period elapses with no finalized checkpoint.
It seems we need to aggregate SYNC_COMMITTEE_SIZE = 512
pubkeys for the aggregate signatures. In the non-ZK setting there is an optimization available:
SyncCommittee
contains aggregate_pubkey
as the elliptic curve sum of all the pubkeys of validators on the sync committee. This means that we only need to subtract from aggregate_pubkey
the pubkeys of people in the committee that did not sign. So if we have a 90% participation rate, then we only need to subtract 51 pubkeys, instead of adding 461.If EIP 2537 is implemented, then with precompiles of field-to-curve and pairing, gas cost of a single BLS signature is 75000 + 43000*2 + 65000 = 226K
gas (at 1 gas = 20 gwei this is 0.00452 ETH
).
We would still need to aggregate the public keys of the sync committee, which would take another 512 * 500 = 256K
gas. Plus gas costs of merkle proofs.
[todo] Call data costs are probably a bit higher than ZK version with private inputs as well.
However given this is only done every 27 hours, with precompiles it still seems better to implement the light client purely on-chain without ZK since it does not require a trusted setup or remote proving servers, which decrease security.
This should not dominate the constraints but does require writing circuits for Merkle proofs, which use Simple Serialization (SSZ) and SHA-256 (should already exist). This should be easier than zkAttestor since SSZ is simpler than RLP.
We need to elliptic curve add 512
pubkeys and then verify one BLS signature. Unfortunately due to the way ZK circuits handle 'if' statements, there is unlikely to be an optimization to reduce the number of additions based on committee participation rate.
For BLS12-381, EllipticCurveAdd
is currently 11.8K
constraints. Note that this circuit performs both EllipticCurveAddUnequal
and EllipticCurveDouble
since we don't know which addition equation to use each time. There is potential for this to be optimized.
At first pass, the aggregation of pubkeys will take 512 * 11.8K = ~6M
constraints. BLS signature verification (with hash to curve and subgroup checks) is 19.2M
constraints. We can ignore the subgroup check on pubkeys since this can be done once upon addition of each new validator – this saves 789K
constraints. There is some additional overhead that we need to deserialize pubkeys and signatures into point format. For now this puts us in the ballpark of 26M
constraints.
ZK Light Client:
Bridge:
Notes:
~26M
constraints.update.finalized_header
is present. This requires trusting the oracle to collect the best valid header in some way.[..., X-1]
to be already existing in the light client.The point is this is censureship resistant because anyone can generate a proof and send it to the ZK light client smart contract