Skip to content

Commit 2fa396f

Browse files
authored
Merge pull request #2746 from ethereum/vbuterin-patch-12
Simplify sync protocol and update to calculate optimistic heads
2 parents 4cd2334 + de89238 commit 2fa396f

File tree

5 files changed

+194
-120
lines changed

5 files changed

+194
-120
lines changed

presets/mainnet/altair.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256
2222
# ---------------------------------------------------------------
2323
# 1
2424
MIN_SYNC_COMMITTEE_PARTICIPANTS: 1
25+
# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 32 * 256)
26+
UPDATE_TIMEOUT: 8192

presets/minimal/altair.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8
2222
# ---------------------------------------------------------------
2323
# 1
2424
MIN_SYNC_COMMITTEE_PARTICIPANTS: 1
25+
# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 8 * 8)
26+
UPDATE_TIMEOUT: 64

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ def combine_dicts(old_dict: Dict[str, T], new_dict: Dict[str, T]) -> Dict[str, T
683683
'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256',
684684
'bytes', 'byte', 'ByteList', 'ByteVector',
685685
'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set',
686+
'Optional',
686687
]
687688

688689

specs/altair/sync-protocol.md

Lines changed: 131 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
- [Preset](#preset)
1414
- [Misc](#misc)
1515
- [Containers](#containers)
16-
- [`LightClientSnapshot`](#lightclientsnapshot)
1716
- [`LightClientUpdate`](#lightclientupdate)
1817
- [`LightClientStore`](#lightclientstore)
1918
- [Helper functions](#helper-functions)
2019
- [`get_subtree_index`](#get_subtree_index)
20+
- [`get_active_header`](#get_active_header)
21+
- [`get_safety_threshold`](#get_safety_threshold)
2122
- [Light client state updates](#light-client-state-updates)
23+
- [`process_slot_for_light_client_store`](#process_slot_for_light_client_store)
2224
- [`validate_light_client_update`](#validate_light_client_update)
2325
- [`apply_light_client_update`](#apply_light_client_update)
2426
- [`process_light_client_update`](#process_light_client_update)
@@ -47,38 +49,27 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain.
4749

4850
### Misc
4951

50-
| Name | Value |
51-
| - | - |
52-
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` |
52+
| Name | Value | Notes |
53+
| - | - | - |
54+
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | |
55+
| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours |
5356

5457
## Containers
5558

56-
### `LightClientSnapshot`
57-
58-
```python
59-
class LightClientSnapshot(Container):
60-
# Beacon block header
61-
header: BeaconBlockHeader
62-
# Sync committees corresponding to the header
63-
current_sync_committee: SyncCommittee
64-
next_sync_committee: SyncCommittee
65-
```
66-
6759
### `LightClientUpdate`
6860

6961
```python
7062
class LightClientUpdate(Container):
71-
# Update beacon block header
72-
header: BeaconBlockHeader
73-
# Next sync committee corresponding to the header
63+
# The beacon block header that is attested to by the sync committee
64+
attested_header: BeaconBlockHeader
65+
# Next sync committee corresponding to the active header
7466
next_sync_committee: SyncCommittee
7567
next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)]
76-
# Finality proof for the update header
77-
finality_header: BeaconBlockHeader
68+
# The finalized beacon block header attested to by Merkle branch
69+
finalized_header: BeaconBlockHeader
7870
finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)]
7971
# Sync committee aggregate signature
80-
sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE]
81-
sync_committee_signature: BLSSignature
72+
sync_committee_aggregate: SyncAggregate
8273
# Fork version for the aggregate signature
8374
fork_version: Version
8475
```
@@ -88,8 +79,18 @@ class LightClientUpdate(Container):
8879
```python
8980
@dataclass
9081
class LightClientStore(object):
91-
snapshot: LightClientSnapshot
92-
valid_updates: Set[LightClientUpdate]
82+
# Beacon block header that is finalized
83+
finalized_header: BeaconBlockHeader
84+
# Sync committees corresponding to the header
85+
current_sync_committee: SyncCommittee
86+
next_sync_committee: SyncCommittee
87+
# Best available header to switch finalized head to if we see nothing else
88+
best_valid_update: Optional[LightClientUpdate]
89+
# Most recent available reasonably-safe header
90+
optimistic_header: BeaconBlockHeader
91+
# Max number of active participants in a sync committee (used to calculate safety threshold)
92+
previous_max_active_participants: uint64
93+
current_max_active_participants: uint64
9394
```
9495

9596
## Helper functions
@@ -101,95 +102,157 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
101102
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
102103
```
103104

105+
### `get_active_header`
106+
107+
```python
108+
def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader:
109+
# The "active header" is the header that the update is trying to convince us
110+
# to accept. If a finalized header is present, it's the finalized header,
111+
# otherwise it's the attested header
112+
if update.finalized_header != BeaconBlockHeader():
113+
return update.finalized_header
114+
else:
115+
return update.attested_header
116+
```
117+
118+
### `get_safety_threshold`
119+
120+
```python
121+
def get_safety_threshold(store: LightClientStore) -> uint64:
122+
return max(
123+
store.previous_max_active_participants,
124+
store.current_max_active_participants,
125+
) // 2
126+
```
127+
104128
## Light client state updates
105129

106-
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 current slot based on some local clock.
130+
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 current slot based on some local clock. `process_slot_for_light_client_store` is processed every time the current slot increments.
131+
132+
#### `process_slot_for_light_client_store`
133+
134+
```python
135+
def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None:
136+
if current_slot % UPDATE_TIMEOUT == 0:
137+
store.previous_max_active_participants = store.current_max_active_participants
138+
store.current_max_active_participants = 0
139+
if (
140+
current_slot > store.finalized_header.slot + UPDATE_TIMEOUT
141+
and store.best_valid_update is not None
142+
):
143+
# Forced best update when the update timeout has elapsed
144+
apply_light_client_update(store, store.best_valid_update)
145+
store.best_valid_update = None
146+
```
107147

108148
#### `validate_light_client_update`
109149

110150
```python
111-
def validate_light_client_update(snapshot: LightClientSnapshot,
151+
def validate_light_client_update(store: LightClientStore,
112152
update: LightClientUpdate,
153+
current_slot: Slot,
113154
genesis_validators_root: Root) -> None:
114-
# Verify update slot is larger than snapshot slot
115-
assert update.header.slot > snapshot.header.slot
155+
# Verify update slot is larger than slot of current best finalized header
156+
active_header = get_active_header(update)
157+
assert current_slot >= active_header.slot > store.finalized_header.slot
116158

117159
# Verify update does not skip a sync committee period
118-
snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
119-
update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
120-
assert update_period in (snapshot_period, snapshot_period + 1)
160+
finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
161+
update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
162+
assert update_period in (finalized_period, finalized_period + 1)
121163

122-
# Verify update header root is the finalized root of the finality header, if specified
123-
if update.finality_header == BeaconBlockHeader():
124-
signed_header = update.header
164+
# Verify that the `finalized_header`, if present, actually is the finalized header saved in the
165+
# state of the `attested header`
166+
if update.finalized_header == BeaconBlockHeader():
125167
assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]
126168
else:
127-
signed_header = update.finality_header
128169
assert is_valid_merkle_branch(
129-
leaf=hash_tree_root(update.header),
170+
leaf=hash_tree_root(update.finalized_header),
130171
branch=update.finality_branch,
131172
depth=floorlog2(FINALIZED_ROOT_INDEX),
132173
index=get_subtree_index(FINALIZED_ROOT_INDEX),
133-
root=update.finality_header.state_root,
174+
root=update.attested_header.state_root,
134175
)
135176

136177
# Verify update next sync committee if the update period incremented
137-
if update_period == snapshot_period:
138-
sync_committee = snapshot.current_sync_committee
178+
if update_period == finalized_period:
179+
sync_committee = store.current_sync_committee
139180
assert update.next_sync_committee_branch == [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]
140181
else:
141-
sync_committee = snapshot.next_sync_committee
182+
sync_committee = store.next_sync_committee
142183
assert is_valid_merkle_branch(
143184
leaf=hash_tree_root(update.next_sync_committee),
144185
branch=update.next_sync_committee_branch,
145186
depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX),
146187
index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX),
147-
root=update.header.state_root,
188+
root=active_header.state_root,
148189
)
190+
191+
sync_aggregate = update.sync_committee_aggregate
149192

150193
# Verify sync committee has sufficient participants
151-
assert sum(update.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS
194+
assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS
152195

153196
# Verify sync committee aggregate signature
154-
participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit]
197+
participant_pubkeys = [
198+
pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys)
199+
if bit
200+
]
155201
domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root)
156-
signing_root = compute_signing_root(signed_header, domain)
157-
assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature)
202+
signing_root = compute_signing_root(update.attested_header, domain)
203+
assert bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature)
158204
```
159205

160206
#### `apply_light_client_update`
161207

162208
```python
163-
def apply_light_client_update(snapshot: LightClientSnapshot, update: LightClientUpdate) -> None:
164-
snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
165-
update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
166-
if update_period == snapshot_period + 1:
167-
snapshot.current_sync_committee = snapshot.next_sync_committee
168-
snapshot.next_sync_committee = update.next_sync_committee
169-
snapshot.header = update.header
209+
def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None:
210+
active_header = get_active_header(update)
211+
finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
212+
update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD
213+
if update_period == finalized_period + 1:
214+
store.current_sync_committee = store.next_sync_committee
215+
store.next_sync_committee = update.next_sync_committee
216+
store.finalized_header = active_header
170217
```
171218

172219
#### `process_light_client_update`
173220

174221
```python
175-
def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot,
222+
def process_light_client_update(store: LightClientStore,
223+
update: LightClientUpdate,
224+
current_slot: Slot,
176225
genesis_validators_root: Root) -> None:
177-
validate_light_client_update(store.snapshot, update, genesis_validators_root)
178-
store.valid_updates.add(update)
226+
validate_light_client_update(store, update, current_slot, genesis_validators_root)
179227

180-
update_timeout = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD
228+
sync_committee_bits = update.sync_committee_aggregate.sync_committee_bits
229+
230+
# Update the best update in case we have to force-update to it if the timeout elapses
181231
if (
182-
sum(update.sync_committee_bits) * 3 >= len(update.sync_committee_bits) * 2
183-
and update.finality_header != BeaconBlockHeader()
232+
store.best_valid_update is None
233+
or sum(sync_committee_bits) > sum(store.best_valid_update.sync_committee_aggregate.sync_committee_bits)
184234
):
185-
# Apply update if (1) 2/3 quorum is reached and (2) we have a finality proof.
186-
# Note that (2) means that the current light client design needs finality.
187-
# It may be changed to re-organizable light client design. See the on-going issue consensus-specs#2182.
188-
apply_light_client_update(store.snapshot, update)
189-
store.valid_updates = set()
190-
elif current_slot > store.snapshot.header.slot + update_timeout:
191-
# Forced best update when the update timeout has elapsed
192-
apply_light_client_update(store.snapshot,
193-
max(store.valid_updates, key=lambda update: sum(update.sync_committee_bits)))
194-
store.valid_updates = set()
235+
store.best_valid_update = update
236+
237+
# Track the maximum number of active participants in the committee signatures
238+
store.current_max_active_participants = max(
239+
store.current_max_active_participants,
240+
sum(sync_committee_bits),
241+
)
242+
243+
# Update the optimistic header
244+
if (
245+
sum(sync_committee_bits) > get_safety_threshold(store)
246+
and update.attested_header.slot > store.optimistic_header.slot
247+
):
248+
store.optimistic_header = update.attested_header
249+
250+
# Update finalized header
251+
if (
252+
sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2
253+
and update.finalized_header != BeaconBlockHeader()
254+
):
255+
# Normal update through 2/3 threshold
256+
apply_light_client_update(store, update)
257+
store.best_valid_update = None
195258
```

0 commit comments

Comments
 (0)