diff --git a/presets/mainnet/altair.yaml b/presets/mainnet/altair.yaml index 9a17b78032..813ef72122 100644 --- a/presets/mainnet/altair.yaml +++ b/presets/mainnet/altair.yaml @@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 256 # --------------------------------------------------------------- # 1 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 32 * 256) +UPDATE_TIMEOUT: 8192 diff --git a/presets/minimal/altair.yaml b/presets/minimal/altair.yaml index 88d78bea36..5e472c49cf 100644 --- a/presets/minimal/altair.yaml +++ b/presets/minimal/altair.yaml @@ -22,3 +22,5 @@ EPOCHS_PER_SYNC_COMMITTEE_PERIOD: 8 # --------------------------------------------------------------- # 1 MIN_SYNC_COMMITTEE_PARTICIPANTS: 1 +# SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD (= 8 * 8) +UPDATE_TIMEOUT: 64 diff --git a/setup.py b/setup.py index 0ced87be2e..7b74fc1557 100644 --- a/setup.py +++ b/setup.py @@ -683,6 +683,7 @@ def combine_dicts(old_dict: Dict[str, T], new_dict: Dict[str, T]) -> Dict[str, T 'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256', 'bytes', 'byte', 'ByteList', 'ByteVector', 'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set', + 'Optional', ] diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index 24c35f8912..c8c7c3d4dc 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -13,12 +13,14 @@ - [Preset](#preset) - [Misc](#misc) - [Containers](#containers) - - [`LightClientSnapshot`](#lightclientsnapshot) - [`LightClientUpdate`](#lightclientupdate) - [`LightClientStore`](#lightclientstore) - [Helper functions](#helper-functions) - [`get_subtree_index`](#get_subtree_index) + - [`get_active_header`](#get_active_header) + - [`get_safety_threshold`](#get_safety_threshold) - [Light client state updates](#light-client-state-updates) + - [`process_slot_for_light_client_store`](#process_slot_for_light_client_store) - [`validate_light_client_update`](#validate_light_client_update) - [`apply_light_client_update`](#apply_light_client_update) - [`process_light_client_update`](#process_light_client_update) @@ -47,38 +49,27 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. ### Misc -| Name | Value | -| - | - | -| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | +| Name | Value | Notes | +| - | - | - | +| `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | | +| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | ~27.3 hours | ## Containers -### `LightClientSnapshot` - -```python -class LightClientSnapshot(Container): - # Beacon block header - header: BeaconBlockHeader - # Sync committees corresponding to the header - current_sync_committee: SyncCommittee - next_sync_committee: SyncCommittee -``` - ### `LightClientUpdate` ```python class LightClientUpdate(Container): - # Update beacon block header - header: BeaconBlockHeader - # Next sync committee corresponding to the header + # The beacon block header that is attested to by the sync committee + attested_header: BeaconBlockHeader + # Next sync committee corresponding to the active header next_sync_committee: SyncCommittee next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)] - # Finality proof for the update header - finality_header: BeaconBlockHeader + # The finalized beacon block header attested to by Merkle branch + finalized_header: BeaconBlockHeader finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)] # Sync committee aggregate signature - sync_committee_bits: Bitvector[SYNC_COMMITTEE_SIZE] - sync_committee_signature: BLSSignature + sync_committee_aggregate: SyncAggregate # Fork version for the aggregate signature fork_version: Version ``` @@ -88,8 +79,18 @@ class LightClientUpdate(Container): ```python @dataclass class LightClientStore(object): - snapshot: LightClientSnapshot - valid_updates: Set[LightClientUpdate] + # Beacon block header that is finalized + finalized_header: BeaconBlockHeader + # Sync committees corresponding to the header + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # Best available header to switch finalized head to if we see nothing else + best_valid_update: Optional[LightClientUpdate] + # Most recent available reasonably-safe header + optimistic_header: BeaconBlockHeader + # Max number of active participants in a sync committee (used to calculate safety threshold) + previous_max_active_participants: uint64 + current_max_active_participants: uint64 ``` ## Helper functions @@ -101,95 +102,157 @@ def get_subtree_index(generalized_index: GeneralizedIndex) -> uint64: return uint64(generalized_index % 2**(floorlog2(generalized_index))) ``` +### `get_active_header` + +```python +def get_active_header(update: LightClientUpdate) -> BeaconBlockHeader: + # The "active header" is the header that the update is trying to convince us + # to accept. If a finalized header is present, it's the finalized header, + # otherwise it's the attested header + if update.finalized_header != BeaconBlockHeader(): + return update.finalized_header + else: + return update.attested_header +``` + +### `get_safety_threshold` + +```python +def get_safety_threshold(store: LightClientStore) -> uint64: + return max( + store.previous_max_active_participants, + store.current_max_active_participants, + ) // 2 +``` + ## Light client state updates -A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. +A light client maintains its state in a `store` object of type `LightClientStore` and receives `update` objects of type `LightClientUpdate`. Every `update` triggers `process_light_client_update(store, update, current_slot)` where `current_slot` is the current slot based on some local clock. `process_slot_for_light_client_store` is processed every time the current slot increments. + +#### `process_slot_for_light_client_store` + +```python +def process_slot_for_light_client_store(store: LightClientStore, current_slot: Slot) -> None: + if current_slot % UPDATE_TIMEOUT == 0: + store.previous_max_active_participants = store.current_max_active_participants + store.current_max_active_participants = 0 + if ( + current_slot > store.finalized_header.slot + UPDATE_TIMEOUT + and store.best_valid_update is not None + ): + # Forced best update when the update timeout has elapsed + apply_light_client_update(store, store.best_valid_update) + store.best_valid_update = None +``` #### `validate_light_client_update` ```python -def validate_light_client_update(snapshot: LightClientSnapshot, +def validate_light_client_update(store: LightClientStore, update: LightClientUpdate, + current_slot: Slot, genesis_validators_root: Root) -> None: - # Verify update slot is larger than snapshot slot - assert update.header.slot > snapshot.header.slot + # 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 # Verify update does not skip a sync committee period - snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - assert update_period in (snapshot_period, snapshot_period + 1) + finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + assert update_period in (finalized_period, finalized_period + 1) - # Verify update header root is the finalized root of the finality header, if specified - if update.finality_header == BeaconBlockHeader(): - signed_header = update.header + # Verify that the `finalized_header`, if present, actually is the finalized header saved in the + # state of the `attested header` + if update.finalized_header == BeaconBlockHeader(): assert update.finality_branch == [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))] else: - signed_header = update.finality_header assert is_valid_merkle_branch( - leaf=hash_tree_root(update.header), + leaf=hash_tree_root(update.finalized_header), branch=update.finality_branch, depth=floorlog2(FINALIZED_ROOT_INDEX), index=get_subtree_index(FINALIZED_ROOT_INDEX), - root=update.finality_header.state_root, + root=update.attested_header.state_root, ) # Verify update next sync committee if the update period incremented - if update_period == snapshot_period: - sync_committee = snapshot.current_sync_committee + 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 = snapshot.next_sync_committee + 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, depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX), index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX), - root=update.header.state_root, + root=active_header.state_root, ) + + sync_aggregate = update.sync_committee_aggregate # Verify sync committee has sufficient participants - assert sum(update.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS + assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS # Verify sync committee aggregate signature - participant_pubkeys = [pubkey for (bit, pubkey) in zip(update.sync_committee_bits, sync_committee.pubkeys) if bit] + participant_pubkeys = [ + pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) + if bit + ] domain = compute_domain(DOMAIN_SYNC_COMMITTEE, update.fork_version, genesis_validators_root) - signing_root = compute_signing_root(signed_header, domain) - assert bls.FastAggregateVerify(participant_pubkeys, signing_root, update.sync_committee_signature) + signing_root = compute_signing_root(update.attested_header, domain) + assert bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature) ``` #### `apply_light_client_update` ```python -def apply_light_client_update(snapshot: LightClientSnapshot, update: LightClientUpdate) -> None: - snapshot_period = compute_epoch_at_slot(snapshot.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - update_period = compute_epoch_at_slot(update.header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - if update_period == snapshot_period + 1: - snapshot.current_sync_committee = snapshot.next_sync_committee - snapshot.next_sync_committee = update.next_sync_committee - snapshot.header = update.header +def apply_light_client_update(store: LightClientStore, update: LightClientUpdate) -> None: + active_header = get_active_header(update) + finalized_period = compute_epoch_at_slot(store.finalized_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + update_period = compute_epoch_at_slot(active_header.slot) // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if update_period == finalized_period + 1: + store.current_sync_committee = store.next_sync_committee + store.next_sync_committee = update.next_sync_committee + store.finalized_header = active_header ``` #### `process_light_client_update` ```python -def process_light_client_update(store: LightClientStore, update: LightClientUpdate, current_slot: Slot, +def process_light_client_update(store: LightClientStore, + update: LightClientUpdate, + current_slot: Slot, genesis_validators_root: Root) -> None: - validate_light_client_update(store.snapshot, update, genesis_validators_root) - store.valid_updates.add(update) + validate_light_client_update(store, update, current_slot, genesis_validators_root) - update_timeout = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD + sync_committee_bits = update.sync_committee_aggregate.sync_committee_bits + + # Update the best update in case we have to force-update to it if the timeout elapses if ( - sum(update.sync_committee_bits) * 3 >= len(update.sync_committee_bits) * 2 - and update.finality_header != BeaconBlockHeader() + store.best_valid_update is None + or sum(sync_committee_bits) > sum(store.best_valid_update.sync_committee_aggregate.sync_committee_bits) ): - # Apply update if (1) 2/3 quorum is reached and (2) we have a finality proof. - # Note that (2) means that the current light client design needs finality. - # It may be changed to re-organizable light client design. See the on-going issue consensus-specs#2182. - apply_light_client_update(store.snapshot, update) - store.valid_updates = set() - elif current_slot > store.snapshot.header.slot + update_timeout: - # Forced best update when the update timeout has elapsed - apply_light_client_update(store.snapshot, - max(store.valid_updates, key=lambda update: sum(update.sync_committee_bits))) - store.valid_updates = set() + store.best_valid_update = update + + # Track the maximum number of active participants in the committee signatures + store.current_max_active_participants = max( + store.current_max_active_participants, + sum(sync_committee_bits), + ) + + # Update the optimistic header + if ( + sum(sync_committee_bits) > get_safety_threshold(store) + and update.attested_header.slot > store.optimistic_header.slot + ): + store.optimistic_header = update.attested_header + + # Update finalized header + if ( + sum(sync_committee_bits) * 3 >= len(sync_committee_bits) * 2 + and update.finalized_header != BeaconBlockHeader() + ): + # Normal update through 2/3 threshold + apply_light_client_update(store, update) + store.best_valid_update = None ``` diff --git a/specs/merge/client-settings.md b/specs/merge/client-settings.md deleted file mode 100644 index 64f912372e..0000000000 --- a/specs/merge/client-settings.md +++ /dev/null @@ -1,29 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [The Merge -- Client Settings](#the-merge----client-settings) - - [Override terminal total difficulty](#override-terminal-total-difficulty) - - [Override terminal block hash](#override-terminal-block-hash) - - - -# The Merge -- Client Settings - -**Notice**: This document is a work-in-progress for researchers and implementers. - -This document specifies configurable settings that clients must implement for the Merge. - -### Override terminal total difficulty - -To coordinate manual overrides to [`TERMINAL_TOTAL_DIFFICULTY`](./beacon-chain.md#Transition-settings) parameter, clients must provide `--terminal-total-difficulty-override` as a configurable setting. The value provided by this setting must take precedence over pre-configured `TERMINAL_TOTAL_DIFFICULTY` parameter. Clients should accept the setting as a decimal value (i.e., *not* hexadecimal). - -Except under exceptional scenarios, this setting is not expected to be used. Sufficient warning to the user about this exceptional configurable setting should be provided. - -### Override terminal block hash - -To allow for transition coordination around a specific PoW block, clients must also provide `--terminal-block-hash-override` and `--terminal-block-hash-epoch-override` as configurable settings. -* The value provided by `--terminal-block-hash-override` takes precedence over the pre-configured `TERMINAL_BLOCK_HASH` parameter. -* The value provided by `--terminal-block-hash-epoch-override` takes precedence over the pre-configured `TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH` parameter. - -Except under exceptional scenarios, these settings are not expected to be used. Sufficient warning to the user about this exceptional configurable setting should be provided. diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index e149989253..95d613e9f7 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -64,7 +64,11 @@ def notify_forkchoice_updated(self: ExecutionEngine, ``` *Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). -As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` must be called with `finalized_block_hash = Hash32()`. +As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` MUST be called with `finalized_block_hash = Hash32()`. + +*Note*: Client software MUST NOT call this function until the transition conditions are met on the PoW network, i.e. there exists a block for which `is_valid_terminal_pow_block` function returns `True`. + +*Note*: Client software MUST call this function to initiate the payload build process to produce the merge transition block; the `head_block_hash` parameter MUST be set to the hash of a terminal PoW block in this case. ## Helpers diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index d082ede306..de0a2e7856 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -181,15 +181,19 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: if (i in store.latest_messages and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) + if store.proposer_boost_root == Root(): + # Return only attestation score if ``proposer_boost_root`` is not set + return attestation_score + + # Calculate proposer score if ``proposer_boost_root`` is set proposer_score = Gwei(0) - if store.proposer_boost_root != Root(): - block = store.blocks[root] - if get_ancestor(store, root, block.slot) == store.proposer_boost_root: - num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) - avg_balance = get_total_active_balance(state) // num_validators - committee_size = num_validators // SLOTS_PER_EPOCH - committee_weight = committee_size * avg_balance - proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 + # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` + if get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) == root: + num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) + avg_balance = get_total_active_balance(state) // num_validators + committee_size = num_validators // SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 return attestation_score + proposer_score ``` @@ -263,6 +267,7 @@ def get_head(store: Store) -> Root: if len(children) == 0: return head # Sort by latest attesting balance with ties broken lexicographically + # Ties broken by favoring block with lexicographically higher root head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root)) ``` diff --git a/specs/sharding/beacon-chain.md b/specs/sharding/beacon-chain.md index 8190951b47..cf3d7bea17 100644 --- a/specs/sharding/beacon-chain.md +++ b/specs/sharding/beacon-chain.md @@ -101,7 +101,7 @@ The following values are (non-configurable) constants used throughout the specif | Name | Value | Notes | | - | - | - | -| `PRIMITIVE_ROOT_OF_UNITY` | `5` | Primitive root of unity of the BLS12_381 (inner) modulus | +| `PRIMITIVE_ROOT_OF_UNITY` | `7` | Primitive root of unity of the BLS12_381 (inner) modulus | | `DATA_AVAILABILITY_INVERSE_CODING_RATE` | `2**1` (= 2) | Factor by which samples are extended for data availability encoding | | `POINTS_PER_SAMPLE` | `uint64(2**3)` (= 8) | 31 * 8 = 248 bytes | | `MODULUS` | `0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001` (curve order of BLS12_381) | diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt index ab679818ce..a5ba932511 100644 --- a/tests/core/pyspec/eth2spec/VERSION.txt +++ b/tests/core/pyspec/eth2spec/VERSION.txt @@ -1 +1 @@ -1.1.6 \ No newline at end of file +1.1.7 \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py b/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py index 15444df819..30444c4ce4 100644 --- a/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py +++ b/tests/core/pyspec/eth2spec/test/altair/unittests/test_sync_protocol.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from eth2spec.test.context import ( spec_state_test, with_presets, @@ -19,20 +21,24 @@ from eth2spec.test.helpers.merkle import build_proof -@with_altair_and_later -@spec_state_test -def test_process_light_client_update_not_updated(spec, state): - pre_snapshot = spec.LightClientSnapshot( - header=spec.BeaconBlockHeader(), +def _initialize_light_client_store(spec, state): + return spec.LightClientStore( + finalized_header=spec.BeaconBlockHeader(), current_sync_committee=state.current_sync_committee, next_sync_committee=state.next_sync_committee, + best_valid_update=None, + optimistic_header=spec.BeaconBlockHeader(), + previous_max_active_participants=0, + current_max_active_participants=0, ) - store = spec.LightClientStore( - snapshot=pre_snapshot, - valid_updates=set(), - ) - # Block at slot 1 doesn't increase sync committee period, so it won't update snapshot + +@with_altair_and_later +@spec_state_test +def test_process_light_client_update_not_timeout(spec, state): + store = _initialize_light_client_store(spec, state) + + # Block at slot 1 doesn't increase sync committee period, so it won't force update store.finalized_header block = build_empty_block_for_next_slot(spec, state) signed_block = state_transition_and_sign_block(spec, state, block) block_header = spec.BeaconBlockHeader( @@ -52,6 +58,10 @@ def test_process_light_client_update_not_updated(spec, state): block_header.slot, committee, ) + sync_committee_aggregate = spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) next_sync_committee_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))] # Ensure that finality checkpoint is genesis @@ -61,40 +71,34 @@ def test_process_light_client_update_not_updated(spec, state): finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))] update = spec.LightClientUpdate( - header=block_header, + attested_header=block_header, next_sync_committee=state.next_sync_committee, next_sync_committee_branch=next_sync_committee_branch, - finality_header=finality_header, + finalized_header=finality_header, finality_branch=finality_branch, - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, + sync_committee_aggregate=sync_committee_aggregate, fork_version=state.fork.current_version, ) + pre_store = deepcopy(store) + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) - assert len(store.valid_updates) == 1 - assert store.valid_updates.pop() == update - assert store.snapshot == pre_snapshot + 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_timeout(spec, state): - pre_snapshot = spec.LightClientSnapshot( - header=spec.BeaconBlockHeader(), - current_sync_committee=state.current_sync_committee, - next_sync_committee=state.next_sync_committee, - ) - store = spec.LightClientStore( - snapshot=pre_snapshot, - valid_updates=set(), - ) + store = _initialize_light_client_store(spec, state) # Forward to next sync committee period - next_slots(spec, state, spec.SLOTS_PER_EPOCH * (spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD)) - snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + next_slots(spec, state, spec.UPDATE_TIMEOUT) + snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD assert snapshot_period + 1 == update_period @@ -119,6 +123,10 @@ def test_process_light_client_update_timeout(spec, state): committee, block_root=spec.Root(block_header.hash_tree_root()), ) + sync_committee_aggregate = spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) # Sync committee is updated next_sync_committee_branch = build_proof(state.get_backing(), spec.NEXT_SYNC_COMMITTEE_INDEX) @@ -127,36 +135,30 @@ def test_process_light_client_update_timeout(spec, state): finality_branch = [spec.Bytes32() for _ in range(spec.floorlog2(spec.FINALIZED_ROOT_INDEX))] update = spec.LightClientUpdate( - header=block_header, + attested_header=block_header, next_sync_committee=state.next_sync_committee, next_sync_committee_branch=next_sync_committee_branch, - finality_header=finality_header, + finalized_header=finality_header, finality_branch=finality_branch, - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, + sync_committee_aggregate=sync_committee_aggregate, fork_version=state.fork.current_version, ) + pre_store = deepcopy(store) + spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) - # snapshot has been updated - assert len(store.valid_updates) == 0 - assert store.snapshot.header == update.header + 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") def test_process_light_client_update_finality_updated(spec, state): - pre_snapshot = spec.LightClientSnapshot( - header=spec.BeaconBlockHeader(), - current_sync_committee=state.current_sync_committee, - next_sync_committee=state.next_sync_committee, - ) - store = spec.LightClientStore( - snapshot=pre_snapshot, - valid_updates=set(), - ) + store = _initialize_light_client_store(spec, state) # Change finality blocks = [] @@ -167,7 +169,7 @@ def test_process_light_client_update_finality_updated(spec, state): # Ensure that finality checkpoint has changed assert state.finalized_checkpoint.epoch == 3 # Ensure that it's same period - snapshot_period = spec.compute_epoch_at_slot(pre_snapshot.header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD + snapshot_period = spec.compute_epoch_at_slot(store.optimistic_header.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD update_period = spec.compute_epoch_at_slot(state.slot) // spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD assert snapshot_period == update_period @@ -199,20 +201,24 @@ def test_process_light_client_update_finality_updated(spec, state): committee, block_root=spec.Root(block_header.hash_tree_root()), ) + sync_committee_aggregate = spec.SyncAggregate( + sync_committee_bits=sync_committee_bits, + sync_committee_signature=sync_committee_signature, + ) update = spec.LightClientUpdate( - header=finalized_block_header, + attested_header=block_header, next_sync_committee=state.next_sync_committee, next_sync_committee_branch=next_sync_committee_branch, - finality_header=block_header, # block_header is the signed header + finalized_header=finalized_block_header, finality_branch=finality_branch, - sync_committee_bits=sync_committee_bits, - sync_committee_signature=sync_committee_signature, + sync_committee_aggregate=sync_committee_aggregate, fork_version=state.fork.current_version, ) spec.process_light_client_update(store, update, state.slot, state.genesis_validators_root) - # snapshot has been updated - assert len(store.valid_updates) == 0 - assert store.snapshot.header == update.header + assert store.current_max_active_participants > 0 + assert store.optimistic_header == update.attested_header + assert store.finalized_header == update.finalized_header + assert store.best_valid_update is None diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 184c0d6098..260cb4d7d5 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -1,6 +1,7 @@ import pytest -from copy import deepcopy from dataclasses import dataclass +import importlib +from eth_utils import encode_hex from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal from eth2spec.altair import mainnet as spec_altair_mainnet, minimal as spec_altair_minimal @@ -85,10 +86,9 @@ class SpecForks(TypedDict, total=False): def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int], spec: Spec, phases: SpecForks): - phase = phases[spec.fork] - balances = balances_fn(phase) - activation_threshold = threshold_fn(phase) - state = create_genesis_state(spec=phase, validator_balances=balances, + balances = balances_fn(spec) + activation_threshold = threshold_fn(spec) + state = create_genesis_state(spec=spec, validator_balances=balances, activation_threshold=activation_threshold) return state @@ -464,6 +464,32 @@ def wrapper(*args, spec: Spec, **kw): return decorator +def _get_basic_dict(ssz_dict: Dict[str, Any]) -> Dict[str, Any]: + """ + Get dict of Python built-in types from a dict of SSZ objects. + """ + result = {} + for k, v in ssz_dict.items(): + if isinstance(v, int): + value = int(v) + elif isinstance(v, bytes): + value = encode_hex(v) + else: + value = str(v) + result[k] = value + return result + + +def _get_copy_of_spec(spec): + fork = spec.fork + preset = spec.config.PRESET_BASE + module_path = f"eth2spec.{fork}.{preset}" + module_spec = importlib.util.find_spec(module_path) + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + return module + + def with_config_overrides(config_overrides): """ WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs. @@ -474,20 +500,20 @@ def with_config_overrides(config_overrides): """ def decorator(fn): def wrapper(*args, spec: Spec, **kw): - # remember the old config - old_config = spec.config + spec = _get_copy_of_spec(spec) # apply our overrides to a copy of it, and apply it to the spec - tmp_config = deepcopy(old_config._asdict()) # not a private method, there are multiple - tmp_config.update(config_overrides) + config = spec.config._asdict() + config.update(config_overrides) config_types = spec.Configuration.__annotations__ - # Retain types of all config values - test_config = {k: config_types[k](v) for k, v in tmp_config.items()} + modified_config = {k: config_types[k](v) for k, v in config.items()} - # Output the config for test vectors (TODO: check config YAML encoding) - yield 'config', 'data', test_config + # To output the changed config to could be serialized with yaml test vectors, + # the dict SSZ objects have to be converted into Python built-in types. + output_config = _get_basic_dict(modified_config) + yield 'config', 'data', output_config - spec.config = spec.Configuration(**test_config) + spec.config = spec.Configuration(**modified_config) # Run the function out = fn(*args, spec=spec, **kw) @@ -495,10 +521,6 @@ def wrapper(*args, spec: Spec, **kw): # it's generating things, and we need to complete it before setting back the config. if out is not None: yield from out - - # Restore the old config and apply it - spec.config = old_config - return wrapper return decorator diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index 0b06f283f6..f056b9acd0 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -42,6 +42,12 @@ def tick_and_add_block(spec, store, signed_block, test_steps, valid=True, return post_state +def add_attestation(spec, store, attestation, test_steps, is_from_block=False): + spec.on_attestation(store, attestation, is_from_block=is_from_block) + yield get_attestation_file_name(attestation), attestation + test_steps.append({'attestation': get_attestation_file_name(attestation)}) + + def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_block=False): parent_block = store.blocks[attestation.data.beacon_block_root] pre_state = store.block_states[spec.hash_tree_root(parent_block)] @@ -52,9 +58,7 @@ def tick_and_run_on_attestation(spec, store, attestation, test_steps, is_from_bl spec.on_tick(store, next_epoch_time) test_steps.append({'tick': int(next_epoch_time)}) - spec.on_attestation(store, attestation, is_from_block=is_from_block) - yield get_attestation_file_name(attestation), attestation - test_steps.append({'attestation': get_attestation_file_name(attestation)}) + yield from add_attestation(spec, store, attestation, test_steps, is_from_block) def run_on_attestation(spec, store, attestation, is_from_block=False, valid=True): diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py new file mode 100644 index 0000000000..d931011565 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_ex_ante.py @@ -0,0 +1,421 @@ +from eth2spec.test.context import ( + MAINNET, + spec_state_test, + with_all_phases, + with_presets, +) +from eth2spec.test.helpers.attestations import ( + get_valid_attestation, + sign_attestation, +) +from eth2spec.test.helpers.block import ( + build_empty_block, +) +from eth2spec.test.helpers.fork_choice import ( + get_genesis_forkchoice_store_and_block, + on_tick_and_append_step, + add_attestation, + add_block, + tick_and_add_block, +) +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, +) + + +def _apply_base_block_a(spec, state, store, test_steps): + # On receiving block A at slot `N` + block = build_empty_block(spec, state, slot=state.slot + 1) + signed_block_a = state_transition_and_sign_block(spec, state, block) + yield from tick_and_add_block(spec, store, signed_block_a, test_steps) + assert spec.get_head(store) == signed_block_a.message.hash_tree_root() + + +@with_all_phases +@spec_state_test +def test_ex_ante_vanilla(spec, state): + """ + With a single adversarial attestation + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_1 (Block B); size `1` - slot N+1 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+2 — C is head + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 at slot `N + 1` voting for block B + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_b, attestation) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head due to proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+2 — C is head + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps + + +def _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root): + """ + Return the minimum attestation participant count such that attestation_score > proposer_score + """ + # calculate proposer boost score + block = store.blocks[root] + proposer_score = 0 + if spec.get_ancestor(store, root, block.slot) == proposer_boost_root: + num_validators = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) + avg_balance = spec.get_total_active_balance(state) // num_validators + committee_size = num_validators // spec.SLOTS_PER_EPOCH + committee_weight = committee_size * avg_balance + proposer_score = (committee_weight * spec.config.PROPOSER_SCORE_BOOST) // 100 + + # calculate minimum participant count such that attestation_score > proposer_score + base_effective_balance = state.validators[0].effective_balance + + return proposer_score // base_effective_balance + 1 + + +@with_all_phases +@with_presets([MAINNET], reason="to create non-duplicate committee") +@spec_state_test +def test_ex_ante_attestations_is_greater_than_proposer_boost_with_boost(spec, state): + """ + Adversarial attestations > proposer boost + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Attestation_set_1 (Block B); size `proposer_boost + 1` - slot N+1 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+2 — B is head + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head due to proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_set_1 at slot `N + 1` voting for block B + proposer_boost_root = signed_block_b.message.hash_tree_root() + root = signed_block_b.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_b, slot=state_b.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_b.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num + sign_attestation(spec, state_b, attestation) + + # Attestation_set_1 received at N+2 — B is head because B's attestation_score > C's proposer_score. + # (B's proposer_score = C's attestation_score = 0) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_b.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_ex_ante_sandwich_without_attestations(spec, state): + """ + Simple Sandwich test with boost and no attestations. + Obejcts: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head (with boost) + Block D received at N+3 — D is head (with boost) + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - D is head, it has proposer score boost + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_ex_ante_sandwich_with_honest_attestation(spec, state): + """ + Boosting necessary to sandwich attack. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_1 (Block C); size 1 - slot N+2 (honest) + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_1 received at N+3 — C is head + Block D received at N+3 — D is head + + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Attestation_1 at N+2 voting for block C + def _filter_participant_set(participants): + return [next(iter(participants))] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == 1 + sign_attestation(spec, state_c, attestation) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_1 received at N+3 — C is head + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - D is head, it has proposer score boost + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_d.message.hash_tree_root() + + yield 'steps', test_steps + + +@with_all_phases +@with_presets([MAINNET], reason="to create non-duplicate committee") +@spec_state_test +def test_ex_ante_sandwich_with_boost_not_sufficient(spec, state): + """ + Boost not sufficient to sandwich attack. + Objects: + Block A - slot N + Block B (parent A) - slot N+1 + Block C (parent A) - slot N+2 + Block D (parent B) - slot N+3 + Attestation_set_1 (Block C); size proposer_boost + 1 - slot N+2 + Steps: + Block A received at N — A is head + Block C received at N+2 — C is head + Block B received at N+2 — C is head + Attestation_set_1 received — C is head + Block D received at N+3 — C is head + """ + test_steps = [] + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + current_time = state.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, current_time, test_steps) + assert store.time == current_time + + # On receiving block A at slot `N` + yield from _apply_base_block_a(spec, state, store, test_steps) + state_a = state.copy() + + # Block B at slot `N + 1`, parent is A + state_b = state_a.copy() + block = build_empty_block(spec, state_a, slot=state_a.slot + 1) + signed_block_b = state_transition_and_sign_block(spec, state_b, block) + + # Block C at slot `N + 2`, parent is A + state_c = state_a.copy() + block = build_empty_block(spec, state_c, slot=state_a.slot + 2) + signed_block_c = state_transition_and_sign_block(spec, state_c, block) + + # Block D at slot `N + 3`, parent is B + state_d = state_b.copy() + block = build_empty_block(spec, state_d, slot=state_a.slot + 3) + signed_block_d = state_transition_and_sign_block(spec, state_d, block) + + # Block C received at N+2 — C is head + time = state_c.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block_c, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block B received at N+2 — C is head, it has proposer score boost + yield from add_block(spec, store, signed_block_b, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Attestation_set_1 at N+2 voting for block C + proposer_boost_root = signed_block_c.message.hash_tree_root() + root = signed_block_c.message.hash_tree_root() + participant_num = _get_greater_than_proposer_boost_score(spec, store, state, proposer_boost_root, root) + + def _filter_participant_set(participants): + return [index for i, index in enumerate(participants) if i < participant_num] + + attestation = get_valid_attestation( + spec, state_c, slot=state_c.slot, signed=False, filter_participant_set=_filter_participant_set + ) + attestation.data.beacon_block_root = signed_block_c.message.hash_tree_root() + assert len([i for i in attestation.aggregation_bits if i == 1]) == participant_num + sign_attestation(spec, state_c, attestation) + + # Attestation_1 received at N+3 — B is head because B's attestation_score > C's proposer_score. + # (B's proposer_score = C's attestation_score = 0) + time = state_d.slot * spec.config.SECONDS_PER_SLOT + store.genesis_time + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + # Block D received at N+3 - C is head, D's boost not sufficient! + yield from add_block(spec, store, signed_block_d, test_steps) + assert spec.get_head(store) == signed_block_c.message.hash_tree_root() + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index d2c84fce79..a524cbd565 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -166,6 +166,9 @@ def test_shorter_chain_but_heavier_weight(spec, state): signed_short_block = state_transition_and_sign_block(spec, short_state, short_block) yield from tick_and_add_block(spec, store, signed_short_block, test_steps) + # Since the long chain has higher proposer_score at slot 1, the latest long block is the head + assert spec.get_head(store) == spec.hash_tree_root(long_block) + short_attestation = get_valid_attestation(spec, short_state, short_block.slot, signed=True) yield from tick_and_run_on_attestation(spec, store, short_attestation, test_steps) diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index 562f851d11..b194dc3bdf 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -6,6 +6,7 @@ phase_0_mods = {key: 'eth2spec.test.phase0.fork_choice.test_' + key for key in [ 'get_head', 'on_block', + 'ex_ante', ]} # No additional Altair specific finality tests, yet. altair_mods = phase_0_mods diff --git a/tests/generators/ssz_generic/ssz_container.py b/tests/generators/ssz_generic/ssz_container.py index 9cd155f76b..8f3a59ce3b 100644 --- a/tests/generators/ssz_generic/ssz_container.py +++ b/tests/generators/ssz_generic/ssz_container.py @@ -106,13 +106,13 @@ def invalid_cases(): RandomizationMode.mode_max_count]: if len(offsets) != 0: for offset_index in offsets: - yield f'{name}_offset_{offset_index}_plus_one', \ + yield f'{name}_{mode.to_name()}_offset_{offset_index}_plus_one', \ invalid_test_case(lambda: mod_offset( b=serialize(container_case_fn(rng, mode, typ)), offset_index=offset_index, change=lambda x: x + 1 )) - yield f'{name}_offset_{offset_index}_zeroed', \ + yield f'{name}_{mode.to_name()}_offset_{offset_index}_zeroed', \ invalid_test_case(lambda: mod_offset( b=serialize(container_case_fn(rng, mode, typ)), offset_index=offset_index,