Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Root next_sync_committee in attested_header #2932

Merged
merged 8 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 84 additions & 42 deletions specs/altair/sync-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
- [Helper functions](#helper-functions)
- [`is_sync_committee_update`](#is_sync_committee_update)
- [`is_finality_update`](#is_finality_update)
- [`get_subtree_index`](#get_subtree_index)
- [`get_active_header`](#get_active_header)
- [`is_better_update`](#is_better_update)
etan-status marked this conversation as resolved.
Show resolved Hide resolved
- [`get_safety_threshold`](#get_safety_threshold)
- [`get_subtree_index`](#get_subtree_index)
- [`compute_sync_committee_period_at_slot`](#compute_sync_committee_period_at_slot)
- [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)
Expand Down Expand Up @@ -64,7 +65,7 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain.
class LightClientUpdate(Container):
# The beacon block header that is attested to by the sync committee
attested_header: BeaconBlockHeader
# Next sync committee corresponding to the active header
# Next sync committee corresponding to `attested_header`
next_sync_committee: SyncCommittee
next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)]
# The finalized beacon block header attested to by Merkle branch
Expand Down Expand Up @@ -111,24 +112,48 @@ def is_finality_update(update: LightClientUpdate) -> bool:
return update.finality_branch != [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]
```

### `get_subtree_index`
### `is_better_update`

```python
def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
```
def is_better_update(new_update: LightClientUpdate, old_update: LightClientUpdate) -> bool:
etan-status marked this conversation as resolved.
Show resolved Hide resolved
# Compare supermajority (> 2/3) sync committee participation
max_active_participants = len(new_update.sync_aggregate.sync_committee_bits)
new_num_active_participants = sum(new_update.sync_aggregate.sync_committee_bits)
old_num_active_participants = sum(old_update.sync_aggregate.sync_committee_bits)
new_has_supermajority = new_num_active_participants * 3 >= max_active_participants * 2
old_has_supermajority = old_num_active_participants * 3 >= max_active_participants * 2
if new_has_supermajority != old_has_supermajority:
return new_has_supermajority > old_has_supermajority
if not new_has_supermajority and new_num_active_participants != old_num_active_participants:
return new_num_active_participants > old_num_active_participants
hwwhww marked this conversation as resolved.
Show resolved Hide resolved

# Compare indication of any finality
new_has_finality = is_finality_update(new_update)
old_has_finality = is_finality_update(old_update)
if new_has_finality != old_has_finality:
return new_has_finality

# Compare sync committee finality
if new_has_finality:
new_has_sync_committee_finality = (
compute_sync_committee_period_at_slot(new_update.finalized_header.slot)
== compute_sync_committee_period_at_slot(new_update.attested_header.slot)
)
old_has_sync_committee_finality = (
compute_sync_committee_period_at_slot(old_update.finalized_header.slot)
== compute_sync_committee_period_at_slot(old_update.attested_header.slot)
)
if new_has_sync_committee_finality != old_has_sync_committee_finality:
return new_has_sync_committee_finality

### `get_active_header`
etan-status marked this conversation as resolved.
Show resolved Hide resolved
# Tiebreaker 1: Sync committee participation beyond supermajority
if new_num_active_participants != old_num_active_participants:
return new_num_active_participants > old_num_active_participants

```python
def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader:
# The "active header" is the header that the update is trying to convince us
# to accept. If a finalized header is present, it's the finalized header,
# otherwise it's the attested header
if is_finality_update(update):
return update.finalized_header
else:
return update.attested_header
# Tiebreaker 2: Prefer older data (fewer changes to best)
if new_update.attested_header.slot != old_update.attested_header.slot:
return new_update.attested_header.slot < old_update.attested_header.slot
return new_update.signature_slot < old_update.signature_slot
```

### `get_safety_threshold`
Expand All @@ -141,6 +166,20 @@ def get_safety_threshold(store: LightClientStore) -> uint64:
) // 2
```

### `get_subtree_index`

```python
def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
```

### `compute_sync_committee_period_at_slot`

```python
def compute_sync_committee_period_at_slot(slot: Slot) -> uint64:
return compute_sync_committee_period(compute_epoch_at_slot(slot))
```

## 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, genesis_validators_root)` where `current_slot` is the current slot based on a local clock. `process_slot_for_light_client_store` is triggered every time the current slot increments.
Expand All @@ -156,7 +195,12 @@ def process_slot_for_light_client_store(store: LightClientStore, current_slot: S
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
# 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
hwwhww marked this conversation as resolved.
Show resolved Hide resolved
apply_light_client_update(store, store.best_valid_update)
store.best_valid_update = None
```
Expand All @@ -168,15 +212,19 @@ def validate_light_client_update(store: LightClientStore,
update: LightClientUpdate,
current_slot: Slot,
genesis_validators_root: Root) -> None:
# Verify update slot is larger than slot of current best finalized header
active_header = get_active_header(update)
assert current_slot >= update.signature_slot > active_header.slot > store.finalized_header.slot
# Verify sync committee has sufficient participants
sync_aggregate = update.sync_aggregate
assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS

# Verify update does not skip a sync committee period
finalized_period = compute_sync_committee_period(compute_epoch_at_slot(store.finalized_header.slot))
update_period = compute_sync_committee_period(compute_epoch_at_slot(active_header.slot))
signature_period = compute_sync_committee_period(compute_epoch_at_slot(update.signature_slot))
assert signature_period in (finalized_period, finalized_period + 1)
assert current_slot >= update.signature_slot > update.attested_header.slot >= update.finalized_header.slot
store_period = compute_sync_committee_period_at_slot(store.finalized_header.slot)
update_signature_period = compute_sync_committee_period_at_slot(update.signature_slot)
assert update_signature_period in (store_period, store_period + 1)

# Verify update is relevant
update_attested_period = compute_sync_committee_period_at_slot(update.attested_header.slot)
assert update.attested_header.slot > store.finalized_header.slot

# Verify that the `finality_branch`, if present, confirms `finalized_header`
# to match the finalized checkpoint root saved in the state of `attested_header`.
Expand All @@ -198,28 +246,23 @@ def validate_light_client_update(store: LightClientStore,
)

# Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the
# state of the `active_header`
# state of the `attested_header`
if not is_sync_committee_update(update):
assert update_period == finalized_period
assert update_attested_period == store_period
assert update.next_sync_committee == SyncCommittee()
else:
if update_period == finalized_period:
if update_attested_period == store_period:
assert update.next_sync_committee == store.next_sync_committee
assert is_valid_merkle_branch(
leaf=hash_tree_root(update.next_sync_committee),
branch=update.next_sync_committee_branch,
depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX),
index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX),
root=active_header.state_root,
root=update.attested_header.state_root,
)

sync_aggregate = update.sync_aggregate

# Verify sync committee has sufficient participants
assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS

# Verify sync committee aggregate signature
if signature_period == finalized_period:
if update_signature_period == store_period:
sync_committee = store.current_sync_committee
else:
sync_committee = store.next_sync_committee
Expand All @@ -237,13 +280,12 @@ def validate_light_client_update(store: LightClientStore,

```python
def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None:
etan-status marked this conversation as resolved.
Show resolved Hide resolved
active_header = get_active_header(update)
finalized_period = compute_sync_committee_period(compute_epoch_at_slot(store.finalized_header.slot))
update_period = compute_sync_committee_period(compute_epoch_at_slot(active_header.slot))
if update_period == finalized_period + 1:
store_period = compute_sync_committee_period_at_slot(store.finalized_header.slot)
update_finalized_period = compute_sync_committee_period_at_slot(update.finalized_header.slot)
if update_finalized_period == store_period + 1:
store.current_sync_committee = store.next_sync_committee
store.next_sync_committee = update.next_sync_committee
store.finalized_header = active_header
store.finalized_header = update.finalized_header
if store.finalized_header.slot > store.optimistic_header.slot:
store.optimistic_header = store.finalized_header
```
Expand All @@ -262,7 +304,7 @@ def process_light_client_update(store: LightClientStore,
# Update the best update in case we have to force-update to it if the timeout elapses
if (
store.best_valid_update is None
or sum(sync_committee_bits) > sum(store.best_valid_update.sync_aggregate.sync_committee_bits)
or is_better_update(update, store.best_valid_update)
):
store.best_valid_update = update

Expand All @@ -282,7 +324,7 @@ def process_light_client_update(store: LightClientStore,
# Update finalized header
if (
sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2
and is_finality_update(update)
and update.finalized_header.slot > store.finalized_header.slot
etan-status marked this conversation as resolved.
Show resolved Hide resolved
):
# Normal update through 2/3 threshold
apply_light_client_update(store, update)
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from eth2spec.test.context import (
spec_state_test,
with_presets,
with_altair_and_later,
)
from eth2spec.test.helpers.attestations import (
next_slots_with_attestations,
state_transition_with_full_block,
)
from eth2spec.test.helpers.constants import MINIMAL
from eth2spec.test.helpers.light_client import (
get_sync_aggregate,
signed_block_to_header,
)
from eth2spec.test.helpers.state import (
next_slots,
)
from eth2spec.test.helpers.merkle import build_proof
from math import floor


def create_update(spec, test, with_next_sync_committee, with_finality, participation_rate):
attested_state, attested_block, finalized_block = test
num_participants = floor(spec.SYNC_COMMITTEE_SIZE * participation_rate)

attested_header = signed_block_to_header(spec, attested_block)

if with_next_sync_committee:
next_sync_committee = attested_state.next_sync_committee
next_sync_committee_branch = build_proof(attested_state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX)
else:
next_sync_committee = spec.SyncCommittee()
next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))]

if with_finality:
finalized_header = signed_block_to_header(spec, finalized_block)
finality_branch = build_proof(attested_state.get_backing(), spec.FINALIZED_ROOT_INDEX)
else:
finalized_header = spec.BeaconBlockHeader()
finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))]

sync_aggregate, signature_slot = get_sync_aggregate(spec, attested_state, num_participants)

return spec.LightClientUpdate(
attested_header=attested_header,
next_sync_committee=next_sync_committee,
next_sync_committee_branch=next_sync_committee_branch,
finalized_header=finalized_header,
finality_branch=finality_branch,
sync_aggregate=sync_aggregate,
signature_slot=signature_slot,
)


@with_altair_and_later
@spec_state_test
@with_presets([MINIMAL], reason="too slow")
def test_update_ranking(spec, state):
# Set up blocks and states:
# - `sig_finalized` / `sig_attested` --> Only signature in next sync committee period
# - `att_finalized` / `att_attested` --> Attested header also in next sync committee period
# - `fin_finalized` / `fin_attested` --> Finalized header also in next sync committee period
# - `lat_finalized` / `lat_attested` --> Like `fin`, but at a later `attested_header.slot`
next_slots(spec, state, spec.compute_start_slot_at_epoch(spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD - 3) - 1)
sig_finalized_block = state_transition_with_full_block(spec, state, True, True)
_, _, state = next_slots_with_attestations(spec, state, spec.SLOTS_PER_EPOCH - 1, True, True)
att_finalized_block = state_transition_with_full_block(spec, state, True, True)
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH - 2, True, True)
sig_attested_block = state_transition_with_full_block(spec, state, True, True)
sig_attested_state = state.copy()
att_attested_block = state_transition_with_full_block(spec, state, True, True)
att_attested_state = state.copy()
fin_finalized_block = att_attested_block
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH - 1, True, True)
fin_attested_block = state_transition_with_full_block(spec, state, True, True)
fin_attested_state = state.copy()
lat_finalized_block = fin_finalized_block
lat_attested_block = state_transition_with_full_block(spec, state, True, True)
lat_attested_state = state.copy()
sig = (sig_attested_state, sig_attested_block, sig_finalized_block)
att = (att_attested_state, att_attested_block, att_finalized_block)
fin = (fin_attested_state, fin_attested_block, fin_finalized_block)
lat = (lat_attested_state, lat_attested_block, lat_finalized_block)

# Create updates (in descending order of quality)
updates = [
# Updates with sync committee finality
create_update(spec, sig, with_next_sync_committee=0, with_finality=1, participation_rate=1.0),
create_update(spec, fin, with_next_sync_committee=1, with_finality=1, participation_rate=1.0),
create_update(spec, lat, with_next_sync_committee=1, with_finality=1, participation_rate=1.0),
create_update(spec, sig, with_next_sync_committee=0, with_finality=1, participation_rate=0.8),
create_update(spec, fin, with_next_sync_committee=1, with_finality=1, participation_rate=0.8),
create_update(spec, lat, with_next_sync_committee=1, with_finality=1, participation_rate=0.8),

# Updates without sync committee finality
create_update(spec, att, with_next_sync_committee=1, with_finality=1, participation_rate=1.0),
create_update(spec, att, with_next_sync_committee=1, with_finality=1, participation_rate=0.8),

# Updates without indication of any finality
create_update(spec, sig, with_next_sync_committee=0, with_finality=0, participation_rate=1.0),
create_update(spec, att, with_next_sync_committee=1, with_finality=0, participation_rate=1.0),
create_update(spec, fin, with_next_sync_committee=1, with_finality=0, participation_rate=1.0),
create_update(spec, lat, with_next_sync_committee=1, with_finality=0, participation_rate=1.0),
create_update(spec, sig, with_next_sync_committee=0, with_finality=0, participation_rate=0.8),
create_update(spec, att, with_next_sync_committee=1, with_finality=0, participation_rate=0.8),
create_update(spec, fin, with_next_sync_committee=1, with_finality=0, participation_rate=0.8),
create_update(spec, lat, with_next_sync_committee=1, with_finality=0, participation_rate=0.8),

# Updates with low sync committee participation
create_update(spec, sig, with_next_sync_committee=0, with_finality=1, participation_rate=0.4),
create_update(spec, fin, with_next_sync_committee=1, with_finality=1, participation_rate=0.4),
create_update(spec, lat, with_next_sync_committee=1, with_finality=1, participation_rate=0.4),
create_update(spec, att, with_next_sync_committee=1, with_finality=1, participation_rate=0.4),
create_update(spec, sig, with_next_sync_committee=0, with_finality=0, participation_rate=0.4),
create_update(spec, att, with_next_sync_committee=1, with_finality=0, participation_rate=0.4),
create_update(spec, fin, with_next_sync_committee=1, with_finality=0, participation_rate=0.4),
create_update(spec, lat, with_next_sync_committee=1, with_finality=0, participation_rate=0.4),

# Updates with very low sync committee participation
create_update(spec, sig, with_next_sync_committee=0, with_finality=1, participation_rate=0.2),
create_update(spec, fin, with_next_sync_committee=1, with_finality=1, participation_rate=0.2),
create_update(spec, lat, with_next_sync_committee=1, with_finality=1, participation_rate=0.2),
create_update(spec, att, with_next_sync_committee=1, with_finality=1, participation_rate=0.2),
create_update(spec, sig, with_next_sync_committee=0, with_finality=0, participation_rate=0.2),
create_update(spec, att, with_next_sync_committee=1, with_finality=0, participation_rate=0.2),
create_update(spec, fin, with_next_sync_committee=1, with_finality=0, participation_rate=0.2),
create_update(spec, lat, with_next_sync_committee=1, with_finality=0, participation_rate=0.2),
]
yield "updates", updates

for i in range(len(updates) - 1):
assert spec.is_better_update(updates[i], updates[i + 1])
Loading