Skip to content

Commit

Permalink
Merge pull request #2805 from etan-status/lc-period
Browse files Browse the repository at this point in the history
Allow light client to verify signatures at period boundary
  • Loading branch information
hwwhww committed Jun 16, 2022
2 parents 3dbbeb9 + 5653649 commit 8cc008d
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 30 deletions.
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

0 comments on commit 8cc008d

Please sign in to comment.