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 1 commit
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
107 changes: 69 additions & 38 deletions specs/altair/sync-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
- [`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)
- [`get_safety_threshold`](#get_safety_threshold)
- [`is_better_update`](#is_better_update)
etan-status marked this conversation as resolved.
Show resolved Hide resolved
- [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 +64,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 @@ -118,19 +118,6 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64:
return uint64(generalized_index % 2**(floorlog2(generalized_index)))
```

### `get_active_header`
etan-status marked this conversation as resolved.
Show resolved Hide resolved

```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
```

### `get_safety_threshold`

```python
Expand All @@ -141,6 +128,50 @@ def get_safety_threshold(store: LightClientStore) -> uint64:
) // 2
```

### `is_better_update`

```python
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 > old_has_finality
etan-status marked this conversation as resolved.
Show resolved Hide resolved

# Compare sync committee finality
if new_has_finality:
new_has_sync_committee_finality = (
compute_sync_committee_period(compute_epoch_at_slot(new_update.finalized_header.slot)) ==
etan-status marked this conversation as resolved.
Show resolved Hide resolved
compute_sync_committee_period(compute_epoch_at_slot(new_update.attested_header.slot))
)
old_has_sync_committee_finality = (
compute_sync_committee_period(compute_epoch_at_slot(old_update.finalized_header.slot)) ==
compute_sync_committee_period(compute_epoch_at_slot(old_update.attested_header.slot))
)
etan-status marked this conversation as resolved.
Show resolved Hide resolved
if new_has_sync_committee_finality != old_has_sync_committee_finality:
return new_has_sync_committee_finality > old_has_sync_committee_finality
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

# 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
```

## 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 @@ -157,6 +188,8 @@ def process_slot_for_light_client_store(store: LightClientStore, current_slot: S
and store.best_valid_update is not None
):
# Forced best update when the update timeout has elapsed
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 +201,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))
assert current_slot >= update.signature_slot > update.attested_header.slot >= update.finalized_header.slot
store_period = compute_sync_committee_period(compute_epoch_at_slot(store.finalized_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 signature_period in (store_period, store_period + 1)

# Verify update is relevant
attested_period = compute_sync_committee_period(compute_epoch_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 +235,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 attested_period == store_period
assert update.next_sync_committee == SyncCommittee()
else:
if update_period == finalized_period:
if 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 signature_period == store_period:
sync_committee = store.current_sync_committee
else:
sync_committee = store.next_sync_committee
Expand All @@ -237,13 +269,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(compute_epoch_at_slot(store.finalized_header.slot))
finalized_period = compute_sync_committee_period(compute_epoch_at_slot(update.finalized_header.slot))
etan-status marked this conversation as resolved.
Show resolved Hide resolved
if 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 +293,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 +313,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])
11 changes: 8 additions & 3 deletions tests/core/pyspec/eth2spec/test/helpers/light_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def initialize_light_client_store(spec, state):
)


def get_sync_aggregate(spec, state, signature_slot=None):
def get_sync_aggregate(spec, state, num_participants=None, signature_slot=None):
# By default, the sync committee signs the previous slot
if signature_slot is None:
signature_slot = state.slot + 1
Expand All @@ -42,13 +42,18 @@ def get_sync_aggregate(spec, state, signature_slot=None):
committee_indices = compute_committee_indices(spec, signature_state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to myself: out of the scope of this PR, compute_committee_indices helper doesn't need spec parameter at all.

committee_size = len(committee_indices)

# By default, use full participation
if num_participants is None:
num_participants = committee_size
assert committee_size >= num_participants >= 0

# Compute sync aggregate
sync_committee_bits = [True] * committee_size
sync_committee_bits = [True] * num_participants + [False] * (committee_size - num_participants)
sync_committee_signature = compute_aggregate_sync_committee_signature(
spec,
signature_state,
signature_slot,
committee_indices,
committee_indices[:num_participants],
)
sync_aggregate = spec.SyncAggregate(
sync_committee_bits=sync_committee_bits,
Expand Down
6 changes: 6 additions & 0 deletions tests/formats/sync_protocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Light client sync protocol tests

This series of tests provides reference test vectors for the light client sync protocol spec.

Handlers:
- `update_ranking`: see [`LightClientUpdate` ranking test format](./update_ranking.md)
Loading