Manually trigger `LightClientStore` force updates

Replaces `process_slot_for_light_client_store` which force updates the
`LightClientStore` automatically based on `finalized_header` age with
`try_light_client_store_force_update` which may be manually called based
on use case dependent heuristics if light client sync appears stuck.
Not all use cases share the same risk profile.
This commit is contained in:
Etan Kissling 2022-05-03 12:31:26 +02:00
parent 78b035abf9
commit f9d866eb28
No known key found for this signature in database
GPG Key ID: B21DA824C5A3D03D
3 changed files with 29 additions and 30 deletions

View File

@ -29,9 +29,9 @@
- [Light client initialization](#light-client-initialization) - [Light client initialization](#light-client-initialization)
- [`initialize_light_client_store`](#initialize_light_client_store) - [`initialize_light_client_store`](#initialize_light_client_store)
- [Light client state updates](#light-client-state-updates) - [Light client state updates](#light-client-state-updates)
- [`process_slot_for_light_client_store`](#process_slot_for_light_client_store)
- [`validate_light_client_update`](#validate_light_client_update) - [`validate_light_client_update`](#validate_light_client_update)
- [`apply_light_client_update`](#apply_light_client_update) - [`apply_light_client_update`](#apply_light_client_update)
- [`try_light_client_store_force_update`](#try_light_client_store_force_update)
- [`process_light_client_update`](#process_light_client_update) - [`process_light_client_update`](#process_light_client_update)
- [`process_light_client_finality_update`](#process_light_client_finality_update) - [`process_light_client_finality_update`](#process_light_client_finality_update)
- [`process_light_client_optimistic_update`](#process_light_client_optimistic_update) - [`process_light_client_optimistic_update`](#process_light_client_optimistic_update)
@ -285,28 +285,7 @@ def initialize_light_client_store(trusted_block_root: Root,
- **`update: LightClientUpdate`**: Every `update` triggers `process_light_client_update(store, update, current_slot, genesis_validators_root)` where `current_slot` is the current slot based on a local clock. - **`update: LightClientUpdate`**: Every `update` triggers `process_light_client_update(store, update, current_slot, genesis_validators_root)` where `current_slot` is the current slot based on a local clock.
- **`finality_update: LightClientFinalityUpdate`**: Every `finality_update` triggers `process_light_client_finality_update(store, finality_update, current_slot, genesis_validators_root)`. - **`finality_update: LightClientFinalityUpdate`**: Every `finality_update` triggers `process_light_client_finality_update(store, finality_update, current_slot, genesis_validators_root)`.
- **`optimistic_update: LightClientOptimisticUpdate`**: Every `optimistic_update` triggers `process_light_client_optimistic_update(store, optimistic_update, current_slot, genesis_validators_root)`. - **`optimistic_update: LightClientOptimisticUpdate`**: Every `optimistic_update` triggers `process_light_client_optimistic_update(store, optimistic_update, current_slot, genesis_validators_root)`.
- `process_slot_for_light_client_store` is triggered every time the current slot increments. - `try_light_client_store_force_update` MAY be called based on use case dependent heuristics if light client sync appears stuck.
### `process_slot_for_light_client_store`
```python
def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None:
if current_slot % UPDATE_TIMEOUT == 0:
store.previous_max_active_participants = store.current_max_active_participants
store.current_max_active_participants = 0
if (
current_slot > store.finalized_header.slot + UPDATE_TIMEOUT
and store.best_valid_update is not None
):
# Forced best update when the update timeout has elapsed.
# Because the apply logic waits for `finalized_header.slot` to indicate sync committee finality,
# the `attested_header` may be treated as `finalized_header` in extended periods of non-finality
# to guarantee progression into later sync committee periods according to `is_better_update`.
if store.best_valid_update.finalized_header.slot <= store.finalized_header.slot:
store.best_valid_update.finalized_header = store.best_valid_update.attested_header
apply_light_client_update(store, store.best_valid_update)
store.best_valid_update = None
```
### `validate_light_client_update` ### `validate_light_client_update`
@ -399,12 +378,32 @@ def apply_light_client_update(store: LightClientStore, update: LightClientUpdate
elif update_finalized_period == store_period + 1: elif update_finalized_period == store_period + 1:
store.current_sync_committee = store.next_sync_committee store.current_sync_committee = store.next_sync_committee
store.next_sync_committee = update.next_sync_committee store.next_sync_committee = update.next_sync_committee
store.previous_max_active_participants = store.current_max_active_participants
store.current_max_active_participants = 0
if update.finalized_header.slot > store.finalized_header.slot: if update.finalized_header.slot > store.finalized_header.slot:
store.finalized_header = update.finalized_header store.finalized_header = update.finalized_header
if store.finalized_header.slot > store.optimistic_header.slot: if store.finalized_header.slot > store.optimistic_header.slot:
store.optimistic_header = store.finalized_header store.optimistic_header = store.finalized_header
``` ```
### `try_light_client_store_force_update`
```python
def try_light_client_store_force_update(store: LightClientStore, current_slot: Slot) -> None:
if (
current_slot > store.finalized_header.slot + UPDATE_TIMEOUT
and store.best_valid_update is not None
):
# Forced best update when the update timeout has elapsed.
# Because the apply logic waits for `finalized_header.slot` to indicate sync committee finality,
# the `attested_header` may be treated as `finalized_header` in extended periods of non-finality
# to guarantee progression into later sync committee periods according to `is_better_update`.
if store.best_valid_update.finalized_header.slot <= store.finalized_header.slot:
store.best_valid_update.finalized_header = store.best_valid_update.attested_header
apply_light_client_update(store, store.best_valid_update)
store.best_valid_update = None
```
### `process_light_client_update` ### `process_light_client_update`
```python ```python

View File

@ -72,13 +72,13 @@ def get_checks(store):
} }
def emit_slot(test, spec, state): def emit_force_update(test, spec, state):
current_slot = state.slot current_slot = state.slot
spec.process_slot_for_light_client_store(test.store, current_slot) spec.try_light_client_store_force_update(test.store, current_slot)
yield from [] # Consistently enable `yield from` syntax in calling tests yield from [] # Consistently enable `yield from` syntax in calling tests
test.steps.append({ test.steps.append({
"process_slot": { "force_update": {
"current_slot": int(current_slot), "current_slot": int(current_slot),
"checks": get_checks(test.store), "checks": get_checks(test.store),
} }
@ -249,7 +249,7 @@ def test_light_client_sync(spec, state):
# ``` # ```
attested_state = state.copy() attested_state = state.copy()
next_slots(spec, state, spec.UPDATE_TIMEOUT - 1) next_slots(spec, state, spec.UPDATE_TIMEOUT - 1)
yield from emit_slot(test, spec, state) yield from emit_force_update(test, spec, state)
assert test.store.finalized_header.slot == store_state.slot assert test.store.finalized_header.slot == store_state.slot
assert test.store.next_sync_committee == store_state.next_sync_committee assert test.store.next_sync_committee == store_state.next_sync_committee
assert test.store.best_valid_update is None assert test.store.best_valid_update is None
@ -293,7 +293,7 @@ def test_light_client_sync(spec, state):
assert test.store.next_sync_committee == store_state.next_sync_committee assert test.store.next_sync_committee == store_state.next_sync_committee
assert test.store.best_valid_update == update assert test.store.best_valid_update == update
assert test.store.optimistic_header.slot == attested_state.slot assert test.store.optimistic_header.slot == attested_state.slot
yield from emit_slot(test, spec, state) yield from emit_force_update(test, spec, state)
assert test.store.finalized_header.slot == attested_state.slot assert test.store.finalized_header.slot == attested_state.slot
assert test.store.next_sync_committee == attested_state.next_sync_committee assert test.store.next_sync_committee == attested_state.next_sync_committee
assert test.store.best_valid_update is None assert test.store.best_valid_update is None

View File

@ -34,9 +34,9 @@ optimistic_header: {
} }
``` ```
#### `process_slot` execution step #### `force_update` execution step
The function `process_slot_for_light_client_store(store, current_slot)` The function `try_light_client_store_force_update(store, current_slot)`
should be executed with the specified parameters: should be executed with the specified parameters:
```yaml ```yaml