mirror of
https://github.com/status-im/eth2.0-specs.git
synced 2025-02-03 06:13:31 +00:00
Add functions for deriving light client data
Adds `create_light_client_bootstrap` and `create_light_client_update` functions as a reference implementation for serving light client data. This also enables a new test harness to verify that light client data gets applied to a `LightClientStore` as expected.
This commit is contained in:
parent
3b6222d74e
commit
86fe93ca96
1
setup.py
1
setup.py
@ -457,6 +457,7 @@ class AltairSpecBuilder(Phase0SpecBuilder):
|
|||||||
from typing import NewType, Union as PyUnion
|
from typing import NewType, Union as PyUnion
|
||||||
|
|
||||||
from eth2spec.phase0 import {preset_name} as phase0
|
from eth2spec.phase0 import {preset_name} as phase0
|
||||||
|
from eth2spec.test.helpers.merkle import build_proof
|
||||||
from eth2spec.utils.ssz.ssz_typing import Path
|
from eth2spec.utils.ssz.ssz_typing import Path
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -31,6 +31,9 @@
|
|||||||
- [`validate_light_client_update`](#validate_light_client_update)
|
- [`validate_light_client_update`](#validate_light_client_update)
|
||||||
- [`apply_light_client_update`](#apply_light_client_update)
|
- [`apply_light_client_update`](#apply_light_client_update)
|
||||||
- [`process_light_client_update`](#process_light_client_update)
|
- [`process_light_client_update`](#process_light_client_update)
|
||||||
|
- [Deriving light client data](#deriving-light-client-data)
|
||||||
|
- [`create_light_client_bootstrap`](#create_light_client_bootstrap)
|
||||||
|
- [`create_light_client_update`](#create_light_client_update)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- /TOC -->
|
<!-- /TOC -->
|
||||||
@ -417,3 +420,105 @@ def process_light_client_update(store: LightClientStore,
|
|||||||
apply_light_client_update(store, update)
|
apply_light_client_update(store, update)
|
||||||
store.best_valid_update = None
|
store.best_valid_update = None
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deriving light client data
|
||||||
|
|
||||||
|
Full nodes are expected to derive light client data from historic blocks and states and provide it to other clients.
|
||||||
|
|
||||||
|
### `create_light_client_bootstrap`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_light_client_bootstrap(state: BeaconState) -> LightClientBootstrap:
|
||||||
|
assert compute_epoch_at_slot(state.slot) >= ALTAIR_FORK_EPOCH
|
||||||
|
assert state.slot == state.latest_block_header.slot
|
||||||
|
|
||||||
|
return LightClientBootstrap(
|
||||||
|
header=BeaconBlockHeader(
|
||||||
|
slot=state.latest_block_header.slot,
|
||||||
|
proposer_index=state.latest_block_header.proposer_index,
|
||||||
|
parent_root=state.latest_block_header.parent_root,
|
||||||
|
state_root=hash_tree_root(state),
|
||||||
|
body_root=state.latest_block_header.body_root,
|
||||||
|
),
|
||||||
|
current_sync_committee=state.current_sync_committee,
|
||||||
|
current_sync_committee_branch=build_proof(state.get_backing(), CURRENT_SYNC_COMMITTEE_INDEX)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Full nodes SHOULD provide `LightClientBootstrap` for all finalized epoch boundary blocks in the epoch range `[max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]` where `current_epoch` is defined by the current wall-clock time. Full nodes MAY also provide `LightClientBootstrap` for other blocks.
|
||||||
|
|
||||||
|
Blocks are considered to be epoch boundary blocks if their block root can occur as part of a valid `Checkpoint`, i.e., if their slot is the initial slot of an epoch, or if all following slots through the initial slot of the next epoch are empty (no block proposed / orphaned).
|
||||||
|
|
||||||
|
`LightClientBootstrap` is computed from the block's immediate post state (without applying empty slots).
|
||||||
|
|
||||||
|
### `create_light_client_update`
|
||||||
|
|
||||||
|
To form a `LightClientUpdate`, the following historical states and blocks are needed:
|
||||||
|
- `state`: the post state of any block with a post-Altair parent block
|
||||||
|
- `block`: the corresponding block
|
||||||
|
- `attested_state`: the post state of the block referred to by `block.parent_root`
|
||||||
|
- `finalized_block`: the block referred to by `attested_state.finalized_checkpoint.root`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_light_client_update(state: BeaconState,
|
||||||
|
block: SignedBeaconBlock,
|
||||||
|
attested_state: BeaconState,
|
||||||
|
finalized_block: Optional[SignedBeaconBlock]) -> LightClientUpdate:
|
||||||
|
assert compute_epoch_at_slot(attested_state.slot) >= ALTAIR_FORK_EPOCH
|
||||||
|
assert sum(block.message.body.sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS
|
||||||
|
|
||||||
|
assert state.slot == state.latest_block_header.slot
|
||||||
|
header = state.latest_block_header.copy()
|
||||||
|
header.state_root = hash_tree_root(state)
|
||||||
|
assert hash_tree_root(header) == hash_tree_root(block.message)
|
||||||
|
update_signature_period = compute_sync_committee_period(compute_epoch_at_slot(block.message.slot))
|
||||||
|
|
||||||
|
assert attested_state.slot == attested_state.latest_block_header.slot
|
||||||
|
attested_header = attested_state.latest_block_header.copy()
|
||||||
|
attested_header.state_root = hash_tree_root(attested_state)
|
||||||
|
assert hash_tree_root(attested_header) == block.message.parent_root
|
||||||
|
update_attested_period = compute_sync_committee_period(compute_epoch_at_slot(attested_header.slot))
|
||||||
|
|
||||||
|
# `next_sync_committee` is only useful if the message is signed by the current sync committee
|
||||||
|
if update_attested_period == update_signature_period:
|
||||||
|
next_sync_committee = attested_state.next_sync_committee
|
||||||
|
next_sync_committee_branch = build_proof(attested_state.get_backing(), NEXT_SYNC_COMMITTEE_INDEX)
|
||||||
|
else:
|
||||||
|
next_sync_committee = SyncCommittee()
|
||||||
|
next_sync_committee_branch = [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]
|
||||||
|
|
||||||
|
# Indicate finality whenever possible
|
||||||
|
if finalized_block is not None:
|
||||||
|
if finalized_block.message.slot != GENESIS_SLOT:
|
||||||
|
finalized_header = BeaconBlockHeader(
|
||||||
|
slot=finalized_block.message.slot,
|
||||||
|
proposer_index=finalized_block.message.proposer_index,
|
||||||
|
parent_root=finalized_block.message.parent_root,
|
||||||
|
state_root=finalized_block.message.state_root,
|
||||||
|
body_root=hash_tree_root(finalized_block.message.body),
|
||||||
|
)
|
||||||
|
assert hash_tree_root(finalized_header) == attested_state.finalized_checkpoint.root
|
||||||
|
else:
|
||||||
|
finalized_header = BeaconBlockHeader()
|
||||||
|
assert attested_state.finalized_checkpoint.root == Bytes32()
|
||||||
|
finality_branch = build_proof(attested_state.get_backing(), FINALIZED_ROOT_INDEX)
|
||||||
|
else:
|
||||||
|
finalized_header = BeaconBlockHeader()
|
||||||
|
finality_branch = [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]
|
||||||
|
|
||||||
|
return 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=block.message.body.sync_aggregate,
|
||||||
|
signature_slot=block.message.slot,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Full nodes SHOULD provide the best derivable `LightClientUpdate` (according to `is_better_update`) for each sync committee period covering any epochs in range `[max(ALTAIR_FORK_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOCK_REQUESTS), current_epoch]` where `current_epoch` is defined by the current wall-clock time. Full nodes MAY also provide `LightClientUpdate` for other sync committee periods.
|
||||||
|
|
||||||
|
- `LightClientUpdate` are assigned to sync committee periods based on their `attested_header.slot`
|
||||||
|
- `LightClientUpdate` are only considered if `compute_sync_committee_period(compute_epoch_at_slot(update.attested_header.slot)) == compute_sync_committee_period(compute_epoch_at_slot(update.signature_slot))`
|
||||||
|
- Only `LightClientUpdate` with `next_sync_committee` as selected by fork choice are provided, regardless of ranking by `is_better_update`. To uniquely identify a non-finalized sync committee fork, all of `period`, `current_sync_committee` and `next_sync_committee` need to be incorporated, as sync committees may reappear over time.
|
||||||
|
@ -0,0 +1,424 @@
|
|||||||
|
from typing import (Any, Dict, List)
|
||||||
|
|
||||||
|
from eth_utils import encode_hex
|
||||||
|
from eth2spec.test.context import (
|
||||||
|
spec_state_test_with_matching_config,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
from eth2spec.test.helpers.state import (
|
||||||
|
next_slots,
|
||||||
|
transition_to,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_test(spec, state):
|
||||||
|
class LightClientSyncTest(object):
|
||||||
|
steps: List[Dict[str, Any]]
|
||||||
|
genesis_validators_root: spec.Root
|
||||||
|
store: spec.LightClientStore
|
||||||
|
|
||||||
|
test = LightClientSyncTest()
|
||||||
|
test.steps = []
|
||||||
|
|
||||||
|
yield "genesis_validators_root", "meta", "0x" + state.genesis_validators_root.hex()
|
||||||
|
test.genesis_validators_root = state.genesis_validators_root
|
||||||
|
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH * 2 - 1)
|
||||||
|
trusted_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
trusted_block_root = trusted_block.message.hash_tree_root()
|
||||||
|
bootstrap = spec.create_light_client_bootstrap(state)
|
||||||
|
yield "trusted_block_root", "meta", "0x" + trusted_block_root.hex()
|
||||||
|
yield "bootstrap", bootstrap
|
||||||
|
test.store = spec.initialize_light_client_store(trusted_block_root, bootstrap)
|
||||||
|
|
||||||
|
return test
|
||||||
|
|
||||||
|
|
||||||
|
def finalize_test(test):
|
||||||
|
yield "steps", test.steps
|
||||||
|
yield "expected_finalized_header", test.store.finalized_header
|
||||||
|
yield "expected_optimistic_header", test.store.optimistic_header
|
||||||
|
|
||||||
|
|
||||||
|
def get_update_file_name(spec, update):
|
||||||
|
if spec.is_sync_committee_update(update):
|
||||||
|
suffix1 = "s"
|
||||||
|
else:
|
||||||
|
suffix1 = "x"
|
||||||
|
if spec.is_finality_update(update):
|
||||||
|
suffix2 = "f"
|
||||||
|
else:
|
||||||
|
suffix2 = "x"
|
||||||
|
return f"update_{encode_hex(update.attested_header.hash_tree_root())}_{suffix1}{suffix2}"
|
||||||
|
|
||||||
|
|
||||||
|
def emit_slot(test, spec, state):
|
||||||
|
current_slot = state.slot
|
||||||
|
yield from []
|
||||||
|
test.steps.append({
|
||||||
|
"process_slot": {
|
||||||
|
"current_slot": int(current_slot),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
spec.process_slot_for_light_client_store(test.store, current_slot)
|
||||||
|
|
||||||
|
|
||||||
|
def emit_update(test, spec, state, block, attested_state, finalized_block, with_next_sync_committee=True):
|
||||||
|
update = spec.create_light_client_update(state, block, attested_state, finalized_block)
|
||||||
|
if not with_next_sync_committee:
|
||||||
|
update.next_sync_committee = spec.SyncCommittee()
|
||||||
|
update.next_sync_committee_branch = \
|
||||||
|
[spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))]
|
||||||
|
current_slot = state.slot
|
||||||
|
yield get_update_file_name(spec, update), update
|
||||||
|
test.steps.append({
|
||||||
|
"process_update": {
|
||||||
|
"update": get_update_file_name(spec, update),
|
||||||
|
"current_slot": int(current_slot),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
spec.process_light_client_update(test.store, update, current_slot, test.genesis_validators_root)
|
||||||
|
return update
|
||||||
|
|
||||||
|
|
||||||
|
def compute_start_slot_at_sync_committee_period(spec, sync_committee_period):
|
||||||
|
return spec.compute_start_slot_at_epoch(sync_committee_period * spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_start_slot_at_next_sync_committee_period(spec, state):
|
||||||
|
sync_committee_period = spec.compute_sync_committee_period(spec.compute_epoch_at_slot(state.slot))
|
||||||
|
return compute_start_slot_at_sync_committee_period(spec, sync_committee_period + 1)
|
||||||
|
|
||||||
|
|
||||||
|
@with_altair_and_later
|
||||||
|
@spec_state_test_with_matching_config
|
||||||
|
@with_presets([MINIMAL], reason="too slow")
|
||||||
|
def test_light_client_sync(spec, state):
|
||||||
|
if state.fork.current_version == spec.config.GENESIS_FORK_VERSION:
|
||||||
|
return # Test requires Altair or later
|
||||||
|
|
||||||
|
# Start test
|
||||||
|
test = yield from setup_test(spec, state)
|
||||||
|
|
||||||
|
# Initial `LightClientUpdate`, populating `store.next_sync_committee`
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# +-----------+ +----------+ +-----------+ |
|
||||||
|
# | finalized | <-- (2 epochs) -- | attested | <-- | signature | |
|
||||||
|
# +-----------+ +----------+ +-----------+ |
|
||||||
|
# |
|
||||||
|
# |
|
||||||
|
# sync committee
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH - 1)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Advance to next sync committee period
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# +-----------+ +----------+ +-----------+ |
|
||||||
|
# | finalized | <-- (2 epochs) -- | attested | <-- | signature | |
|
||||||
|
# +-----------+ +----------+ +-----------+ |
|
||||||
|
# |
|
||||||
|
# |
|
||||||
|
# sync committee
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
transition_to(spec, state, compute_start_slot_at_next_sync_committee_period(spec, state))
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH - 1)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Edge case: Signature in next period
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# +-----------+ +----------+ | +-----------+
|
||||||
|
# | finalized | <-- (2 epochs) -- | attested | <-- | signature |
|
||||||
|
# +-----------+ +----------+ | +-----------+
|
||||||
|
# |
|
||||||
|
# |
|
||||||
|
# sync committee
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH - 2)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
transition_to(spec, state, compute_start_slot_at_next_sync_committee_period(spec, state))
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Edge case: Finalized header not included
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# + - - - - - + | +----------+ +-----------+
|
||||||
|
# ¦ finalized ¦ <-- (2 epochs) -- | attested | <-- | signature |
|
||||||
|
# + - - - - - + | +----------+ +-----------+
|
||||||
|
# |
|
||||||
|
# |
|
||||||
|
# sync committee
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
update = yield from emit_update(test, spec, state, block, attested_state, finalized_block=None)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update == update
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Non-finalized case: Attested `next_sync_committee` is not finalized
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# +-----------+ | +----------+ +-----------+
|
||||||
|
# | finalized | <-- (2 epochs) -- | attested | <-- | signature |
|
||||||
|
# +-----------+ | +----------+ +-----------+
|
||||||
|
# |
|
||||||
|
# |
|
||||||
|
# sync committee
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
attested_state = state.copy()
|
||||||
|
store_state = attested_state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
update = yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update == update
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Force-update using timeout
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# +-----------+ | +----------+
|
||||||
|
# | finalized | <-- (2 epochs) -- | attested |
|
||||||
|
# +-----------+ | +----------+
|
||||||
|
# | ^
|
||||||
|
# | \
|
||||||
|
# sync committee `--- store.finalized_header
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
attested_state = state.copy()
|
||||||
|
next_slots(spec, state, spec.UPDATE_TIMEOUT - 1)
|
||||||
|
yield from emit_slot(test, spec, state)
|
||||||
|
assert test.store.finalized_header.slot == store_state.slot
|
||||||
|
assert test.store.next_sync_committee == store_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == store_state.slot
|
||||||
|
|
||||||
|
# Edge case: Finalized header not included, after force-update
|
||||||
|
# ```
|
||||||
|
# | |
|
||||||
|
# + - - - - - + | +--+ +----------+ | +-----------+
|
||||||
|
# ¦ finalized ¦ <-- (2 epochs) -- | | <-- | attested | <-- | signature |
|
||||||
|
# + - - - - - + | +--+ +----------+ | +-----------+
|
||||||
|
# | / |
|
||||||
|
# | store.fin |
|
||||||
|
# sync committee sync committee
|
||||||
|
# period boundary period boundary
|
||||||
|
# ```
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
update = yield from emit_update(test, spec, state, block, attested_state, finalized_block=None)
|
||||||
|
assert test.store.finalized_header.slot == store_state.slot
|
||||||
|
assert test.store.next_sync_committee == store_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update == update
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Edge case: Finalized header older than store
|
||||||
|
# ```
|
||||||
|
# | |
|
||||||
|
# +-----------+ | +--+ | +----------+ +-----------+
|
||||||
|
# | finalized | <-- (2 epochs) -- | | <-- | attested | <-- | signature |
|
||||||
|
# +-----------+ | +--+ | +----------+ +-----------+
|
||||||
|
# | / |
|
||||||
|
# | store.fin |
|
||||||
|
# sync committee sync committee
|
||||||
|
# period boundary period boundary
|
||||||
|
# ```
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
update = yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == store_state.slot
|
||||||
|
assert test.store.next_sync_committee == store_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update == update
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
yield from emit_slot(test, spec, state)
|
||||||
|
assert test.store.finalized_header.slot == attested_state.slot
|
||||||
|
assert test.store.next_sync_committee == attested_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Advance to next sync committee period
|
||||||
|
# ```
|
||||||
|
# |
|
||||||
|
# +-----------+ +----------+ +-----------+ |
|
||||||
|
# | finalized | <-- (2 epochs) -- | attested | <-- | signature | |
|
||||||
|
# +-----------+ +----------+ +-----------+ |
|
||||||
|
# |
|
||||||
|
# |
|
||||||
|
# sync committee
|
||||||
|
# period boundary
|
||||||
|
# ```
|
||||||
|
transition_to(spec, state, compute_start_slot_at_next_sync_committee_period(spec, state))
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH - 1)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Finish test
|
||||||
|
yield from finalize_test(test)
|
||||||
|
|
||||||
|
|
||||||
|
@with_altair_and_later
|
||||||
|
@spec_state_test_with_matching_config
|
||||||
|
@with_presets([MINIMAL], reason="too slow")
|
||||||
|
def test_supply_sync_committee_from_past_update(spec, state):
|
||||||
|
if state.fork.current_version == spec.config.GENESIS_FORK_VERSION:
|
||||||
|
return # Test requires Altair or later
|
||||||
|
|
||||||
|
# Advance the chain, so that a `LightClientUpdate` from the past is available
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH * 2 - 1)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
past_state = state.copy()
|
||||||
|
|
||||||
|
# Start test
|
||||||
|
test = yield from setup_test(spec, state)
|
||||||
|
assert not spec.is_next_sync_committee_known(test.store)
|
||||||
|
|
||||||
|
# Apply `LightClientUpdate` from the past, populating `store.next_sync_committee`
|
||||||
|
yield from emit_update(test, spec, past_state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == state.slot
|
||||||
|
|
||||||
|
# Finish test
|
||||||
|
yield from finalize_test(test)
|
||||||
|
|
||||||
|
|
||||||
|
@with_altair_and_later
|
||||||
|
@spec_state_test_with_matching_config
|
||||||
|
@with_presets([MINIMAL], reason="too slow")
|
||||||
|
def test_advance_finality_without_sync_committee(spec, state):
|
||||||
|
if state.fork.current_version == spec.config.GENESIS_FORK_VERSION:
|
||||||
|
return # Test requires Altair or later
|
||||||
|
|
||||||
|
# Start test
|
||||||
|
test = yield from setup_test(spec, state)
|
||||||
|
|
||||||
|
# Initial `LightClientUpdate`, populating `store.next_sync_committee`
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH - 1)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Advance finality into next sync committee period, but omit `next_sync_committee`
|
||||||
|
transition_to(spec, state, compute_start_slot_at_next_sync_committee_period(spec, state))
|
||||||
|
next_slots(spec, state, spec.SLOTS_PER_EPOCH - 1)
|
||||||
|
finalized_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
finalized_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, spec.SLOTS_PER_EPOCH - 1, True, True)
|
||||||
|
justified_block = state_transition_with_full_block(spec, state, True, True)
|
||||||
|
justified_state = state.copy()
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, spec.SLOTS_PER_EPOCH, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block, with_next_sync_committee=False)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert not spec.is_next_sync_committee_known(test.store)
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Advance finality once more, with `next_sync_committee` still unknown
|
||||||
|
past_state = finalized_state
|
||||||
|
finalized_block = justified_block
|
||||||
|
finalized_state = justified_state
|
||||||
|
_, _, state = next_slots_with_attestations(spec, state, spec.SLOTS_PER_EPOCH - 1, True, True)
|
||||||
|
attested_state = state.copy()
|
||||||
|
sync_aggregate, _ = get_sync_aggregate(spec, state)
|
||||||
|
block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate)
|
||||||
|
|
||||||
|
# Apply `LightClientUpdate` without `finalized_header` nor `next_sync_committee`
|
||||||
|
update = yield from emit_update(test, spec, state, block, attested_state, None, with_next_sync_committee=False)
|
||||||
|
assert test.store.finalized_header.slot == past_state.slot
|
||||||
|
assert not spec.is_next_sync_committee_known(test.store)
|
||||||
|
assert test.store.best_valid_update == update
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Apply `LightClientUpdate` with `finalized_header` but no `next_sync_committee`
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block, with_next_sync_committee=False)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert not spec.is_next_sync_committee_known(test.store)
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Apply full `LightClientUpdate`, supplying `next_sync_committee`
|
||||||
|
yield from emit_update(test, spec, state, block, attested_state, finalized_block)
|
||||||
|
assert test.store.finalized_header.slot == finalized_state.slot
|
||||||
|
assert test.store.next_sync_committee == finalized_state.next_sync_committee
|
||||||
|
assert test.store.best_valid_update is None
|
||||||
|
assert test.store.optimistic_header.slot == attested_state.slot
|
||||||
|
|
||||||
|
# Finish test
|
||||||
|
yield from finalize_test(test)
|
@ -246,7 +246,12 @@ def next_epoch_with_attestations(spec,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def state_transition_with_full_block(spec, state, fill_cur_epoch, fill_prev_epoch, participation_fn=None):
|
def state_transition_with_full_block(spec,
|
||||||
|
state,
|
||||||
|
fill_cur_epoch,
|
||||||
|
fill_prev_epoch,
|
||||||
|
participation_fn=None,
|
||||||
|
sync_aggregate=None):
|
||||||
"""
|
"""
|
||||||
Build and apply a block with attestions at the calculated `slot_to_attest` of current epoch and/or previous epoch.
|
Build and apply a block with attestions at the calculated `slot_to_attest` of current epoch and/or previous epoch.
|
||||||
"""
|
"""
|
||||||
@ -272,6 +277,8 @@ def state_transition_with_full_block(spec, state, fill_cur_epoch, fill_prev_epoc
|
|||||||
)
|
)
|
||||||
for attestation in attestations:
|
for attestation in attestations:
|
||||||
block.body.attestations.append(attestation)
|
block.body.attestations.append(attestation)
|
||||||
|
if sync_aggregate is not None:
|
||||||
|
block.body.sync_aggregate = sync_aggregate
|
||||||
|
|
||||||
signed_block = state_transition_and_sign_block(spec, state, block)
|
signed_block = state_transition_and_sign_block(spec, state, block)
|
||||||
return signed_block
|
return signed_block
|
||||||
|
@ -3,4 +3,5 @@
|
|||||||
This series of tests provides reference test vectors for the light client sync protocol spec.
|
This series of tests provides reference test vectors for the light client sync protocol spec.
|
||||||
|
|
||||||
Handlers:
|
Handlers:
|
||||||
|
- `light_client_sync`: see [Light client sync test format](./light_client_sync.md)
|
||||||
- `update_ranking`: see [`LightClientUpdate` ranking test format](./update_ranking.md)
|
- `update_ranking`: see [`LightClientUpdate` ranking test format](./update_ranking.md)
|
||||||
|
59
tests/formats/sync_protocol/light_client_sync.md
Normal file
59
tests/formats/sync_protocol/light_client_sync.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Light client sync tests
|
||||||
|
|
||||||
|
This series of tests provides reference test vectors for validating that a light client implementing the sync protocol can sync to the latest block header.
|
||||||
|
|
||||||
|
## Test case format
|
||||||
|
|
||||||
|
### `meta.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
genesis_validators_root: Bytes32 -- string, hex encoded, with 0x prefix
|
||||||
|
trusted_block_root: Bytes32 -- string, hex encoded, with 0x prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bootstrap.ssz_snappy`
|
||||||
|
|
||||||
|
An SSZ-snappy encoded `bootstrap` object of type `LightClientBootstrap` to initialize a local `store` object of type `LightClientStore` using `initialize_light_client_store(trusted_block_rooot, bootstrap)`.
|
||||||
|
|
||||||
|
### `steps.yaml`
|
||||||
|
|
||||||
|
The steps to execute in sequence. There may be multiple items of the following types:
|
||||||
|
|
||||||
|
#### `process_slot` execution step
|
||||||
|
|
||||||
|
The function `process_slot_for_light_client_store(store, current_slot)`
|
||||||
|
should be executed with the specified parameters:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{
|
||||||
|
current_slot: int -- integer, decimal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After this step, the `store` object may have been updated.
|
||||||
|
|
||||||
|
#### `process_update` execution step
|
||||||
|
|
||||||
|
The function `process_light_client_update(store, update, current_slot, genesis_validators_root)` should be executed with the specified parameters:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
{
|
||||||
|
update: string -- name of the `*.ssz_snappy` file to load
|
||||||
|
as a `LightClientUpdate` object
|
||||||
|
current_slot: int -- integer, decimal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After this step, the `store` object may have been updated.
|
||||||
|
|
||||||
|
### `expected_finalized_header.ssz_snappy`
|
||||||
|
|
||||||
|
An SSZ-snappy encoded `expected_finalized_header` object of type `BeaconBlockHeader` that represents the expected `store.finalized_header` after applying all the test steps.
|
||||||
|
|
||||||
|
### `expected_optimistic_header.ssz_snappy`
|
||||||
|
|
||||||
|
An SSZ-snappy encoded `expected_optimistic_header` object of type `BeaconBlockHeader` that represents the expected `store.finalized_header` after applying all the test steps.
|
||||||
|
|
||||||
|
## Condition
|
||||||
|
|
||||||
|
A test-runner should initialize a local `LightClientStore` using the provided `bootstrap` object. It should then proceed to execute all the test steps in sequence. Finally, it should verify that the resulting `store` refers to the provided `expected_finalized_header` and `expected_optimistic_header`.
|
@ -4,6 +4,7 @@ from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
altair_mods = {key: 'eth2spec.test.altair.sync_protocol.test_' + key for key in [
|
altair_mods = {key: 'eth2spec.test.altair.sync_protocol.test_' + key for key in [
|
||||||
|
'light_client_sync',
|
||||||
'update_ranking',
|
'update_ranking',
|
||||||
]}
|
]}
|
||||||
bellatrix_mods = altair_mods
|
bellatrix_mods = altair_mods
|
||||||
|
Loading…
x
Reference in New Issue
Block a user