Revamp minimal light client (lots of polish and some bug fixes)

This commit is contained in:
Justin 2020-11-17 12:42:09 +00:00 committed by GitHub
parent 692a0aaaa5
commit 99219c874f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,4 +1,4 @@
# Minimal Light Client Design # Minimal Light Client
**Notice**: This document is a work-in-progress for researchers and implementers. **Notice**: This document is a work-in-progress for researchers and implementers.
@ -11,122 +11,151 @@
- [Introduction](#introduction) - [Introduction](#introduction)
- [Constants](#constants) - [Constants](#constants)
- [Configuration](#configuration)
- [Misc](#misc)
- [Time parameters](#time-parameters)
- [Containers](#containers) - [Containers](#containers)
- [`LightClientUpdate`](#lightclientupdate) - [`LightClientSnapshot`](#lightclientsnapshot)
- [Helpers](#helpers) - [`LightClientUpdate`](#lightclientupdate)
- [`LightClientMemory`](#lightclientmemory) - [`LightClientStore`](#lightclientstore)
- [Light client state updates](#light-client-state-updates) - [Light client state updates](#light-client-state-updates)
- [`validate_update`](#validate_update) - [`is_valid_light_client_update`](#is_valid_light_client_update)
- [`update_memory`](#update_memory) - [`process_light_client_update`](#process_light_client_update)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- /TOC --> <!-- /TOC -->
## Introduction ## Introduction
Ethereum 2.0 is designed to be light client friendly. This allows low-resource clients such as mobile phones to access Ethereum 2.0 with reasonable safety and liveness. It also facilitates the development of "bridges" to external blockchains. This document suggests a minimal light client design for the beacon chain that uses the concept of "sync committees" introduced in [./beacon-chain.md](the the light-client-friendliness beacon chain extension). Eth2 is designed to be light client friendly for constrained environments to access Eth2 with reasonable satefy and liveness. Such environments include resource-constrained devices (e.g. phones for trust-minimised wallets) and metered VMs (e.g. blockchain VMs for cross-chain bridges).
This document suggests a minimal light client design for the beacon chain that uses sync committees introduced in [this beacon chain extension](./beacon-chain.md).
## Constants ## Constants
| Name | Value | | Name | Value |
| - | - | | - | - |
| `SYNC_COMMITTEES_GENERALIZED_INDEX` | `GeneralizedIndexConcat(GeneralizedIndex(BeaconBlock, 'state_root'), GeneralizedIndex(BeaconState, 'current_sync_committee'))` | | `NEXT_SYNC_COMMITTEE_INDEX` | `IndexConcat(Index(BeaconBlock, 'state_root'), Index(BeaconState, 'next_sync_committee'))` |
| `FORK_GENERALIZED_INDEX` | `GeneralizedIndexConcat(GeneralizedIndex(BeaconBlock, 'state_root'), GeneralizedIndex(BeaconState, 'fork'))` | | `FORK_INDEX` | `IndexConcat(Index(BeaconBlock, 'state_root'), Index(BeaconState, 'fork'))` |
## Configuration
### Misc
| Name | Value |
| - | - |
| `MAX_VALID_LIGHT_CLIENT_UPDATES` | `uint64(2**64 - 1)` |
### Time parameters
| Name | Value | Unit | Duration |
| - | - | :-: | :-: |
| `LIGHT_CLIENT_UPDATE_TIMEOUT` | `Slot(2**13)` | slots | ~27 hours |
## Containers ## Containers
### `LightClientUpdate` #### `LightClientSnapshot`
```python ```python
class LightClientUpdate(Container): class LightClientSnapshot(Container):
# Updated beacon header # Beacon block header
header: BeaconBlockHeader header: BeaconBlockHeader
# Sync committee signature to that header # Fork data corresponding to the header
aggregation_bits: Bitlist[MAX_SYNC_COMMITTEE_SIZE] fork: Fork
signature: BLSSignature # Sync committees corresponding to the header
# Updates fork version
new_fork: Fork
fork_branch: Vector[Bytes32, log_2(FORK_GENERALIZED_INDEX)]
# Updated sync committee (and authenticating branch)
new_current_sync_committee: SyncCommittee
new_next_sync_committee: SyncCommittee
sync_committee_branch: Vector[Bytes32, log_2(SYNC_COMMITTEES_GENERALIZED_INDEX)]
```
## Helpers
### `LightClientMemory`
```python
class LightClientMemory(Container):
# Beacon header which is not expected to revert
header: BeaconBlockHeader
# Fork version data
fork_version: Version
# sync committees corresponding to the beacon header
current_sync_committee: SyncCommittee current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee next_sync_committee: SyncCommittee
``` ```
## Light client state updates #### `LightClientUpdate`
The state of a light client is stored in a `memory` object of type `LightClientMemory`. To advance its state a light client requests an `update` object of type `LightClientUpdate` from the network by sending a request containing `(memory.shard, memory.header.slot, slot_range_end)`. It calls `validate_update(memory, update)` on each update that it receives in response. If `sum(update.aggregate_bits) * 3 > len(update.aggregate_bits) * 2` for any valid update, it accepts that update immediately; otherwise, it waits around for some time and then finally calls `update_memory(memory, update)` on the valid update with the highest `sum(update.aggregate_bits)`.
#### `validate_update`
```python ```python
def validate_update(memory: LightClientMemory, update: LightClientUpdate) -> bool: class LightClientUpdate(Container):
# Verify the update does not skip a period # Updated snapshot
current_period = compute_epoch_at_slot(memory.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD snapshot: LightClientSnapshot
new_epoch = compute_epoch_of_shard_slot(update.header.slot) # Merkle branches for the updated snapshot
new_period = new_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD fork_branch: Vector[Bytes32, log_2(FORK_INDEX)]
assert new_period in (current_period, current_period + 1) next_sync_committee_branch: Vector[Bytes32, log_2(NEXT_SYNC_COMMITTEE_INDEX)]
# Sync committee aggregate signature
# Verify that it actually updates to a newer slot sync_committee_bits: Bitlist[MAX_SYNC_COMMITTEE_SIZE]
assert update.header.slot > memory.header.slot sync_committee_signature: BLSSignature
```
# Independent variable for convenience
committee = memory.current_sync_committee if new_period == current_period else memory.next_sync_committee
assert len(update.aggregation_bits) == len(committee)
# Verify signature
active_pubkeys = [p for (bit, p) in zip(update.aggregation_bits, committee.pubkeys) if bit]
domain = compute_domain(DOMAIN_SYNC_COMMITTEE, memory.fork_version)
signing_root = compute_signing_root(update.header, domain)
assert bls.FastAggregateVerify(pubkeys, signing_root, update.signature)
# Verify Merkle branches of new info #### `LightClientStore`
```python
class LightClientStore(Container):
snapshot: LightClientSnapshot
valid_updates: List[LightClientUpdate, MAX_VALID_LIGHT_CLIENT_UPDATES]
```
## Light client state updates
A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the currect slot based on some local clock.
#### `is_valid_light_client_update`
```python
def is_valid_light_client_update(store: LightClientStore, update: LightClientUpdate) -> bool:
# Verify new slot is larger than old slot
old_snapshot = store.snapshot
new_snapshot = update.snapshot
assert new_snapshot.header.slot > old_snapshot.header.slot
# Verify update does not skip a sync committee period
old_period = compute_epoch_at_slot(old_snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
new_period = compute_epoch_at_slot(new_snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
assert new_period in (old_period, old_period + 1)
# Verify new snapshot sync committees
if new_period == old_period:
assert new_snapshot.current_sync_committee == old_snapshot.current_sync_committee
assert new_snapshot.next_sync_committee == old_snapshot.next_sync_committee
else new_period == old_period + 1:
assert new_snapshot.current_sync_committee == old_snapshot.next_sync_committee
assert is_valid_merkle_branch(
leaf=hash_tree_root(new_snapshot.next_sync_committee),
branch=update.next_sync_committee_branch,
depth=log2(NEXT_SYNC_COMMITTEE_INDEX),
index=NEXT_SYNC_COMMITTEE_INDEX % 2**log2(NEXT_SYNC_COMMITTEE_INDEX),
root=hash_tree_root(new_snapshot.header),
)
# Verify new snapshot fork
assert is_valid_merkle_branch( assert is_valid_merkle_branch(
leaf=hash_tree_root(update.new_fork), leaf=hash_tree_root(new_snapshot.fork),
branch=update.fork_branch, branch=update.fork_branch,
depth=log2(FORK_GENERALIZED_INDEX), depth=log2(FORK_INDEX),
index=FORK_GENERALIZED_INDEX % 2**log2(FORK_GENERALIZED_INDEX), index=FORK_INDEX % 2**log2(FORK_INDEX),
root=hash_tree_root(update.header), root=hash_tree_root(new_snapshot.header),
) )
assert is_valid_merkle_branch(
leaf=hash_tree_root(update.current_sync_committee), # Verify sync committee bitfield length
branch=update.sync_committee_branch, sync_committee = new_snapshot.current_sync_committee
depth=log2(SYNC_COMMITTEES_GENERALIZED_INDEX), assert len(update.sync_committee_bits) == len(sync_committee)
index=SYNC_COMMITTEES_GENERALIZED_INDEX % 2**log2(SYNC_COMMITTEES_GENERALIZED_INDEX),
root=hash_tree_root(update.header), # Verify sync committee aggregate signature
) participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit]
# Verify consistency of committees domain = compute_domain(DOMAIN_SYNC_COMMITTEE, fork_version.current_version)
if new_period == current_period: signing_root = compute_signing_root(new_snapshot.header, domain)
assert update.new_current_sync_committee == memory.current_sync_committee assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature)
assert update.new_next_sync_committee == memory.next_sync_committee
else:
assert update.new_current_sync_committee == memory.next_sync_committee
return True return True
``` ```
#### `update_memory` #### `process_update`
```python ```python
def update_memory(memory: LightClientMemory, update: LightClientUpdate) -> None: def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot) -> None:
memory.header = update.header assert is_valid_light_client_update(store, update)
epoch = compute_epoch_at_slot(update.header.slot) if sum(update.sync_committee_bits) * 3 > len(update.sync_committee_bits) * 2:
memory.fork_version = update.new_fork.previous_version if epoch < update.new_fork.epoch else update.new_fork.current_version store.snapshot = update.snapshot
memory.current_sync_committee = update.new_current_sync_committee valid_updates = []
memory.next_sync_committee == update.new_next_sync_committee else:
valid_updates.append(update)
# Force an update after the update timeout has elapsed
if current_slot > old_snapshot.header.slot + LIGHT_CLIENT_UPDATE_TIMEOUT:
best_update = max(valid_updates, key=lambda update: sum(update.sync_committee_bits))
store.snapshot = best_update.new_snapshot
``` ```