diff --git a/setup.py b/setup.py index 973c49fd4..cd6a5264f 100644 --- a/setup.py +++ b/setup.py @@ -457,6 +457,7 @@ class AltairSpecBuilder(Phase0SpecBuilder): from typing import NewType, Union as PyUnion from eth2spec.phase0 import {preset_name} as phase0 +from eth2spec.test.helpers.merkle import build_proof from eth2spec.utils.ssz.ssz_typing import Path ''' diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 8d420eda5..a07adbd67 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -31,6 +31,9 @@ - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_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) @@ -417,3 +420,105 @@ def process_light_client_update(store: LightClientStore, apply_light_client_update(store, update) 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. diff --git a/tests/core/pyspec/eth2spec/test/altair/sync_protocol/test_light_client_sync.py b/tests/core/pyspec/eth2spec/test/altair/sync_protocol/test_light_client_sync.py new file mode 100644 index 000000000..765d42968 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/sync_protocol/test_light_client_sync.py @@ -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) diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index ffd484ecd..7ba71a969 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -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. """ @@ -272,6 +277,8 @@ def state_transition_with_full_block(spec, state, fill_cur_epoch, fill_prev_epoc ) for attestation in attestations: 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) return signed_block diff --git a/tests/formats/sync_protocol/README.md b/tests/formats/sync_protocol/README.md index da93972d6..c189cbc23 100644 --- a/tests/formats/sync_protocol/README.md +++ b/tests/formats/sync_protocol/README.md @@ -3,4 +3,5 @@ This series of tests provides reference test vectors for the light client sync protocol spec. Handlers: +- `light_client_sync`: see [Light client sync test format](./light_client_sync.md) - `update_ranking`: see [`LightClientUpdate` ranking test format](./update_ranking.md) diff --git a/tests/formats/sync_protocol/light_client_sync.md b/tests/formats/sync_protocol/light_client_sync.md new file mode 100644 index 000000000..1d1506936 --- /dev/null +++ b/tests/formats/sync_protocol/light_client_sync.md @@ -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`. diff --git a/tests/generators/sync_protocol/main.py b/tests/generators/sync_protocol/main.py index 0fe8e0fe1..607587d93 100644 --- a/tests/generators/sync_protocol/main.py +++ b/tests/generators/sync_protocol/main.py @@ -4,6 +4,7 @@ from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators if __name__ == "__main__": altair_mods = {key: 'eth2spec.test.altair.sync_protocol.test_' + key for key in [ + 'light_client_sync', 'update_ranking', ]} bellatrix_mods = altair_mods