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

Allow light client to verify signatures at period boundary #2805

Merged
merged 1 commit into from
Jun 16, 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
17 changes: 11 additions & 6 deletions specs/altair/sync-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain.

| Name | Value | Unit | Duration |
| - | - | - | - |
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | validators |
| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | validators | |
| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | slots | ~27.3 hours |

## Containers
Expand All @@ -73,6 +73,8 @@ class LightClientUpdate(Container):
sync_aggregate: SyncAggregate
# Fork version for the aggregate signature
fork_version: Version
# Slot at which the aggregate signature was created (untrusted)
signature_slot: Slot
```

### `LightClientStore`
Expand Down Expand Up @@ -162,15 +164,16 @@ def validate_light_client_update(store: LightClientStore,
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 >= active_header.slot > store.finalized_header.slot
assert current_slot >= update.signature_slot > active_header.slot > store.finalized_header.slot

# 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 update_period in (finalized_period, finalized_period + 1)
signature_period = compute_sync_committee_period(compute_epoch_at_slot(update.signature_slot))
assert signature_period in (finalized_period, finalized_period + 1)

# Verify that the `finalized_header`, if present, actually is the finalized header saved in the
# state of the `attested header`
# state of the `attested_header`
if not is_finality_update(update):
assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]
else:
Expand All @@ -184,10 +187,8 @@ def validate_light_client_update(store: LightClientStore,

# Verify update next sync committee if the update period incremented
if update_period == finalized_period:
sync_committee = store.current_sync_committee
assert update.next_sync_committee_branch == [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]
else:
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,
Expand All @@ -202,6 +203,10 @@ def validate_light_client_update(store: LightClientStore,
assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS

# Verify sync committee aggregate signature
if signature_period == finalized_period:
sync_committee = store.current_sync_committee
else:
sync_committee = store.next_sync_committee
participant_pubkeys = [
pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys)
if bit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ def test_process_light_client_update_not_timeout(spec, state):
state_root=signed_block.message.state_root,
body_root=signed_block.message.body.hash_tree_root(),
)
# Sync committee signing the header
sync_aggregate = get_sync_aggregate(spec, state, block_header, block_root=None)

# Sync committee signing the block_header
sync_aggregate, fork_version, signature_slot = get_sync_aggregate(spec, state, block_header)
next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))]

# Ensure that finality checkpoint is genesis
Expand All @@ -56,19 +57,71 @@ def test_process_light_client_update_not_timeout(spec, state):
finalized_header=finality_header,
finality_branch=finality_branch,
sync_aggregate=sync_aggregate,
fork_version=state.fork.current_version,
fork_version=fork_version,
signature_slot=signature_slot,
)

pre_store = deepcopy(store)

spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root)
spec.process_light_client_update(store, update, signature_slot, state.genesis_validators_root)

assert store.current_max_active_participants > 0
assert store.optimistic_header == update.attested_header
assert store.finalized_header == pre_store.finalized_header
assert store.best_valid_update == update


@with_altair_and_later
@spec_state_test
@with_presets([MINIMAL], reason="too slow")
def test_process_light_client_update_at_period_boundary(spec, state):
store = initialize_light_client_store(spec, state)

# Forward to slot before next sync committee period so that next block is final one in period
next_slots(spec, state, spec.UPDATE_TIMEOUT - 2)
snapshot_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(store.optimistic_header.slot))
update_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(state.slot))
assert snapshot_period == update_period

block = build_empty_block_for_next_slot(spec, state)
signed_block = state_transition_and_sign_block(spec, state, block)
block_header = spec.BeaconBlockHeader(
slot=signed_block.message.slot,
proposer_index=signed_block.message.proposer_index,
parent_root=signed_block.message.parent_root,
state_root=signed_block.message.state_root,
body_root=signed_block.message.body.hash_tree_root(),
)

# Sync committee signing the block_header
sync_aggregate, fork_version, signature_slot = get_sync_aggregate(spec, state, block_header)
next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))]

# Finality is unchanged
finality_header = spec.BeaconBlockHeader()
finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))]

update = spec.LightClientUpdate(
attested_header=block_header,
next_sync_committee=state.next_sync_committee,
next_sync_committee_branch=next_sync_committee_branch,
finalized_header=finality_header,
finality_branch=finality_branch,
sync_aggregate=sync_aggregate,
fork_version=fork_version,
signature_slot=signature_slot,
)

pre_store = deepcopy(store)

spec.process_light_client_update(store, update, signature_slot, state.genesis_validators_root)

assert store.current_max_active_participants > 0
assert store.optimistic_header == update.attested_header
assert store.best_valid_update == update
assert store.finalized_header == pre_store.finalized_header


@with_altair_and_later
@spec_state_test
@with_presets([MINIMAL], reason="too slow")
Expand All @@ -91,9 +144,8 @@ def test_process_light_client_update_timeout(spec, state):
body_root=signed_block.message.body.hash_tree_root(),
)

# Sync committee signing the finalized_block_header
sync_aggregate = get_sync_aggregate(
spec, state, block_header, block_root=spec.Root(block_header.hash_tree_root()))
# Sync committee signing the block_header
sync_aggregate, fork_version, signature_slot = get_sync_aggregate(spec, state, block_header)

# Sync committee is updated
next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX)
Expand All @@ -108,12 +160,13 @@ def test_process_light_client_update_timeout(spec, state):
finalized_header=finality_header,
finality_branch=finality_branch,
sync_aggregate=sync_aggregate,
fork_version=state.fork.current_version,
fork_version=fork_version,
signature_slot=signature_slot,
)

pre_store = deepcopy(store)

spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root)
spec.process_light_client_update(store, update, signature_slot, state.genesis_validators_root)

assert store.current_max_active_participants > 0
assert store.optimistic_header == update.attested_header
Expand Down Expand Up @@ -157,9 +210,8 @@ def test_process_light_client_update_finality_updated(spec, state):
body_root=block.body.hash_tree_root(),
)

# Sync committee signing the finalized_block_header
sync_aggregate = get_sync_aggregate(
spec, state, block_header, block_root=spec.Root(block_header.hash_tree_root()))
# Sync committee signing the block_header
sync_aggregate, fork_version, signature_slot = get_sync_aggregate(spec, state, block_header)

update = spec.LightClientUpdate(
attested_header=block_header,
Expand All @@ -168,10 +220,11 @@ def test_process_light_client_update_finality_updated(spec, state):
finalized_header=finalized_block_header,
finality_branch=finality_branch,
sync_aggregate=sync_aggregate,
fork_version=state.fork.current_version,
fork_version=fork_version,
signature_slot=signature_slot,
)

spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root)
spec.process_light_client_update(store, update, signature_slot, state.genesis_validators_root)

assert store.current_max_active_participants > 0
assert store.optimistic_header == update.attested_header
Expand Down
34 changes: 24 additions & 10 deletions tests/core/pyspec/eth2spec/test/helpers/light_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from eth2spec.test.helpers.state import (
transition_to,
)
from eth2spec.test.helpers.sync_committee import (
compute_aggregate_sync_committee_signature,
compute_committee_indices,
)


Expand All @@ -15,21 +19,31 @@ def initialize_light_client_store(spec, state):
)


def get_sync_aggregate(spec, state, block_header, block_root=None, signature_slot=None):
def get_sync_aggregate(spec, state, block_header, signature_slot=None):
# By default, the sync committee signs the previous slot
if signature_slot is None:
signature_slot = block_header.slot
signature_slot = block_header.slot + 1

# Ensure correct sync committee and fork version are selected
signature_state = state.copy()
transition_to(spec, signature_state, signature_slot)

# Fetch sync committee
committee_indices = compute_committee_indices(spec, signature_state)
committee_size = len(committee_indices)

all_pubkeys = [v.pubkey for v in state.validators]
committee = [all_pubkeys.index(pubkey) for pubkey in state.current_sync_committee.pubkeys]
sync_committee_bits = [True] * len(committee)
# Compute sync aggregate
sync_committee_bits = [True] * committee_size
sync_committee_signature = compute_aggregate_sync_committee_signature(
spec,
state,
block_header.slot,
committee,
block_root=block_root,
signature_state,
signature_slot,
committee_indices,
block_root=spec.Root(block_header.hash_tree_root()),
)
return spec.SyncAggregate(
sync_aggregate = spec.SyncAggregate(
sync_committee_bits=sync_committee_bits,
sync_committee_signature=sync_committee_signature,
)
fork_version = signature_state.fork.current_version
return sync_aggregate, fork_version, signature_slot