Merge pull request #2805 from etan-status/lc-period
Allow light client to verify signatures at period boundary
This commit is contained in:
commit
8cc008d11c
|
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -56,12 +57,13 @@ 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
|
||||
|
@ -69,6 +71,57 @@ def test_process_light_client_update_not_timeout(spec, state):
|
|||
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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
|
Loading…
Reference in New Issue