diff --git a/README.md b/README.md index bc4ba100b..b5a898d37 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The merge is still actively in development. The exact specification has not been * [Merge fork](specs/merge/fork.md) * [Fork Choice changes](specs/merge/fork-choice.md) * [Validator additions](specs/merge/validator.md) + * [Client settings](specs/merge/client_settings.md) ### Sharding @@ -53,7 +54,7 @@ Sharding follows the merge, and is divided into three parts: * Sharding base functionality - In early engineering phase * [Beacon Chain changes](specs/sharding/beacon-chain.md) * [P2P Network changes](specs/sharding/p2p-interface.md) -* Custody Game - Ready, dependent on sharding +* Custody Game - Ready, dependent on sharding * [Beacon Chain changes](specs/custody_game/beacon-chain.md) * [Validator custody work](specs/custody_game/validator.md) * Data Availability Sampling - In active R&D diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e46fab4de --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Please see [Releases](https://github.com/ethereum/consensus-specs/releases/). We recommend using the [most recently released version](https://github.com/ethereum/consensus-specs/releases/latest). + +## Reporting a Vulnerability + +**Please do not file a public ticket** mentioning the vulnerability. + +To find out how to disclose a vulnerability in the Ethereum Consensus Layer visit [https://eth2bounty.ethereum.org](https://eth2bounty.ethereum.org) or email eth2bounty@ethereum.org. Please read the [disclosure page](https://eth2bounty.ethereum.org) for more information about publicly disclosed security vulnerabilities. diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 37a428b50..b067f222f 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -58,8 +58,8 @@ INACTIVITY_SCORE_RECOVERY_RATE: 16 EJECTION_BALANCE: 16000000000 # 2**2 (= 4) MIN_PER_EPOCH_CHURN_LIMIT: 4 -# 2**16 (= 65,536) -CHURN_LIMIT_QUOTIENT: 65536 +# [customized] scale queue churn at much lower validator counts for testing +CHURN_LIMIT_QUOTIENT: 32 # Deposit contract diff --git a/setup.py b/setup.py index ccc3afbe4..281e292dc 100644 --- a/setup.py +++ b/setup.py @@ -223,7 +223,7 @@ def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str]) -> if not _is_constant_id(name): # Check for short type declarations - if value.startswith("uint") or value.startswith("Bytes") or value.startswith("ByteList") or value.startswith("Union"): + if value.startswith(("uint", "Bytes", "ByteList", "Union")): custom_types[name] = value continue diff --git a/specs/merge/client_settings.md b/specs/merge/client_settings.md new file mode 100644 index 000000000..a8ca633ff --- /dev/null +++ b/specs/merge/client_settings.md @@ -0,0 +1,26 @@ + + +**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) + + + +# 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`](fork-choice.md#transitionstore), clients +must provide `--terminal-total-difficulty-override` as a configurable setting. + +If `TransitionStore` has already [been initialized](./fork.md#initializing-transition-store), this alters the previously initialized value of +`TransitionStore.terminal_total_difficulty`, otherwise this setting initializes `TransitionStore` with the specified, bypassing `compute_terminal_total_difficulty` and the use of an `anchor_pow_block`. +`terminal_total_difficulty`. + +Except under exceptional scenarios, this setting is expected to not be used, and `terminal_total_difficulty` will operate with [default functionality](./fork.md#initializing-transition-store). Sufficient warning to the user about this exceptional configurable setting should be provided. +[here](fork.md#initializing-transition-store). diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 900d8b331..de82b17fa 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -73,7 +73,7 @@ def finalize_block(self: ExecutionEngine, block_hash: Hash32) -> bool: ```python @dataclass class TransitionStore(object): - transition_total_difficulty: uint256 + terminal_total_difficulty: uint256 ``` ### `PowBlock` @@ -101,8 +101,8 @@ Used by fork-choice handler, `on_block`. ```python def is_valid_terminal_pow_block(transition_store: TransitionStore, block: PowBlock, parent: PowBlock) -> bool: - is_total_difficulty_reached = block.total_difficulty >= transition_store.transition_total_difficulty - is_parent_total_difficulty_valid = parent.total_difficulty < transition_store.transition_total_difficulty + is_total_difficulty_reached = block.total_difficulty >= transition_store.terminal_total_difficulty + is_parent_total_difficulty_valid = parent.total_difficulty < transition_store.terminal_total_difficulty return block.is_valid and is_total_difficulty_reached and is_parent_total_difficulty_valid ``` @@ -129,7 +129,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock, transition_store: Tr assert get_ancestor(store, block.parent_root, finalized_slot) == store.finalized_checkpoint.root # [New in Merge] - if (transition_store is not None) and is_merge_block(pre_state, block): + if (transition_store is not None) and is_merge_block(pre_state, block.body): # Delay consideration of block until PoW block is processed by the PoW node pow_block = get_pow_block(block.body.execution_payload.parent_hash) pow_parent = get_pow_block(pow_block.parent_hash) diff --git a/specs/merge/fork.md b/specs/merge/fork.md index 0a1271d82..f2547758d 100644 --- a/specs/merge/fork.md +++ b/specs/merge/fork.md @@ -97,18 +97,18 @@ def upgrade_to_merge(pre: altair.BeaconState) -> BeaconState: # Execution-layer latest_execution_payload_header=ExecutionPayloadHeader(), ) - + return post ``` ### Initializing transition store -If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == MERGE_FORK_EPOCH`, a transition store is initialized to be further utilized by the transition process of the Merge. +If `state.slot % SLOTS_PER_EPOCH == 0`, `compute_epoch_at_slot(state.slot) == MERGE_FORK_EPOCH`, and the transition store has not already been initialized, a transition store is initialized to be further utilized by the transition process of the Merge. Transition store initialization occurs after the state has been modified by corresponding `upgrade_to_merge` function. ```python -def compute_transition_total_difficulty(anchor_pow_block: PowBlock) -> uint256: +def compute_terminal_total_difficulty(anchor_pow_block: PowBlock) -> uint256: seconds_per_voting_period = EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH * SECONDS_PER_SLOT pow_blocks_per_voting_period = seconds_per_voting_period // SECONDS_PER_ETH1_BLOCK pow_blocks_to_merge = TARGET_SECONDS_TO_MERGE // SECONDS_PER_ETH1_BLOCK @@ -119,11 +119,14 @@ def compute_transition_total_difficulty(anchor_pow_block: PowBlock) -> uint256: def get_transition_store(anchor_pow_block: PowBlock) -> TransitionStore: - transition_total_difficulty = compute_transition_total_difficulty(anchor_pow_block) - return TransitionStore(transition_total_difficulty=transition_total_difficulty) + terminal_total_difficulty = compute_terminal_total_difficulty(anchor_pow_block) + return TransitionStore(terminal_total_difficulty=terminal_total_difficulty) def initialize_transition_store(state: BeaconState) -> TransitionStore: pow_block = get_pow_block(state.eth1_data.block_hash) return get_transition_store(pow_block) ``` + +*Note*: Transition store can also be initialized at client startup by [overriding terminal total +difficulty](client_settings.md#override-terminal-total-difficulty). diff --git a/specs/merge/validator.md b/specs/merge/validator.md index 84207e49c..efeeca061 100644 --- a/specs/merge/validator.md +++ b/specs/merge/validator.md @@ -95,7 +95,7 @@ def get_execution_payload(state: BeaconState, execution_engine: ExecutionEngine, pow_chain: Sequence[PowBlock]) -> ExecutionPayload: if not is_merge_complete(state): - terminal_pow_block = get_pow_block_at_total_difficulty(transition_store.transition_total_difficulty, pow_chain) + terminal_pow_block = get_pow_block_at_total_difficulty(transition_store.terminal_total_difficulty, pow_chain) if terminal_pow_block is None: # Pre-merge, empty payload return ExecutionPayload() diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 7d382a9ab..276aa8029 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -327,9 +327,13 @@ def on_tick(store: Store, time: uint64) -> None: # Not a new epoch, return if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0): return - # Update store.justified_checkpoint if a better checkpoint is known + + # Update store.justified_checkpoint if a better checkpoint on the store.finalized_checkpoint chain if store.best_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - store.justified_checkpoint = store.best_justified_checkpoint + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + ancestor_at_finalized_slot = get_ancestor(store, store.best_justified_checkpoint.root, finalized_slot) + if ancestor_at_finalized_slot == store.finalized_checkpoint.root: + store.justified_checkpoint = store.best_justified_checkpoint ``` #### `on_block` diff --git a/specs/sharding/beacon-chain.md b/specs/sharding/beacon-chain.md index 2e2c3d487..f54394275 100644 --- a/specs/sharding/beacon-chain.md +++ b/specs/sharding/beacon-chain.md @@ -32,7 +32,7 @@ - [`Builder`](#builder) - [`DataCommitment`](#datacommitment) - [`AttestedDataCommitment`](#attesteddatacommitment) - - [ShardBlobBody](#shardblobbody) + - [`ShardBlobBody`](#shardblobbody) - [`ShardBlobBodySummary`](#shardblobbodysummary) - [`ShardBlob`](#shardblob) - [`ShardBlobHeader`](#shardblobheader) @@ -257,7 +257,7 @@ class AttestedDataCommitment(Container): includer_index: ValidatorIndex ``` -### ShardBlobBody +### `ShardBlobBody` Unsigned shard data, bundled by a shard-builder. Unique, signing different bodies as shard proposer for the same `(slot, shard)` is slashable. @@ -503,14 +503,14 @@ def get_active_shard_count(state: BeaconState, epoch: Epoch) -> uint64: #### `get_shard_proposer_index` ```python -def get_shard_proposer_index(beacon_state: BeaconState, slot: Slot, shard: Shard) -> ValidatorIndex: +def get_shard_proposer_index(state: BeaconState, slot: Slot, shard: Shard) -> ValidatorIndex: """ Return the proposer's index of shard block at ``slot``. """ epoch = compute_epoch_at_slot(slot) - seed = hash(get_seed(beacon_state, epoch, DOMAIN_SHARD_BLOB) + uint_to_bytes(slot) + uint_to_bytes(shard)) + seed = hash(get_seed(state, epoch, DOMAIN_SHARD_BLOB) + uint_to_bytes(slot) + uint_to_bytes(shard)) indices = get_active_validator_indices(state, epoch) - return compute_proposer_index(beacon_state, indices, seed) + return compute_proposer_index(state, indices, seed) ``` #### `get_start_shard` @@ -520,7 +520,7 @@ def get_start_shard(state: BeaconState, slot: Slot) -> Shard: """ Return the start shard at ``slot``. """ - epoch = compute_epoch_at_slot(Slot(_slot)) + epoch = compute_epoch_at_slot(Slot(slot)) committee_count = get_committee_count_per_slot(state, epoch) active_shard_count = get_active_shard_count(state, epoch) return committee_count * slot % active_shard_count @@ -759,7 +759,7 @@ def process_shard_header(state: BeaconState, signed_header: SignedShardBlobHeade commitment=body_summary.commitment, root=header_root, includer_index=get_beacon_proposer_index(state), - ) + ), votes=initial_votes, weight=0, update_slot=state.slot, @@ -885,7 +885,7 @@ def reset_pending_shard_work(state: BeaconState) -> None: selector=SHARD_WORK_PENDING, value=List[PendingShardHeader, MAX_SHARD_HEADERS_PER_SHARD]( PendingShardHeader( - attested=AttestedDataCommitment() + attested=AttestedDataCommitment(), votes=Bitlist[MAX_VALIDATORS_PER_COMMITTEE]([0] * committee_length), weight=0, update_slot=slot, diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt index a0e0952ff..3bec004b5 100644 --- a/tests/core/pyspec/eth2spec/VERSION.txt +++ b/tests/core/pyspec/eth2spec/VERSION.txt @@ -1 +1 @@ -1.1.0-beta.3 \ No newline at end of file +1.1.0-beta.4 \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/gen_helpers/gen_base/gen_runner.py b/tests/core/pyspec/eth2spec/gen_helpers/gen_base/gen_runner.py index be5720265..6850af188 100644 --- a/tests/core/pyspec/eth2spec/gen_helpers/gen_base/gen_runner.py +++ b/tests/core/pyspec/eth2spec/gen_helpers/gen_base/gen_runner.py @@ -1,4 +1,5 @@ import os +import time import shutil import argparse from pathlib import Path @@ -22,6 +23,9 @@ from .gen_typing import TestProvider context.is_pytest = False +TIME_THRESHOLD_TO_PRINT = 1.0 # seconds + + def validate_output_dir(path_str): path = Path(path_str) @@ -73,6 +77,13 @@ def run_generator(generator_name, test_providers: Iterable[TestProvider]): required=False, help="specify presets to run with. Allows all if no preset names are specified.", ) + parser.add_argument( + "-c", + "--collect-only", + action="store_true", + default=False, + help="if set only print tests to generate, do not actually run the test and dump the target data", + ) args = parser.parse_args() output_dir = args.output_dir @@ -96,11 +107,15 @@ def run_generator(generator_name, test_providers: Iterable[TestProvider]): if len(presets) != 0: print(f"Filtering test-generator runs to only include presets: {', '.join(presets)}") + collect_only = args.collect_only + collected_test_count = 0 generated_test_count = 0 skipped_test_count = 0 + provider_start = time.time() for tprov in test_providers: - # runs anything that we don't want to repeat for every test case. - tprov.prepare() + if not collect_only: + # runs anything that we don't want to repeat for every test case. + tprov.prepare() for test_case in tprov.make_cases(): case_dir = ( @@ -110,8 +125,14 @@ def run_generator(generator_name, test_providers: Iterable[TestProvider]): ) incomplete_tag_file = case_dir / "INCOMPLETE" + collected_test_count += 1 + if collect_only: + print(f"Collected test at: {case_dir}") + continue + if case_dir.exists(): if not args.force and not incomplete_tag_file.exists(): + skipped_test_count += 1 print(f'Skipping already existing test: {case_dir}') continue else: @@ -121,6 +142,7 @@ def run_generator(generator_name, test_providers: Iterable[TestProvider]): shutil.rmtree(case_dir) print(f'Generating test: {case_dir}') + test_start = time.time() written_part = False @@ -178,10 +200,22 @@ def run_generator(generator_name, test_providers: Iterable[TestProvider]): generated_test_count += 1 # Only remove `INCOMPLETE` tag file os.remove(incomplete_tag_file) + test_end = time.time() + span = round(test_end - test_start, 2) + if span > TIME_THRESHOLD_TO_PRINT: + print(f' - generated in {span} seconds') - summary_message = f"completed generation of {generator_name} with {generated_test_count} tests" - summary_message += f" ({skipped_test_count} skipped tests)" - print(summary_message) + provider_end = time.time() + span = round(provider_end - provider_start, 2) + + if collect_only: + print(f"Collected {collected_test_count} tests in total") + else: + summary_message = f"completed generation of {generator_name} with {generated_test_count} tests" + summary_message += f" ({skipped_test_count} skipped tests)" + if span > TIME_THRESHOLD_TO_PRINT: + summary_message += f" in {span} seconds" + print(summary_message) def dump_yaml_fn(data: Any, name: str, file_mode: str, yaml_encoder: YAML): diff --git a/tests/core/pyspec/eth2spec/gen_helpers/gen_from_tests/gen.py b/tests/core/pyspec/eth2spec/gen_helpers/gen_from_tests/gen.py index db3f9e949..88bc6d601 100644 --- a/tests/core/pyspec/eth2spec/gen_helpers/gen_from_tests/gen.py +++ b/tests/core/pyspec/eth2spec/gen_helpers/gen_from_tests/gen.py @@ -1,6 +1,6 @@ from importlib import import_module from inspect import getmembers, isfunction -from typing import Any, Callable, Dict, Iterable, Optional +from typing import Any, Callable, Dict, Iterable, Optional, List, Union from eth2spec.utils import bls from eth2spec.test.helpers.constants import ALL_PRESETS, TESTGEN_FORKS @@ -59,8 +59,10 @@ def generate_from_tests(runner_name: str, handler_name: str, src: Any, def get_provider(create_provider_fn: Callable[[SpecForkName, PresetBaseName, str, str], TestProvider], fork_name: SpecForkName, preset_name: PresetBaseName, - all_mods: Dict[str, Dict[str, str]]) -> Iterable[TestProvider]: + all_mods: Dict[str, Dict[str, Union[List[str], str]]]) -> Iterable[TestProvider]: for key, mod_name in all_mods[fork_name].items(): + if not isinstance(mod_name, List): + mod_name = [mod_name] yield create_provider_fn( fork_name=fork_name, preset_name=preset_name, @@ -75,16 +77,17 @@ def get_create_provider_fn(runner_name: str) -> Callable[[SpecForkName, str, str return def create_provider(fork_name: SpecForkName, preset_name: PresetBaseName, - handler_name: str, tests_src_mod_name: str) -> TestProvider: + handler_name: str, tests_src_mod_name: List[str]) -> TestProvider: def cases_fn() -> Iterable[TestCase]: - tests_src = import_module(tests_src_mod_name) - return generate_from_tests( - runner_name=runner_name, - handler_name=handler_name, - src=tests_src, - fork_name=fork_name, - preset_name=preset_name, - ) + for mod_name in tests_src_mod_name: + tests_src = import_module(mod_name) + yield from generate_from_tests( + runner_name=runner_name, + handler_name=handler_name, + src=tests_src, + fork_name=fork_name, + preset_name=preset_name, + ) return TestProvider(prepare=prepare_fn, make_cases=cases_fn) return create_provider diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py index 4be6737e2..8a2deabb1 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate.py @@ -5,6 +5,7 @@ from eth2spec.test.helpers.block import ( from eth2spec.test.helpers.state import ( state_transition_and_sign_block, transition_to, + next_epoch_via_block, ) from eth2spec.test.helpers.constants import ( MAINNET, MINIMAL, @@ -12,10 +13,12 @@ from eth2spec.test.helpers.constants import ( from eth2spec.test.helpers.sync_committee import ( compute_aggregate_sync_committee_signature, compute_committee_indices, - get_committee_indices, run_sync_committee_processing, run_successful_sync_committee_test, ) +from eth2spec.test.helpers.voluntary_exits import ( + get_unslashed_exited_validators, +) from eth2spec.test.context import ( with_altair_and_later, with_presets, @@ -28,19 +31,17 @@ from eth2spec.test.context import ( @spec_state_test @always_bls def test_invalid_signature_bad_domain(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) - rng = random.Random(2020) - random_participant = rng.choice(committee_indices) + committee_indices = compute_committee_indices(spec, state) block = build_empty_block_for_next_slot(spec, state) - # Exclude one participant whose signature was included. block.body.sync_aggregate = spec.SyncAggregate( - sync_committee_bits=[index != random_participant for index in committee_indices], + sync_committee_bits=[True] * len(committee_indices), sync_committee_signature=compute_aggregate_sync_committee_signature( spec, state, block.slot - 1, committee_indices, # full committee signs + block_root=block.parent_root, domain_type=spec.DOMAIN_BEACON_ATTESTER, # Incorrect domain ) ) @@ -51,7 +52,7 @@ def test_invalid_signature_bad_domain(spec, state): @spec_state_test @always_bls def test_invalid_signature_missing_participant(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) rng = random.Random(2020) random_participant = rng.choice(committee_indices) @@ -64,6 +65,7 @@ def test_invalid_signature_missing_participant(spec, state): state, block.slot - 1, committee_indices, # full committee signs + block_root=block.parent_root, ) ) yield from run_sync_committee_processing(spec, state, block, expect_exception=True) @@ -114,7 +116,7 @@ def test_invalid_signature_infinite_signature_with_single_participant(spec, stat @spec_state_test @always_bls def test_invalid_signature_extra_participant(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) rng = random.Random(3030) random_participant = rng.choice(committee_indices) @@ -127,6 +129,7 @@ def test_invalid_signature_extra_participant(spec, state): state, block.slot - 1, [index for index in committee_indices if index != random_participant], + block_root=block.parent_root, ) ) @@ -137,7 +140,7 @@ def test_invalid_signature_extra_participant(spec, state): @with_presets([MINIMAL], reason="to create nonduplicate committee") @spec_state_test def test_sync_committee_rewards_nonduplicate_committee(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=False) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [True] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) @@ -153,7 +156,7 @@ def test_sync_committee_rewards_nonduplicate_committee(spec, state): @with_presets([MAINNET], reason="to create duplicate committee") @spec_state_test def test_sync_committee_rewards_duplicate_committee_no_participation(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=True) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [False] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) @@ -169,7 +172,7 @@ def test_sync_committee_rewards_duplicate_committee_no_participation(spec, state @with_presets([MAINNET], reason="to create duplicate committee") @spec_state_test def test_sync_committee_rewards_duplicate_committee_half_participation(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=True) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [True] * (committee_size // 2) + [False] * (committee_size // 2) assert len(committee_bits) == committee_size @@ -186,7 +189,7 @@ def test_sync_committee_rewards_duplicate_committee_half_participation(spec, sta @with_presets([MAINNET], reason="to create duplicate committee") @spec_state_test def test_sync_committee_rewards_duplicate_committee_full_participation(spec, state): - committee_indices = get_committee_indices(spec, state, duplicates=True) + committee_indices = compute_committee_indices(spec, state) committee_size = len(committee_indices) committee_bits = [True] * committee_size active_validator_count = len(spec.get_active_validator_indices(state, spec.get_current_epoch(state))) @@ -202,7 +205,7 @@ def test_sync_committee_rewards_duplicate_committee_full_participation(spec, sta @spec_state_test @always_bls def test_sync_committee_rewards_not_full_participants(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) rng = random.Random(1010) committee_bits = [rng.choice([True, False]) for _ in committee_indices] @@ -213,7 +216,7 @@ def test_sync_committee_rewards_not_full_participants(spec, state): @spec_state_test @always_bls def test_sync_committee_rewards_empty_participants(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) committee_bits = [False for _ in committee_indices] yield from run_successful_sync_committee_test(spec, state, committee_indices, committee_bits) @@ -223,7 +226,7 @@ def test_sync_committee_rewards_empty_participants(spec, state): @spec_state_test @always_bls def test_invalid_signature_past_block(spec, state): - committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + committee_indices = compute_committee_indices(spec, state) for _ in range(2): # NOTE: need to transition twice to move beyond the degenerate case at genesis @@ -236,6 +239,7 @@ def test_invalid_signature_past_block(spec, state): state, block.slot - 1, committee_indices, + block_root=block.parent_root, ) ) @@ -286,6 +290,7 @@ def test_invalid_signature_previous_committee(spec, state): state, block.slot - 1, committee_indices, + block_root=block.parent_root, ) ) @@ -327,6 +332,7 @@ def test_valid_signature_future_committee(spec, state): state, block.slot - 1, committee_indices, + block_root=block.parent_root, ) ) @@ -360,6 +366,7 @@ def test_proposer_in_committee_without_participation(spec, state): state, block.slot - 1, participants, + block_root=block.parent_root, ) ) @@ -396,6 +403,7 @@ def test_proposer_in_committee_with_participation(spec, state): state, block.slot - 1, committee_indices, + block_root=block.parent_root, ) ) @@ -406,3 +414,191 @@ def test_proposer_in_committee_with_participation(spec, state): else: state_transition_and_sign_block(spec, state, block) raise AssertionError("failed to find a proposer in the sync committee set; check test setup") + + +def _exit_validator_from_committee_and_transition_state(spec, + state, + committee_indices, + rng, + target_epoch_provider, + withdrawable_offset=1): + exited_validator_index = rng.sample(committee_indices, 1)[0] + validator = state.validators[exited_validator_index] + current_epoch = spec.get_current_epoch(state) + validator.exit_epoch = current_epoch + validator.withdrawable_epoch = validator.exit_epoch + withdrawable_offset + + target_epoch = target_epoch_provider(state.validators[exited_validator_index]) + target_slot = target_epoch * spec.SLOTS_PER_EPOCH + transition_to(spec, state, target_slot) + + exited_validator_indices = get_unslashed_exited_validators(spec, state) + assert exited_validator_index in exited_validator_indices + exited_pubkey = state.validators[exited_validator_index].pubkey + assert exited_pubkey in state.current_sync_committee.pubkeys + + return exited_validator_index + + +@with_altair_and_later +@spec_state_test +@always_bls +def test_sync_committee_with_participating_exited_member(spec, state): + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # move forward via some blocks + for _ in range(3): + next_epoch_via_block(spec, state) + + committee_indices = compute_committee_indices(spec, state) + rng = random.Random(1010) + + exited_index = _exit_validator_from_committee_and_transition_state( + spec, + state, + committee_indices, + rng, + lambda v: v.exit_epoch, + ) + + current_epoch = spec.get_current_epoch(state) + assert current_epoch < state.validators[exited_index].withdrawable_epoch + + block = build_empty_block_for_next_slot(spec, state) + block.body.sync_aggregate = spec.SyncAggregate( + sync_committee_bits=[True] * len(committee_indices), + sync_committee_signature=compute_aggregate_sync_committee_signature( + spec, + state, + block.slot - 1, + committee_indices, # full committee signs + block_root=block.parent_root, + ) + ) + yield from run_sync_committee_processing(spec, state, block) + + +@with_altair_and_later +@spec_state_test +@always_bls +def test_sync_committee_with_nonparticipating_exited_member(spec, state): + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # move forward via some blocks + for _ in range(3): + next_epoch_via_block(spec, state) + + committee_indices = compute_committee_indices(spec, state) + rng = random.Random(1010) + + exited_index = _exit_validator_from_committee_and_transition_state( + spec, + state, + committee_indices, + rng, + lambda v: v.exit_epoch, + ) + exited_pubkey = state.validators[exited_index].pubkey + + current_epoch = spec.get_current_epoch(state) + assert current_epoch < state.validators[exited_index].withdrawable_epoch + + exited_committee_index = state.current_sync_committee.pubkeys.index(exited_pubkey) + block = build_empty_block_for_next_slot(spec, state) + committee_bits = [i != exited_committee_index for i in committee_indices] + committee_indices = [index for index in committee_indices if index != exited_committee_index] + block.body.sync_aggregate = spec.SyncAggregate( + sync_committee_bits=committee_bits, + sync_committee_signature=compute_aggregate_sync_committee_signature( + spec, + state, + block.slot - 1, + committee_indices, # with exited validator removed + block_root=block.parent_root, + ) + ) + yield from run_sync_committee_processing(spec, state, block) + + +@with_altair_and_later +@spec_state_test +@always_bls +def test_sync_committee_with_participating_withdrawable_member(spec, state): + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # move forward via some blocks + for _ in range(3): + next_epoch_via_block(spec, state) + + committee_indices = compute_committee_indices(spec, state) + rng = random.Random(1010) + + exited_index = _exit_validator_from_committee_and_transition_state( + spec, + state, + committee_indices, + rng, + lambda v: v.withdrawable_epoch + 1, + ) + + current_epoch = spec.get_current_epoch(state) + assert current_epoch > state.validators[exited_index].withdrawable_epoch + + block = build_empty_block_for_next_slot(spec, state) + block.body.sync_aggregate = spec.SyncAggregate( + sync_committee_bits=[True] * len(committee_indices), + sync_committee_signature=compute_aggregate_sync_committee_signature( + spec, + state, + block.slot - 1, + committee_indices, # full committee signs + block_root=block.parent_root, + ) + ) + yield from run_sync_committee_processing(spec, state, block) + + +@with_altair_and_later +@spec_state_test +@always_bls +def test_sync_committee_with_nonparticipating_withdrawable_member(spec, state): + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # move forward via some blocks + for _ in range(3): + next_epoch_via_block(spec, state) + + committee_indices = compute_committee_indices(spec, state) + rng = random.Random(1010) + + exited_index = _exit_validator_from_committee_and_transition_state( + spec, + state, + committee_indices, + rng, + lambda v: v.withdrawable_epoch + 1, + ) + exited_pubkey = state.validators[exited_index].pubkey + + current_epoch = spec.get_current_epoch(state) + assert current_epoch > state.validators[exited_index].withdrawable_epoch + + target_committee_index = state.current_sync_committee.pubkeys.index(exited_pubkey) + block = build_empty_block_for_next_slot(spec, state) + committee_bits = [i != target_committee_index for i in committee_indices] + committee_indices = [index for index in committee_indices if index != target_committee_index] + block.body.sync_aggregate = spec.SyncAggregate( + sync_committee_bits=committee_bits, + sync_committee_signature=compute_aggregate_sync_committee_signature( + spec, + state, + block.slot - 1, + committee_indices, # with withdrawable validator removed + block_root=block.parent_root, + ) + ) + yield from run_sync_committee_processing(spec, state, block) diff --git a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py index 75845e060..903df4081 100644 --- a/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py +++ b/tests/core/pyspec/eth2spec/test/altair/block_processing/sync_aggregate/test_process_sync_aggregate_random.py @@ -2,10 +2,19 @@ import random from eth2spec.test.helpers.constants import ( MAINNET, MINIMAL, ) +from eth2spec.test.helpers.random import ( + randomize_state, +) +from eth2spec.test.helpers.state import ( + has_active_balance_differential, +) from eth2spec.test.helpers.sync_committee import ( - get_committee_indices, + compute_committee_indices, run_successful_sync_committee_test, ) +from eth2spec.test.helpers.voluntary_exits import ( + get_unslashed_exited_validators, +) from eth2spec.test.context import ( with_altair_and_later, spec_state_test, @@ -18,8 +27,8 @@ from eth2spec.test.context import ( ) -def _test_harness_for_randomized_test_case(spec, state, duplicates=False, participation_fn=None): - committee_indices = get_committee_indices(spec, state, duplicates=duplicates) +def _test_harness_for_randomized_test_case(spec, state, expect_duplicates=False, participation_fn=None): + committee_indices = compute_committee_indices(spec, state) if participation_fn: participating_indices = participation_fn(committee_indices) @@ -28,7 +37,7 @@ def _test_harness_for_randomized_test_case(spec, state, duplicates=False, partic committee_bits = [index in participating_indices for index in committee_indices] committee_size = len(committee_indices) - if duplicates: + if expect_duplicates: assert committee_size > len(set(committee_indices)) else: assert committee_size == len(set(committee_indices)) @@ -44,7 +53,7 @@ def test_random_only_one_participant_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: [rng.choice(comm)], ) @@ -57,7 +66,7 @@ def test_random_low_participation_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, int(len(comm) * 0.25)), ) @@ -70,7 +79,7 @@ def test_random_high_participation_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, int(len(comm) * 0.75)), ) @@ -83,7 +92,7 @@ def test_random_all_but_one_participating_with_duplicates(spec, state): yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, len(comm) - 1), ) @@ -98,7 +107,25 @@ def test_random_misc_balances_and_half_participation_with_duplicates(spec, state yield from _test_harness_for_randomized_test_case( spec, state, - duplicates=True, + expect_duplicates=True, + participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), + ) + + +@with_altair_and_later +@with_presets([MAINNET], reason="to create duplicate committee") +@spec_state_test +@single_phase +def test_random_with_exits_with_duplicates(spec, state): + rng = random.Random(1402) + randomize_state(spec, state, rng=rng, exit_fraction=0.1, slash_fraction=0.0) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + yield from _test_harness_for_randomized_test_case( + spec, + state, + expect_duplicates=True, participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), ) @@ -163,3 +190,20 @@ def test_random_misc_balances_and_half_participation_without_duplicates(spec, st state, participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), ) + + +@with_altair_and_later +@with_presets([MINIMAL], reason="to create nonduplicate committee") +@spec_state_test +@single_phase +def test_random_with_exits_without_duplicates(spec, state): + rng = random.Random(1502) + randomize_state(spec, state, rng=rng, exit_fraction=0.1, slash_fraction=0.0) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + yield from _test_harness_for_randomized_test_case( + spec, + state, + participation_fn=lambda comm: rng.sample(comm, len(comm) // 2), + ) diff --git a/tests/core/pyspec/eth2spec/test/altair/random/__init__.py b/tests/core/pyspec/eth2spec/test/altair/random/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/altair/random/test_random.py b/tests/core/pyspec/eth2spec/test/altair/random/test_random.py new file mode 100644 index 000000000..2250101bd --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/random/test_random.py @@ -0,0 +1,438 @@ +""" +This module is generated from the ``random`` test generator. +Please do not edit this file manually. +See the README for that generator for more information. +""" + +from eth2spec.test.helpers.constants import ALTAIR +from eth2spec.test.context import ( + misc_balances_in_default_range_with_many_validators, + with_phases, + zero_activation_threshold, + only_generator, +) +from eth2spec.test.context import ( + always_bls, + spec_test, + with_custom_state, + single_phase, +) +from eth2spec.test.utils.randomized_block_tests import ( + run_generated_randomized_test, +) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_0(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_1(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_2(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_3(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_4(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_5(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_6(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_7(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_8(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_9(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_10(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_11(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_12(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_13(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_14(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([ALTAIR]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_15(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block_altair_with_cycling_sync_committee_participation + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block_altair_with_cycling_sync_committee_participation', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state_altair'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 7fddc0762..ef92efade 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -126,6 +126,17 @@ def default_balances(spec): return [spec.MAX_EFFECTIVE_BALANCE] * num_validators +def scaled_churn_balances(spec): + """ + Helper method to create enough validators to scale the churn limit. + (This is *firmly* over the churn limit -- thus the +2 instead of just +1) + See the second argument of ``max`` in ``get_validator_churn_limit``. + Usage: `@with_custom_state(balances_fn=scaled_churn_balances, ...)` + """ + num_validators = spec.config.CHURN_LIMIT_QUOTIENT * (2 + spec.config.MIN_PER_EPOCH_CHURN_LIMIT) + return [spec.MAX_EFFECTIVE_BALANCE] * num_validators + + with_state = with_custom_state(default_balances, default_activation_threshold) @@ -152,6 +163,22 @@ def misc_balances(spec): return balances +def misc_balances_in_default_range_with_many_validators(spec): + """ + Helper method to create a series of balances that includes some misc. balances but + none that are below the ``EJECTION_BALANCE``. + """ + # Double validators to facilitate randomized testing + num_validators = spec.SLOTS_PER_EPOCH * 8 * 2 + floor = spec.config.EJECTION_BALANCE + spec.EFFECTIVE_BALANCE_INCREMENT + balances = [ + max(spec.MAX_EFFECTIVE_BALANCE * 2 * i // num_validators, floor) for i in range(num_validators) + ] + rng = Random(1234) + rng.shuffle(balances) + return balances + + def low_single_balance(spec): """ Helper method to create a single of balance of 1 Gwei. @@ -440,6 +467,17 @@ with_altair_and_later = with_phases([ALTAIR, MERGE]) with_merge_and_later = with_phases([MERGE]) # TODO: include sharding when spec stabilizes. +def only_generator(reason): + def _decorator(inner): + def _wrapper(*args, **kwargs): + if is_pytest: + dump_skipping_message(reason) + return None + return inner(*args, **kwargs) + return _wrapper + return _decorator + + def fork_transition_test(pre_fork_name, post_fork_name, fork_epoch=None): """ A decorator to construct a "transition" test from one fork of the eth2 spec diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index ec5793af5..65d6975f2 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -156,9 +156,18 @@ def add_block(spec, store, signed_block, test_steps, valid=True, allow_invalid_a 'checks': { 'time': int(store.time), 'head': get_formatted_head_output(spec, store), - 'justified_checkpoint_root': encode_hex(store.justified_checkpoint.root), - 'finalized_checkpoint_root': encode_hex(store.finalized_checkpoint.root), - 'best_justified_checkpoint': encode_hex(store.best_justified_checkpoint.root), + 'justified_checkpoint': { + 'epoch': int(store.justified_checkpoint.epoch), + 'root': encode_hex(store.justified_checkpoint.root), + }, + 'finalized_checkpoint': { + 'epoch': int(store.finalized_checkpoint.epoch), + 'root': encode_hex(store.finalized_checkpoint.root), + }, + 'best_justified_checkpoint': { + 'epoch': int(store.best_justified_checkpoint.epoch), + 'root': encode_hex(store.best_justified_checkpoint.root), + }, } }) diff --git a/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py b/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py index 4b6c9b25d..c2cc6d98a 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py @@ -7,6 +7,10 @@ from eth2spec.test.helpers.state import ( from eth2spec.test.helpers.block import ( build_empty_block_for_next_slot, ) +from eth2spec.test.helpers.sync_committee import ( + compute_committee_indices, + compute_aggregate_sync_committee_signature, +) from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing_by_indices from eth2spec.test.helpers.attestations import get_valid_attestation @@ -44,8 +48,12 @@ def run_slash_and_exit(spec, state, slash_index, exit_index, valid=True): def get_random_proposer_slashings(spec, state, rng): - num_slashings = rng.randrange(spec.MAX_PROPOSER_SLASHINGS) - indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)).copy() + num_slashings = rng.randrange(1, spec.MAX_PROPOSER_SLASHINGS) + active_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)).copy() + indices = [ + index for index in active_indices + if not state.validators[index].slashed + ] slashings = [ get_valid_proposer_slashing( spec, state, @@ -56,14 +64,33 @@ def get_random_proposer_slashings(spec, state, rng): return slashings -def get_random_attester_slashings(spec, state, rng): - num_slashings = rng.randrange(spec.MAX_ATTESTER_SLASHINGS) - indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)).copy() +def get_random_attester_slashings(spec, state, rng, slashed_indices=[]): + """ + Caller can supply ``slashed_indices`` if they are aware of other indices + that will be slashed by other operations in the same block as the one that + contains the output of this function. + """ + # ensure at least one attester slashing, the max count + # is small so not much room for random inclusion + num_slashings = rng.randrange(1, spec.MAX_ATTESTER_SLASHINGS) + active_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)).copy() + indices = [ + index for index in active_indices + if ( + not state.validators[index].slashed + and index not in slashed_indices + ) + ] + sample_upper_bound = 4 + max_slashed_count = num_slashings * sample_upper_bound - 1 + if len(indices) < max_slashed_count: + return [] + slot_range = list(range(state.slot - spec.SLOTS_PER_HISTORICAL_ROOT + 1, state.slot)) slashings = [ get_valid_attester_slashing_by_indices( spec, state, - sorted([indices.pop(rng.randrange(len(indices))) for _ in range(rng.randrange(1, 4))]), + sorted([indices.pop(rng.randrange(len(indices))) for _ in range(rng.randrange(1, sample_upper_bound))]), slot=slot_range.pop(rng.randrange(len(slot_range))), signed_1=True, signed_2=True, ) @@ -73,7 +100,7 @@ def get_random_attester_slashings(spec, state, rng): def get_random_attestations(spec, state, rng): - num_attestations = rng.randrange(spec.MAX_ATTESTATIONS) + num_attestations = rng.randrange(1, spec.MAX_ATTESTATIONS) attestations = [ get_valid_attestation( @@ -86,8 +113,12 @@ def get_random_attestations(spec, state, rng): return attestations -def prepare_state_and_get_random_deposits(spec, state, rng): - num_deposits = rng.randrange(spec.MAX_DEPOSITS) +def get_random_deposits(spec, state, rng, num_deposits=None): + if not num_deposits: + num_deposits = rng.randrange(1, spec.MAX_DEPOSITS) + + if num_deposits == 0: + return [], b"\x00" * 32 deposit_data_leaves = [spec.DepositData() for _ in range(len(state.validators))] deposits = [] @@ -105,38 +136,82 @@ def prepare_state_and_get_random_deposits(spec, state, rng): signed=True, ) - state.eth1_data.deposit_root = root - state.eth1_data.deposit_count += num_deposits - # Then for that context, build deposits/proofs for i in range(num_deposits): index = len(state.validators) + i deposit, _, _ = deposit_from_context(spec, deposit_data_leaves, index) deposits.append(deposit) + return deposits, root + + +def prepare_state_and_get_random_deposits(spec, state, rng, num_deposits=None): + deposits, root = get_random_deposits(spec, state, rng, num_deposits=num_deposits) + state.eth1_data.deposit_root = root + state.eth1_data.deposit_count += len(deposits) return deposits +def _eligible_for_exit(spec, state, index): + validator = state.validators[index] + + not_slashed = not validator.slashed + + current_epoch = spec.get_current_epoch(state) + activation_epoch = validator.activation_epoch + active_for_long_enough = current_epoch >= activation_epoch + spec.config.SHARD_COMMITTEE_PERIOD + + not_exited = validator.exit_epoch == spec.FAR_FUTURE_EPOCH + + return not_slashed and active_for_long_enough and not_exited + + def get_random_voluntary_exits(spec, state, to_be_slashed_indices, rng): - num_exits = rng.randrange(spec.MAX_VOLUNTARY_EXITS) - indices = set(spec.get_active_validator_indices(state, spec.get_current_epoch(state)).copy()) + num_exits = rng.randrange(1, spec.MAX_VOLUNTARY_EXITS) + active_indices = set(spec.get_active_validator_indices(state, spec.get_current_epoch(state)).copy()) + indices = set( + index for index in active_indices + if _eligible_for_exit(spec, state, index) + ) eligible_indices = indices - to_be_slashed_indices - exit_indices = [eligible_indices.pop() for _ in range(num_exits)] + indices_count = min(num_exits, len(eligible_indices)) + exit_indices = [eligible_indices.pop() for _ in range(indices_count)] return prepare_signed_exits(spec, state, exit_indices) -def run_test_full_random_operations(spec, state, rng=Random(2080)): - # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit - state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH +def get_random_sync_aggregate(spec, state, slot, block_root=None, fraction_participated=1.0, rng=Random(2099)): + committee_indices = compute_committee_indices(spec, state, state.current_sync_committee) + participant_count = int(len(committee_indices) * fraction_participated) + participant_indices = rng.sample(range(len(committee_indices)), participant_count) + participants = [ + committee_indices[index] + for index in participant_indices + ] + signature = compute_aggregate_sync_committee_signature( + spec, + state, + slot, + participants, + block_root=block_root, + ) + return spec.SyncAggregate( + sync_committee_bits=[index in participant_indices for index in range(len(committee_indices))], + sync_committee_signature=signature, + ) - # prepare state for deposits before building block - deposits = prepare_state_and_get_random_deposits(spec, state, rng) +def build_random_block_from_state_for_next_slot(spec, state, rng=Random(2188), deposits=None): block = build_empty_block_for_next_slot(spec, state) - block.body.proposer_slashings = get_random_proposer_slashings(spec, state, rng) - block.body.attester_slashings = get_random_attester_slashings(spec, state, rng) + proposer_slashings = get_random_proposer_slashings(spec, state, rng) + block.body.proposer_slashings = proposer_slashings + slashed_indices = [ + slashing.signed_header_1.message.proposer_index + for slashing in proposer_slashings + ] + block.body.attester_slashings = get_random_attester_slashings(spec, state, rng, slashed_indices) block.body.attestations = get_random_attestations(spec, state, rng) - block.body.deposits = deposits + if deposits: + block.body.deposits = deposits # cannot include to be slashed indices as exits slashed_indices = set([ @@ -148,6 +223,17 @@ def run_test_full_random_operations(spec, state, rng=Random(2080)): slashed_indices = slashed_indices.union(attester_slashing.attestation_2.attesting_indices) block.body.voluntary_exits = get_random_voluntary_exits(spec, state, slashed_indices, rng) + return block + + +def run_test_full_random_operations(spec, state, rng=Random(2080)): + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # prepare state for deposits before building block + deposits = prepare_state_and_get_random_deposits(spec, state, rng) + block = build_random_block_from_state_for_next_slot(spec, state, rng, deposits=deposits) + yield 'pre', state signed_block = state_transition_and_sign_block(spec, state, block) diff --git a/tests/core/pyspec/eth2spec/test/helpers/random.py b/tests/core/pyspec/eth2spec/test/helpers/random.py index 70c871a34..8448b2424 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/random.py +++ b/tests/core/pyspec/eth2spec/test/helpers/random.py @@ -20,32 +20,40 @@ def set_some_new_deposits(spec, state, rng): state.validators[index].activation_eligibility_epoch = spec.get_current_epoch(state) -def exit_random_validators(spec, state, rng): +def exit_random_validators(spec, state, rng, fraction=None): + if fraction is None: + # Exit ~1/2 + fraction = 0.5 + if spec.get_current_epoch(state) < 5: # Move epochs forward to allow for some validators already exited/withdrawable for _ in range(5): next_epoch(spec, state) current_epoch = spec.get_current_epoch(state) - # Exit ~1/2 of validators for index in spec.get_active_validator_indices(state, current_epoch): - if rng.choice([True, False]): + sampled = rng.random() < fraction + if not sampled: continue validator = state.validators[index] - validator.exit_epoch = rng.choice([current_epoch - 1, current_epoch - 2, current_epoch - 3]) - # ~1/2 are withdrawable + validator.exit_epoch = rng.choice([current_epoch, current_epoch - 1, current_epoch - 2, current_epoch - 3]) + # ~1/2 are withdrawable (note, unnatural span between exit epoch and withdrawable epoch) if rng.choice([True, False]): validator.withdrawable_epoch = current_epoch else: validator.withdrawable_epoch = current_epoch + 1 -def slash_random_validators(spec, state, rng): - # Slash ~1/2 of validators +def slash_random_validators(spec, state, rng, fraction=None): + if fraction is None: + # Slash ~1/2 of validators + fraction = 0.5 + for index in range(len(state.validators)): # slash at least one validator - if index == 0 or rng.choice([True, False]): + sampled = rng.random() < fraction + if index == 0 or sampled: spec.slash_validator(state, index) @@ -115,8 +123,39 @@ def randomize_attestation_participation(spec, state, rng=Random(8020)): randomize_epoch_participation(spec, state, spec.get_current_epoch(state), rng) -def randomize_state(spec, state, rng=Random(8020)): +def randomize_state(spec, state, rng=Random(8020), exit_fraction=None, slash_fraction=None): set_some_new_deposits(spec, state, rng) - exit_random_validators(spec, state, rng) - slash_random_validators(spec, state, rng) + exit_random_validators(spec, state, rng, fraction=exit_fraction) + slash_random_validators(spec, state, rng, fraction=slash_fraction) randomize_attestation_participation(spec, state, rng) + + +def patch_state_to_non_leaking(spec, state): + """ + This function performs an irregular state transition so that: + 1. the current justified checkpoint references the previous epoch + 2. the previous justified checkpoint references the epoch before previous + 3. the finalized checkpoint matches the previous justified checkpoint + + The effects of this function are intended to offset randomization side effects + performed by other functionality in this module so that if the ``state`` was leaking, + then the ``state`` is not leaking after. + """ + state.justification_bits[0] = True + state.justification_bits[1] = True + previous_epoch = spec.get_previous_epoch(state) + previous_root = spec.get_block_root(state, previous_epoch) + previous_previous_epoch = max(spec.GENESIS_EPOCH, spec.Epoch(previous_epoch - 1)) + previous_previous_root = spec.get_block_root(state, previous_previous_epoch) + state.previous_justified_checkpoint = spec.Checkpoint( + epoch=previous_previous_epoch, + root=previous_previous_root, + ) + state.current_justified_checkpoint = spec.Checkpoint( + epoch=previous_epoch, + root=previous_root, + ) + state.finalized_checkpoint = spec.Checkpoint( + epoch=previous_previous_epoch, + root=previous_previous_root, + ) diff --git a/tests/core/pyspec/eth2spec/test/helpers/rewards.py b/tests/core/pyspec/eth2spec/test/helpers/rewards.py index 1867db08f..ec617bda9 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/rewards.py +++ b/tests/core/pyspec/eth2spec/test/helpers/rewards.py @@ -255,7 +255,19 @@ def run_get_inactivity_penalty_deltas(spec, state): else: assert penalties[index] > base_penalty else: - assert penalties[index] == 0 + if not is_post_altair(spec): + assert penalties[index] == 0 + continue + else: + # post altair, this penalty is derived from the inactivity score + # regardless if the state is leaking or not... + if index in matching_attesting_indices: + assert penalties[index] == 0 + else: + # copied from spec: + penalty_numerator = state.validators[index].effective_balance * state.inactivity_scores[index] + penalty_denominator = spec.config.INACTIVITY_SCORE_BIAS * spec.INACTIVITY_PENALTY_QUOTIENT_ALTAIR + assert penalties[index] == penalty_numerator // penalty_denominator def transition_state_to_leak(spec, state, epochs=None): diff --git a/tests/core/pyspec/eth2spec/test/helpers/state.py b/tests/core/pyspec/eth2spec/test/helpers/state.py index 666023fec..6f1923e54 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/state.py +++ b/tests/core/pyspec/eth2spec/test/helpers/state.py @@ -1,5 +1,6 @@ from eth2spec.test.context import expect_assertion_error, is_post_altair from eth2spec.test.helpers.block import apply_empty_block, sign_block, transition_unsigned_block +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators def get_balance(state, index): @@ -133,3 +134,34 @@ def _set_empty_participation(spec, state, current=True, previous=True): def set_empty_participation(spec, state, rng=None): _set_empty_participation(spec, state) + + +def ensure_state_has_validators_across_lifecycle(spec, state): + """ + Scan the validator registry to ensure there is at least 1 validator + for each of the following lifecycle states: + 1. Pending / deposited + 2. Active + 3. Exited (but not slashed) + 4. Slashed + """ + has_pending = any(filter(spec.is_eligible_for_activation_queue, state.validators)) + + current_epoch = spec.get_current_epoch(state) + has_active = any(filter(lambda v: spec.is_active_validator(v, current_epoch), state.validators)) + + has_exited = any(get_unslashed_exited_validators(spec, state)) + + has_slashed = any(filter(lambda v: v.slashed, state.validators)) + + return has_pending and has_active and has_exited and has_slashed + + +def has_active_balance_differential(spec, state): + """ + Ensure there is a difference between the total balance of + all _active_ validators and _all_ validators. + """ + active_balance = spec.get_total_active_balance(state) + total_balance = spec.get_total_balance(state, set(range(len(state.validators)))) + return active_balance // spec.EFFECTIVE_BALANCE_INCREMENT != total_balance // spec.EFFECTIVE_BALANCE_INCREMENT diff --git a/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py b/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py index e59f679e1..417802ece 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py +++ b/tests/core/pyspec/eth2spec/test/helpers/sync_committee.py @@ -9,7 +9,6 @@ from eth2spec.test.helpers.block import ( ) from eth2spec.test.helpers.block_processing import run_block_processing_to from eth2spec.utils import bls -from eth2spec.utils.hash_function import hash def compute_sync_committee_signature(spec, state, slot, privkey, block_root=None, domain_type=None): @@ -75,10 +74,12 @@ def compute_sync_committee_proposer_reward(spec, state, committee_indices, commi return spec.Gwei(participant_reward * participant_number) -def compute_committee_indices(spec, state, committee): +def compute_committee_indices(spec, state, committee=None): """ Given a ``committee``, calculate and return the related indices """ + if committee is None: + committee = state.current_sync_committee all_pubkeys = [v.pubkey for v in state.validators] return [all_pubkeys.index(pubkey) for pubkey in committee.pubkeys] @@ -153,6 +154,7 @@ def _build_block_for_next_slot_with_sync_participation(spec, state, committee_in state, block.slot - 1, [index for index, bit in zip(committee_indices, committee_bits) if bit], + block_root=block.parent_root, ) ) return block @@ -161,23 +163,3 @@ def _build_block_for_next_slot_with_sync_participation(spec, state, committee_in def run_successful_sync_committee_test(spec, state, committee_indices, committee_bits): block = _build_block_for_next_slot_with_sync_participation(spec, state, committee_indices, committee_bits) yield from run_sync_committee_processing(spec, state, block) - - -def get_committee_indices(spec, state, duplicates=False): - """ - This utility function allows the caller to ensure there are or are not - duplicate validator indices in the returned committee based on - the boolean ``duplicates``. - """ - state = state.copy() - current_epoch = spec.get_current_epoch(state) - randao_index = (current_epoch + 1) % spec.EPOCHS_PER_HISTORICAL_VECTOR - while True: - committee = spec.get_next_sync_committee_indices(state) - if duplicates: - if len(committee) != len(set(committee)): - return committee - else: - if len(committee) == len(set(committee)): - return committee - state.randao_mixes[randao_index] = hash(state.randao_mixes[randao_index]) diff --git a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py index 73d4598b3..55ea0b5b0 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py +++ b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py @@ -34,6 +34,13 @@ def get_exited_validators(spec, state): return [index for (index, validator) in enumerate(state.validators) if validator.exit_epoch <= current_epoch] +def get_unslashed_exited_validators(spec, state): + return [ + index for index in get_exited_validators(spec, state) + if not state.validators[index].slashed + ] + + def exit_validators(spec, state, validator_count, rng=None): if rng is None: rng = Random(1337) diff --git a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py index f713d1792..9e209f23e 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py +++ b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py @@ -1,4 +1,10 @@ -from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls, with_all_phases +from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.context import ( + spec_state_test, expect_assertion_error, + always_bls, with_all_phases, with_presets, + spec_test, single_phase, + with_custom_state, scaled_churn_balances, +) from eth2spec.test.helpers.keys import pubkey_to_privkey from eth2spec.test.helpers.voluntary_exits import sign_voluntary_exit @@ -68,9 +74,7 @@ def test_invalid_signature(spec, state): yield from run_voluntary_exit_processing(spec, state, signed_voluntary_exit, False) -@with_all_phases -@spec_state_test -def test_success_exit_queue(spec, state): +def run_test_success_exit_queue(spec, state): # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit state.slot += spec.config.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH @@ -106,10 +110,29 @@ def test_success_exit_queue(spec, state): # when processing an additional exit, it results in an exit in a later epoch yield from run_voluntary_exit_processing(spec, state, signed_voluntary_exit) - assert ( - state.validators[validator_index].exit_epoch == - state.validators[initial_indices[0]].exit_epoch + 1 - ) + for index in initial_indices: + assert ( + state.validators[validator_index].exit_epoch == + state.validators[index].exit_epoch + 1 + ) + + +@with_all_phases +@spec_state_test +def test_success_exit_queue__min_churn(spec, state): + yield from run_test_success_exit_queue(spec, state) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_success_exit_queue__scaled_churn(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_success_exit_queue(spec, state) @with_all_phases diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py index 9db6076f8..1d3197ba6 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_justification_and_finalization.py @@ -1,8 +1,10 @@ +from random import Random from eth2spec.test.context import is_post_altair, spec_state_test, with_all_phases from eth2spec.test.helpers.epoch_processing import ( run_epoch_processing_with, ) -from eth2spec.test.helpers.state import transition_to +from eth2spec.test.helpers.state import transition_to, next_epoch_via_block, next_slot +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators def run_process_just_and_fin(spec, state): @@ -300,3 +302,76 @@ def test_12_ok_support_messed_target(spec, state): @spec_state_test def test_12_poor_support(spec, state): yield from finalize_on_12(spec, state, 3, False, False) + + +@with_all_phases +@spec_state_test +def test_balance_threshold_with_exited_validators(spec, state): + """ + This test exercises a very specific failure mode where + exited validators are incorrectly included in the total active balance + when weighing justification. + """ + rng = Random(133333) + # move past genesis conditions + for _ in range(3): + next_epoch_via_block(spec, state) + + # mock attestation helper requires last slot of epoch + for _ in range(spec.SLOTS_PER_EPOCH - 1): + next_slot(spec, state) + + # Step 1: Exit ~1/2 vals in current epoch + epoch = spec.get_current_epoch(state) + for index in spec.get_active_validator_indices(state, epoch): + if rng.choice([True, False]): + continue + + validator = state.validators[index] + validator.exit_epoch = epoch + validator.withdrawable_epoch = epoch + 1 + validator.withdrawable_epoch = validator.exit_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + + exited_validators = get_unslashed_exited_validators(spec, state) + assert len(exited_validators) != 0 + + source = state.current_justified_checkpoint + target = spec.Checkpoint( + epoch=epoch, + root=spec.get_block_root(state, epoch) + ) + add_mock_attestations( + spec, + state, + epoch, + source, + target, + sufficient_support=False, + ) + + if not is_post_altair(spec): + current_attestations = spec.get_matching_target_attestations(state, epoch) + total_active_balance = spec.get_total_active_balance(state) + current_target_balance = spec.get_attesting_balance(state, current_attestations) + # Check we will not justify the current checkpoint + does_justify = current_target_balance * 3 >= total_active_balance * 2 + assert not does_justify + # Ensure we would have justified the current checkpoint w/ the exited validators + current_exited_balance = spec.get_total_balance(state, exited_validators) + does_justify = (current_target_balance + current_exited_balance) * 3 >= total_active_balance * 2 + assert does_justify + else: + current_indices = spec.get_unslashed_participating_indices(state, spec.TIMELY_TARGET_FLAG_INDEX, epoch) + total_active_balance = spec.get_total_active_balance(state) + current_target_balance = spec.get_total_balance(state, current_indices) + # Check we will not justify the current checkpoint + does_justify = current_target_balance * 3 >= total_active_balance * 2 + assert not does_justify + # Ensure we would have justified the current checkpoint w/ the exited validators + current_exited_balance = spec.get_total_balance(state, exited_validators) + does_justify = (current_target_balance + current_exited_balance) * 3 >= total_active_balance * 2 + assert does_justify + + yield from run_process_just_and_fin(spec, state) + + assert state.current_justified_checkpoint.epoch != epoch diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py index 6e7784aa9..6539dc92d 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py @@ -1,6 +1,12 @@ from eth2spec.test.helpers.deposits import mock_deposit from eth2spec.test.helpers.state import next_epoch, next_slots -from eth2spec.test.context import spec_state_test, with_all_phases +from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.context import ( + spec_test, spec_state_test, + with_all_phases, single_phase, + with_custom_state, with_presets, + scaled_churn_balances, +) from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with @@ -112,9 +118,7 @@ def test_activation_queue_sorting(spec, state): assert state.validators[churn_limit - 1].activation_epoch != spec.FAR_FUTURE_EPOCH -@with_all_phases -@spec_state_test -def test_activation_queue_efficiency(spec, state): +def run_test_activation_queue_efficiency(spec, state): churn_limit = spec.get_validator_churn_limit(state) mock_activations = churn_limit * 2 @@ -128,23 +132,45 @@ def test_activation_queue_efficiency(spec, state): state.finalized_checkpoint.epoch = epoch + 1 + # Churn limit could have changed given the active vals removed via `mock_deposit` + churn_limit_0 = spec.get_validator_churn_limit(state) + # Run first registry update. Do not yield test vectors for _ in run_process_registry_updates(spec, state): pass # Half should churn in first run of registry update for i in range(mock_activations): - if i < mock_activations // 2: + if i < churn_limit_0: assert state.validators[i].activation_epoch < spec.FAR_FUTURE_EPOCH else: assert state.validators[i].activation_epoch == spec.FAR_FUTURE_EPOCH # Second half should churn in second run of registry update + churn_limit_1 = spec.get_validator_churn_limit(state) yield from run_process_registry_updates(spec, state) - for i in range(mock_activations): + for i in range(churn_limit_0 + churn_limit_1): assert state.validators[i].activation_epoch < spec.FAR_FUTURE_EPOCH +@with_all_phases +@spec_state_test +def test_activation_queue_efficiency_min(spec, state): + assert spec.get_validator_churn_limit(state) == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_efficiency(spec, state) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_activation_queue_efficiency_scaled(spec, state): + assert spec.get_validator_churn_limit(state) > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_efficiency(spec, state) + + @with_all_phases @spec_state_test def test_ejection(spec, state): @@ -165,9 +191,7 @@ def test_ejection(spec, state): ) -@with_all_phases -@spec_state_test -def test_ejection_past_churn_limit(spec, state): +def run_test_ejection_past_churn_limit(spec, state): churn_limit = spec.get_validator_churn_limit(state) # try to eject more than per-epoch churn limit @@ -184,58 +208,137 @@ def test_ejection_past_churn_limit(spec, state): # first third ejected in normal speed if i < mock_ejections // 3: assert state.validators[i].exit_epoch == expected_ejection_epoch - # second thirdgets delayed by 1 epoch + # second third gets delayed by 1 epoch elif mock_ejections // 3 <= i < mock_ejections * 2 // 3: assert state.validators[i].exit_epoch == expected_ejection_epoch + 1 - # second thirdgets delayed by 2 epochs + # final third gets delayed by 2 epochs else: assert state.validators[i].exit_epoch == expected_ejection_epoch + 2 @with_all_phases @spec_state_test -def test_activation_queue_activation_and_ejection(spec, state): +def test_ejection_past_churn_limit_min(spec, state): + assert spec.get_validator_churn_limit(state) == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_ejection_past_churn_limit(spec, state) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_ejection_past_churn_limit_scaled(spec, state): + assert spec.get_validator_churn_limit(state) > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_ejection_past_churn_limit(spec, state) + + +def run_test_activation_queue_activation_and_ejection(spec, state, num_per_status): # move past first two irregular epochs wrt finality next_epoch(spec, state) next_epoch(spec, state) # ready for entrance into activation queue - activation_queue_index = 0 - mock_deposit(spec, state, activation_queue_index) + activation_queue_start_index = 0 + activation_queue_indices = list(range(activation_queue_start_index, activation_queue_start_index + num_per_status)) + for validator_index in activation_queue_indices: + mock_deposit(spec, state, validator_index) # ready for activation - activation_index = 1 - mock_deposit(spec, state, activation_index) state.finalized_checkpoint.epoch = spec.get_current_epoch(state) - 1 - state.validators[activation_index].activation_eligibility_epoch = state.finalized_checkpoint.epoch + activation_start_index = num_per_status + activation_indices = list(range(activation_start_index, activation_start_index + num_per_status)) + for validator_index in activation_indices: + mock_deposit(spec, state, validator_index) + state.validators[validator_index].activation_eligibility_epoch = state.finalized_checkpoint.epoch # ready for ejection - ejection_index = 2 - state.validators[ejection_index].effective_balance = spec.config.EJECTION_BALANCE + ejection_start_index = num_per_status * 2 + ejection_indices = list(range(ejection_start_index, ejection_start_index + num_per_status)) + for validator_index in ejection_indices: + state.validators[validator_index].effective_balance = spec.config.EJECTION_BALANCE + churn_limit = spec.get_validator_churn_limit(state) yield from run_process_registry_updates(spec, state) - # validator moved into activation queue - validator = state.validators[activation_queue_index] - assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH - assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH - assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + # all eligible validators moved into activation queue + for validator_index in activation_queue_indices: + validator = state.validators[validator_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) - # validator activated for future epoch - validator = state.validators[activation_index] - assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH - assert validator.activation_epoch != spec.FAR_FUTURE_EPOCH - assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) - assert spec.is_active_validator( - validator, - spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) - ) + # up to churn limit validators get activated for future epoch from the queue + for validator_index in activation_indices[:churn_limit]: + validator = state.validators[validator_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch != spec.FAR_FUTURE_EPOCH + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + assert spec.is_active_validator( + validator, + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + ) - # validator ejected for future epoch - validator = state.validators[ejection_index] - assert validator.exit_epoch != spec.FAR_FUTURE_EPOCH - assert spec.is_active_validator(validator, spec.get_current_epoch(state)) - assert not spec.is_active_validator( - validator, - spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) - ) + # any remaining validators do not exit the activation queue + for validator_index in activation_indices[churn_limit:]: + validator = state.validators[validator_index] + assert validator.activation_eligibility_epoch != spec.FAR_FUTURE_EPOCH + assert validator.activation_epoch == spec.FAR_FUTURE_EPOCH + + # all ejection balance validators ejected for a future epoch + for i, validator_index in enumerate(ejection_indices): + validator = state.validators[validator_index] + assert validator.exit_epoch != spec.FAR_FUTURE_EPOCH + assert spec.is_active_validator(validator, spec.get_current_epoch(state)) + queue_offset = i // churn_limit + assert not spec.is_active_validator( + validator, + spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + queue_offset + ) + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection__1(spec, state): + yield from run_test_activation_queue_activation_and_ejection(spec, state, 1) + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection__churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit) + + +@with_all_phases +@spec_state_test +def test_activation_queue_activation_and_ejection__exceed_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit == spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit + 1) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_activation_queue_activation_and_ejection__scaled_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit) + + +@with_all_phases +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@spec_test +@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@single_phase +def test_activation_queue_activation_and_ejection__exceed_scaled_churn_limit(spec, state): + churn_limit = spec.get_validator_churn_limit(state) + assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT + yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit * 2) diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py index e336ebef7..1b977640d 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_slashings.py @@ -1,7 +1,11 @@ +from random import Random from eth2spec.test.context import spec_state_test, with_all_phases, is_post_altair from eth2spec.test.helpers.epoch_processing import ( run_epoch_processing_with, run_epoch_processing_to ) +from eth2spec.test.helpers.random import randomize_state +from eth2spec.test.helpers.state import has_active_balance_differential +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators from eth2spec.test.helpers.state import next_epoch @@ -22,6 +26,9 @@ def slash_validators(spec, state, indices, out_epochs): spec.get_current_epoch(state) % spec.EPOCHS_PER_SLASHINGS_VECTOR ] = total_slashed_balance + # verify some slashings happened... + assert total_slashed_balance != 0 + def get_slashing_multiplier(spec): if is_post_altair(spec): @@ -30,9 +37,7 @@ def get_slashing_multiplier(spec): return spec.PROPORTIONAL_SLASHING_MULTIPLIER -@with_all_phases -@spec_state_test -def test_max_penalties(spec, state): +def _setup_process_slashings_test(spec, state, not_slashable_set=set()): # Slashed count to ensure that enough validators are slashed to induce maximum penalties slashed_count = min( (len(state.validators) // get_slashing_multiplier(spec)) + 1, @@ -41,14 +46,23 @@ def test_max_penalties(spec, state): ) out_epoch = spec.get_current_epoch(state) + (spec.EPOCHS_PER_SLASHINGS_VECTOR // 2) - slashed_indices = list(range(slashed_count)) - slash_validators(spec, state, slashed_indices, [out_epoch] * slashed_count) + eligible_indices = set(range(slashed_count)) + slashed_indices = eligible_indices.difference(not_slashable_set) + slash_validators(spec, state, sorted(slashed_indices), [out_epoch] * slashed_count) total_balance = spec.get_total_active_balance(state) total_penalties = sum(state.slashings) assert total_balance // get_slashing_multiplier(spec) <= total_penalties + return slashed_indices + + +@with_all_phases +@spec_state_test +def test_max_penalties(spec, state): + slashed_indices = _setup_process_slashings_test(spec, state) + yield from run_process_slashings(spec, state) for i in slashed_indices: @@ -171,3 +185,28 @@ def test_scaled_penalties(spec, state): * spec.EFFECTIVE_BALANCE_INCREMENT ) assert state.balances[i] == pre_slash_balances[i] - expected_penalty + + +@with_all_phases +@spec_state_test +def test_slashings_with_random_state(spec, state): + rng = Random(9998) + randomize_state(spec, state, rng) + + pre_balances = state.balances.copy() + + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + + slashed_indices = _setup_process_slashings_test(spec, state, not_slashable_set=target_validators) + + # ensure no accidental slashings of protected set... + current_target_validators = get_unslashed_exited_validators(spec, state) + assert len(current_target_validators) != 0 + assert current_target_validators == target_validators + + yield from run_process_slashings(spec, state) + + for i in slashed_indices: + assert state.balances[i] < pre_balances[i] diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py index 634610fe7..c00a91b28 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py @@ -26,7 +26,6 @@ from eth2spec.test.helpers.state import ( next_epoch, next_slots, state_transition_and_sign_block, - transition_to, ) @@ -191,6 +190,10 @@ def test_on_block_before_finalized(spec, state): @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_on_block_finalized_skip_slots(spec, state): + """ + Test case was originally from https://github.com/ethereum/consensus-specs/pull/1579 + And then rewrote largely. + """ test_steps = [] # Initialization store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) @@ -200,21 +203,31 @@ def test_on_block_finalized_skip_slots(spec, state): on_tick_and_append_step(spec, store, current_time, test_steps) assert store.time == current_time - # Create a finalized chain - for _ in range(4): + # Fill epoch 0 and the first slot of epoch 1 + state, store, _ = yield from apply_next_slots_with_attestations( + spec, state, store, spec.SLOTS_PER_EPOCH, True, False, test_steps) + + # Skip the rest slots of epoch 1 and the first slot of epoch 2 + next_slots(spec, state, spec.SLOTS_PER_EPOCH) + + # The state after the skipped slots + target_state = state.copy() + + # Fill epoch 3 and 4 + for _ in range(2): state, store, _ = yield from apply_next_epoch_with_attestations( - spec, state, store, True, False, test_steps=test_steps) - assert store.finalized_checkpoint.epoch == 2 + spec, state, store, True, True, test_steps=test_steps) - # Another chain - another_state = store.block_states[store.finalized_checkpoint.root].copy() - # Build block that includes the skipped slots up to finality in chain - block = build_empty_block(spec, - another_state, - spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + 2) - block.body.graffiti = b'\x12' * 32 - signed_block = state_transition_and_sign_block(spec, another_state, block) + # Now we get finalized epoch 2, where `compute_start_slot_at_epoch(2)` is a skipped slot + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert store.finalized_checkpoint.root == spec.get_block_root(state, 1) == spec.get_block_root(state, 2) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert store.justified_checkpoint == state.current_justified_checkpoint + # Now build a block at later slot than finalized *epoch* + # Includes finalized block in chain and the skipped slots + block = build_empty_block_for_next_slot(spec, target_state) + signed_block = state_transition_and_sign_block(spec, target_state, block) yield from tick_and_add_block(spec, store, signed_block, test_steps) yield 'steps', test_steps @@ -224,36 +237,43 @@ def test_on_block_finalized_skip_slots(spec, state): @spec_state_test @with_presets([MINIMAL], reason="too slow") def test_on_block_finalized_skip_slots_not_in_skip_chain(spec, state): + """ + Test case was originally from https://github.com/ethereum/consensus-specs/pull/1579 + And then rewrote largely. + """ test_steps = [] # Initialization - transition_to(spec, state, state.slot + spec.SLOTS_PER_EPOCH - 1) - block = build_empty_block_for_next_slot(spec, state) - transition_unsigned_block(spec, state, block) - block.state_root = state.hash_tree_root() - store = spec.get_forkchoice_store(state, block) + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) yield 'anchor_state', state - yield 'anchor_block', block - + 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 - pre_finalized_checkpoint_epoch = store.finalized_checkpoint.epoch + # Fill epoch 0 and the first slot of epoch 1 + state, store, _ = yield from apply_next_slots_with_attestations( + spec, state, store, spec.SLOTS_PER_EPOCH, True, False, test_steps) - # Finalized - for _ in range(3): + # Skip the rest slots of epoch 1 and the first slot of epoch 2 + next_slots(spec, state, spec.SLOTS_PER_EPOCH) + + # Fill epoch 3 and 4 + for _ in range(2): state, store, _ = yield from apply_next_epoch_with_attestations( - spec, state, store, True, False, test_steps=test_steps) - assert store.finalized_checkpoint.epoch == pre_finalized_checkpoint_epoch + 1 + spec, state, store, True, True, test_steps=test_steps) - # Now build a block at later slot than finalized epoch - # Includes finalized block in chain, but not at appropriate skip slot - pre_state = store.block_states[block.hash_tree_root()].copy() - block = build_empty_block(spec, - state=pre_state, - slot=spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + 2) - block.body.graffiti = b'\x12' * 32 - signed_block = sign_block(spec, pre_state, block) + # Now we get finalized epoch 2, where `compute_start_slot_at_epoch(2)` is a skipped slot + assert state.finalized_checkpoint.epoch == store.finalized_checkpoint.epoch == 2 + assert store.finalized_checkpoint.root == spec.get_block_root(state, 1) == spec.get_block_root(state, 2) + assert state.current_justified_checkpoint.epoch == store.justified_checkpoint.epoch == 3 + assert store.justified_checkpoint == state.current_justified_checkpoint + + # Now build a block after the block of the finalized **root** + # Includes finalized block in chain, but does not include finalized skipped slots + another_state = store.block_states[store.finalized_checkpoint.root].copy() + assert another_state.slot == spec.compute_start_slot_at_epoch(store.finalized_checkpoint.epoch - 1) + block = build_empty_block_for_next_slot(spec, another_state) + signed_block = state_transition_and_sign_block(spec, another_state, block) yield from tick_and_add_block(spec, store, signed_block, test_steps, valid=False) yield 'steps', test_steps @@ -483,7 +503,8 @@ def test_new_justified_is_later_than_store_justified(spec, state): assert fork_2_state.finalized_checkpoint.epoch == 0 assert fork_2_state.current_justified_checkpoint.epoch == 5 # Check SAFE_SLOTS_TO_UPDATE_JUSTIFIED - spec.on_tick(store, store.genesis_time + fork_2_state.slot * spec.config.SECONDS_PER_SLOT) + time = store.genesis_time + fork_2_state.slot * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) assert spec.compute_slots_since_epoch_start(spec.get_current_slot(store)) >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED # Run on_block yield from add_block(spec, store, signed_block, test_steps) @@ -526,7 +547,8 @@ def test_new_justified_is_later_than_store_justified(spec, state): # # Apply blocks of `fork_3_state` to `store` # for block in all_blocks: # if store.time < spec.compute_time_at_slot(fork_2_state, block.message.slot): - # spec.on_tick(store, store.genesis_time + block.message.slot * spec.config.SECONDS_PER_SLOT) + # time = store.genesis_time + block.message.slot * spec.config.SECONDS_PER_SLOT + # on_tick_and_append_step(spec, store, time, test_steps) # # valid_attestations=False because the attestations are outdated (older than previous epoch) # yield from add_block(spec, store, block, test_steps, allow_invalid_attestations=False) @@ -643,7 +665,6 @@ def test_new_finalized_slot_is_justified_checkpoint_ancestor(spec, state): # Process state next_epoch(spec, state) - spec.on_tick(store, store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT) state, store, _ = yield from apply_next_epoch_with_attestations( spec, state, store, False, True, test_steps=test_steps) diff --git a/tests/core/pyspec/eth2spec/test/phase0/random/__init__.py b/tests/core/pyspec/eth2spec/test/phase0/random/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/phase0/random/test_random.py b/tests/core/pyspec/eth2spec/test/phase0/random/test_random.py new file mode 100644 index 000000000..89a457bed --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/random/test_random.py @@ -0,0 +1,438 @@ +""" +This module is generated from the ``random`` test generator. +Please do not edit this file manually. +See the README for that generator for more information. +""" + +from eth2spec.test.helpers.constants import PHASE0 +from eth2spec.test.context import ( + misc_balances_in_default_range_with_many_validators, + with_phases, + zero_activation_threshold, + only_generator, +) +from eth2spec.test.context import ( + always_bls, + spec_test, + with_custom_state, + single_phase, +) +from eth2spec.test.utils.randomized_block_tests import ( + run_generated_randomized_test, +) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_0(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_1(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_2(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_3(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_4(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_5(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_6(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_7(spec, state): + # scenario as high-level, informal text: + # epochs:0,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'validation': 'validate_is_not_leaking', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_8(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_9(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_10(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_11(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_12(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:last_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'last_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_13(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:random_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'random_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_14(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:penultimate_slot_in_epoch,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 0, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 'penultimate_slot_in_epoch', 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) + + +@only_generator("randomized test for broad coverage, not point-to-point CI") +@with_phases([PHASE0]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_15(spec, state): + # scenario as high-level, informal text: + # epochs:epochs_until_leak,slots:0,with-block:no_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + # epochs:1,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:no_block + # epochs:0,slots:0,with-block:random_block + scenario = {'transitions': [{'epochs_to_skip': 'epochs_until_leak', 'validation': 'validate_is_leaking', 'slots_to_skip': 0, 'block_producer': 'no_block'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}, {'epochs_to_skip': 1, 'slots_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'slots_to_skip': 0, 'epochs_to_skip': 0, 'block_producer': 'no_block', 'validation': 'no_op_validation'}, {'block_producer': 'random_block', 'epochs_to_skip': 0, 'slots_to_skip': 0, 'validation': 'no_op_validation'}], 'state_randomizer': 'randomize_state'} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + ) diff --git a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py index 1184a6617..e6112557a 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py +++ b/tests/core/pyspec/eth2spec/test/phase0/rewards/test_random.py @@ -9,6 +9,13 @@ from eth2spec.test.context import ( low_balances, misc_balances, ) import eth2spec.test.helpers.rewards as rewards_helpers +from eth2spec.test.helpers.random import ( + randomize_state, + patch_state_to_non_leaking, + randomize_attestation_participation, +) +from eth2spec.test.helpers.state import has_active_balance_differential, next_epoch +from eth2spec.test.helpers.voluntary_exits import get_unslashed_exited_validators @with_all_phases @@ -35,6 +42,21 @@ def test_full_random_3(spec, state): yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(4040)) +@with_all_phases +@spec_state_test +def test_full_random_4(spec, state): + """ + Ensure a rewards test with some exited (but not slashed) validators. + """ + rng = Random(5050) + randomize_state(spec, state, rng) + assert spec.is_in_inactivity_leak(state) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + yield from rewards_helpers.run_deltas(spec, state) + + @with_all_phases @with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) @spec_test @@ -57,3 +79,52 @@ def test_full_random_low_balances_1(spec, state): @single_phase def test_full_random_misc_balances(spec, state): yield from rewards_helpers.run_test_full_random(spec, state, rng=Random(7070)) + + +@with_all_phases +@spec_state_test +def test_full_random_without_leak_0(spec, state): + rng = Random(1010) + randomize_state(spec, state, rng) + assert spec.is_in_inactivity_leak(state) + patch_state_to_non_leaking(spec, state) + assert not spec.is_in_inactivity_leak(state) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + assert has_active_balance_differential(spec, state) + yield from rewards_helpers.run_deltas(spec, state) + + +@with_all_phases +@spec_state_test +def test_full_random_without_leak_and_current_exit_0(spec, state): + """ + This test specifically ensures a validator exits in the current epoch + to ensure rewards are handled properly in this case. + """ + rng = Random(1011) + randomize_state(spec, state, rng) + assert spec.is_in_inactivity_leak(state) + patch_state_to_non_leaking(spec, state) + assert not spec.is_in_inactivity_leak(state) + target_validators = get_unslashed_exited_validators(spec, state) + assert len(target_validators) != 0 + + # move forward some epochs to process attestations added + # by ``randomize_state`` before we exit validators in + # what will be the current epoch + for _ in range(2): + next_epoch(spec, state) + + current_epoch = spec.get_current_epoch(state) + for index in target_validators: + # patch exited validators to exit in the current epoch + validator = state.validators[index] + validator.exit_epoch = current_epoch + validator.withdrawable_epoch = current_epoch + spec.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY + + # re-randomize attestation participation for the current epoch + randomize_attestation_participation(spec, state, rng) + + assert has_active_balance_differential(spec, state) + yield from rewards_helpers.run_deltas(spec, state) diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py index a79ed94d2..6866a86ae 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py @@ -19,6 +19,7 @@ from eth2spec.test.helpers.attester_slashings import ( from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect from eth2spec.test.helpers.attestations import get_valid_attestation from eth2spec.test.helpers.deposits import prepare_state_and_deposit +from eth2spec.test.helpers.execution_payload import build_empty_execution_payload from eth2spec.test.helpers.voluntary_exits import prepare_signed_exits from eth2spec.test.helpers.multi_operations import ( run_slash_and_exit, @@ -38,6 +39,7 @@ from eth2spec.test.context import ( with_custom_state, large_validator_set, is_post_altair, + is_post_merge, ) @@ -146,6 +148,11 @@ def process_and_sign_block_without_header_validations(spec, state, block): spec.process_randao(state, block.body) spec.process_eth1_data(state, block.body) spec.process_operations(state, block.body) + if is_post_altair(spec): + spec.process_sync_aggregate(state, block.body.sync_aggregate) + if is_post_merge(spec): + if spec.is_execution_enabled(state, block.body): + spec.process_execution_payload(state, block.body.execution_payload, spec.EXECUTION_ENGINE) # Insert post-state rot block.state_root = state.hash_tree_root() @@ -188,6 +195,10 @@ def test_parent_from_same_slot(spec, state): child_block = parent_block.copy() child_block.parent_root = state.latest_block_header.hash_tree_root() + if is_post_merge(spec): + randao_mix = spec.compute_randao_mix(state, child_block.body.randao_reveal) + child_block.body.execution_payload = build_empty_execution_payload(spec, state, randao_mix) + # Show that normal path through transition fails failed_state = state.copy() expect_assertion_error( @@ -974,12 +985,9 @@ def test_historical_batch(spec, state): @with_all_phases +@with_presets([MINIMAL], reason="suffices to test eth1 data voting without long voting period") @spec_state_test def test_eth1_data_votes_consensus(spec, state): - if spec.EPOCHS_PER_ETH1_VOTING_PERIOD > 2: - return dump_skipping_message("Skip test if config with longer `EPOCHS_PER_ETH1_VOTING_PERIOD` for saving time." - " Minimal config suffice to cover the target-of-test.") - voting_period_slots = spec.EPOCHS_PER_ETH1_VOTING_PERIOD * spec.SLOTS_PER_EPOCH offset_block = build_empty_block(spec, state, slot=voting_period_slots - 1) @@ -1018,12 +1026,9 @@ def test_eth1_data_votes_consensus(spec, state): @with_all_phases +@with_presets([MINIMAL], reason="suffices to test eth1 data voting without long voting period") @spec_state_test def test_eth1_data_votes_no_consensus(spec, state): - if spec.EPOCHS_PER_ETH1_VOTING_PERIOD > 2: - return dump_skipping_message("Skip test if config with longer `EPOCHS_PER_ETH1_VOTING_PERIOD` for saving time." - " Minimal config suffice to cover the target-of-test.") - voting_period_slots = spec.EPOCHS_PER_ETH1_VOTING_PERIOD * spec.SLOTS_PER_EPOCH pre_eth1_hash = state.eth1_data.block_hash diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py index ef643fd6b..0d9f6ddf5 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py +++ b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_tick.py @@ -1,5 +1,13 @@ from eth2spec.test.context import with_all_phases, spec_state_test from eth2spec.test.helpers.fork_choice import get_genesis_forkchoice_store +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.state import ( + next_epoch, + state_transition_and_sign_block, + transition_to, +) def run_on_tick(spec, store, time, new_justified_checkpoint=False): @@ -26,18 +34,92 @@ def test_basic(spec, state): @with_all_phases @spec_state_test -def test_update_justified_single(spec, state): +def test_update_justified_single_on_store_finalized_chain(spec, state): store = get_genesis_forkchoice_store(spec, state) - next_epoch = spec.get_current_epoch(state) + 1 - next_epoch_start_slot = spec.compute_start_slot_at_epoch(next_epoch) - seconds_until_next_epoch = next_epoch_start_slot * spec.config.SECONDS_PER_SLOT - store.time - store.best_justified_checkpoint = spec.Checkpoint( - epoch=store.justified_checkpoint.epoch + 1, - root=b'\x55' * 32, + # [Mock store.best_justified_checkpoint] + # Create a block at epoch 1 + next_epoch(spec, state) + block = build_empty_block_for_next_slot(spec, state) + state_transition_and_sign_block(spec, state, block) + store.blocks[block.hash_tree_root()] = block.copy() + store.block_states[block.hash_tree_root()] = state.copy() + parent_block = block.copy() + # To make compute_slots_since_epoch_start(current_slot) == 0, transition to the end of the epoch + slot = state.slot + spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, slot) + # Create a block at the start of epoch 2 + block = build_empty_block_for_next_slot(spec, state) + # Mock state + state.current_justified_checkpoint = spec.Checkpoint( + epoch=spec.compute_epoch_at_slot(parent_block.slot), + root=parent_block.hash_tree_root(), + ) + state_transition_and_sign_block(spec, state, block) + store.blocks[block.hash_tree_root()] = block + store.block_states[block.hash_tree_root()] = state + # Mock store.best_justified_checkpoint + store.best_justified_checkpoint = state.current_justified_checkpoint.copy() + + run_on_tick( + spec, + store, + store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, + new_justified_checkpoint=True ) - run_on_tick(spec, store, store.time + seconds_until_next_epoch, True) + +@with_all_phases +@spec_state_test +def test_update_justified_single_not_on_store_finalized_chain(spec, state): + store = get_genesis_forkchoice_store(spec, state) + init_state = state.copy() + + # Chain grows + # Create a block at epoch 1 + next_epoch(spec, state) + block = build_empty_block_for_next_slot(spec, state) + block.body.graffiti = b'\x11' * 32 + state_transition_and_sign_block(spec, state, block) + store.blocks[block.hash_tree_root()] = block.copy() + store.block_states[block.hash_tree_root()] = state.copy() + # Mock store.finalized_checkpoint + store.finalized_checkpoint = spec.Checkpoint( + epoch=spec.compute_epoch_at_slot(block.slot), + root=block.hash_tree_root(), + ) + + # [Mock store.best_justified_checkpoint] + # Create a block at epoch 1 + state = init_state.copy() + next_epoch(spec, state) + block = build_empty_block_for_next_slot(spec, state) + block.body.graffiti = b'\x22' * 32 + state_transition_and_sign_block(spec, state, block) + store.blocks[block.hash_tree_root()] = block.copy() + store.block_states[block.hash_tree_root()] = state.copy() + parent_block = block.copy() + # To make compute_slots_since_epoch_start(current_slot) == 0, transition to the end of the epoch + slot = state.slot + spec.SLOTS_PER_EPOCH - state.slot % spec.SLOTS_PER_EPOCH - 1 + transition_to(spec, state, slot) + # Create a block at the start of epoch 2 + block = build_empty_block_for_next_slot(spec, state) + # Mock state + state.current_justified_checkpoint = spec.Checkpoint( + epoch=spec.compute_epoch_at_slot(parent_block.slot), + root=parent_block.hash_tree_root(), + ) + state_transition_and_sign_block(spec, state, block) + store.blocks[block.hash_tree_root()] = block.copy() + store.block_states[block.hash_tree_root()] = state.copy() + # Mock store.best_justified_checkpoint + store.best_justified_checkpoint = state.current_justified_checkpoint.copy() + + run_on_tick( + spec, + store, + store.genesis_time + state.slot * spec.config.SECONDS_PER_SLOT, + ) @with_all_phases diff --git a/tests/core/pyspec/eth2spec/test/utils/__init__.py b/tests/core/pyspec/eth2spec/test/utils/__init__.py new file mode 100644 index 000000000..f6b2a8a44 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/utils/__init__.py @@ -0,0 +1,12 @@ +from .utils import ( + vector_test, + with_meta_tags, + build_transition_test, +) + + +__all__ = [ # avoid "unused import" lint error + "vector_test", + "with_meta_tags", + "build_transition_test", +] diff --git a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py new file mode 100644 index 000000000..02a5464f7 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py @@ -0,0 +1,361 @@ +""" +Utility code to generate randomized block tests +""" + +import sys +import warnings +from random import Random +from typing import Callable + +from eth2spec.test.helpers.multi_operations import ( + build_random_block_from_state_for_next_slot, + get_random_sync_aggregate, + prepare_state_and_get_random_deposits, +) +from eth2spec.test.helpers.inactivity_scores import ( + randomize_inactivity_scores, +) +from eth2spec.test.helpers.random import ( + randomize_state as randomize_state_helper, + patch_state_to_non_leaking, +) +from eth2spec.test.helpers.state import ( + next_slot, + next_epoch, + ensure_state_has_validators_across_lifecycle, + state_transition_and_sign_block, +) + +# primitives: +# state + + +def _randomize_deposit_state(spec, state, stats): + """ + To introduce valid, randomized deposits, the ``state`` deposit sub-state + must be coordinated with the data that will ultimately go into blocks. + + This function randomizes the ``state`` in a way that can signal downstream to + the block constructors how they should (or should not) make some randomized deposits. + """ + rng = Random(999) + block_count = stats.get("block_count", 0) + deposits = [] + if block_count > 0: + num_deposits = rng.randrange(1, block_count * spec.MAX_DEPOSITS) + deposits = prepare_state_and_get_random_deposits(spec, state, rng, num_deposits=num_deposits) + return { + "deposits": deposits, + } + + +def randomize_state(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1): + randomize_state_helper(spec, state, exit_fraction=exit_fraction, slash_fraction=slash_fraction) + scenario_state = _randomize_deposit_state(spec, state, stats) + return scenario_state + + +def randomize_state_altair(spec, state, stats): + scenario_state = randomize_state(spec, state, stats, exit_fraction=0.1, slash_fraction=0.1) + randomize_inactivity_scores(spec, state) + return scenario_state + + +# epochs + +def epochs_until_leak(spec): + """ + State is "leaking" if the current epoch is at least + this value after the last finalized epoch. + """ + return spec.MIN_EPOCHS_TO_INACTIVITY_PENALTY + 1 + + +def epochs_for_shard_committee_period(spec): + return spec.config.SHARD_COMMITTEE_PERIOD + + +# slots + +def last_slot_in_epoch(spec): + return spec.SLOTS_PER_EPOCH - 1 + + +def random_slot_in_epoch(spec, rng=Random(1336)): + return rng.randrange(1, spec.SLOTS_PER_EPOCH - 2) + + +def penultimate_slot_in_epoch(spec): + return spec.SLOTS_PER_EPOCH - 2 + + +# blocks + +def no_block(_spec, _pre_state, _signed_blocks, _scenario_state): + return None + + +# May need to make several attempts to find a block that does not correspond to a slashed +# proposer with the randomization helpers... +BLOCK_ATTEMPTS = 32 + + +def _warn_if_empty_operations(block): + """ + NOTE: a block may be missing deposits depending on how many were created + and already inserted into existing blocks in a given scenario. + """ + if len(block.body.proposer_slashings) == 0: + warnings.warn(f"proposer slashings missing in block at slot {block.slot}") + + if len(block.body.attester_slashings) == 0: + warnings.warn(f"attester slashings missing in block at slot {block.slot}") + + if len(block.body.attestations) == 0: + warnings.warn(f"attestations missing in block at slot {block.slot}") + + if len(block.body.voluntary_exits) == 0: + warnings.warn(f"voluntary exits missing in block at slot {block.slot}") + + +def _pull_deposits_from_scenario_state(spec, scenario_state, existing_block_count): + all_deposits = scenario_state.get("deposits", []) + start = existing_block_count * spec.MAX_DEPOSITS + return all_deposits[start:start + spec.MAX_DEPOSITS] + + +def random_block(spec, state, signed_blocks, scenario_state): + """ + Produce a random block. + NOTE: this helper may mutate state, as it will attempt + to produce a block over ``BLOCK_ATTEMPTS`` slots in order + to find a valid block in the event that the proposer has already been slashed. + """ + # NOTE: ``state`` has been "randomized" at this point and so will likely + # contain a large number of slashed validators. This function needs to return + # a valid block so it needs to check that the proposer of the next slot is not + # slashed. + # To do this, generate a ``temp_state`` to use for checking the propser in the next slot. + # This ensures no accidental mutations happen to the ``state`` the caller expects to get back + # after this function returns. + # Using a copy of the state for proposer sampling is also sound as any inputs used for the + # shuffling are fixed a few epochs prior to ``spec.get_current_epoch(state)``. + temp_state = state.copy() + next_slot(spec, temp_state) + for _ in range(BLOCK_ATTEMPTS): + proposer_index = spec.get_beacon_proposer_index(temp_state) + proposer = state.validators[proposer_index] + if proposer.slashed: + next_slot(spec, state) + next_slot(spec, temp_state) + else: + deposits_for_block = _pull_deposits_from_scenario_state(spec, scenario_state, len(signed_blocks)) + block = build_random_block_from_state_for_next_slot(spec, state, deposits=deposits_for_block) + _warn_if_empty_operations(block) + return block + else: + raise AssertionError("could not find a block with an unslashed proposer, check ``state`` input") + + +SYNC_AGGREGATE_PARTICIPATION_BUCKETS = 4 + + +def random_block_altair_with_cycling_sync_committee_participation(spec, + state, + signed_blocks, + scenario_state): + block = random_block(spec, state, signed_blocks, scenario_state) + block_index = len(signed_blocks) % SYNC_AGGREGATE_PARTICIPATION_BUCKETS + fraction_missed = block_index * (1 / SYNC_AGGREGATE_PARTICIPATION_BUCKETS) + fraction_participated = 1.0 - fraction_missed + previous_root = block.parent_root + block.body.sync_aggregate = get_random_sync_aggregate( + spec, + state, + block.slot - 1, + block_root=previous_root, + fraction_participated=fraction_participated, + ) + return block + + +# validations + +def no_op_validation(_spec, _state): + return True + + +def validate_is_leaking(spec, state): + return spec.is_in_inactivity_leak(state) + + +def validate_is_not_leaking(spec, state): + return not validate_is_leaking(spec, state) + + +# transitions + +def with_validation(transition, validation): + if isinstance(transition, Callable): + transition = transition() + transition["validation"] = validation + return transition + + +def no_op_transition(): + return {} + + +def epoch_transition(n=0): + return { + "epochs_to_skip": n, + } + + +def slot_transition(n=0): + return { + "slots_to_skip": n, + } + + +def transition_to_leaking(): + return { + "epochs_to_skip": epochs_until_leak, + "validation": validate_is_leaking, + } + + +transition_without_leak = with_validation(no_op_transition, validate_is_not_leaking) + +# block transitions + + +def transition_with_random_block(block_randomizer): + """ + Build a block transition with randomized data. + Provide optional sub-transitions to advance some + number of epochs or slots before applying the random block. + """ + return { + "block_producer": block_randomizer, + } + + +# setup and test gen + + +def _randomized_scenario_setup(state_randomizer): + """ + Return a sequence of pairs of ("mutation", "validation"). + A "mutation" is a function that accepts (``spec``, ``state``, ``stats``) arguments and + allegedly performs some change to the state. + A "validation" is a function that accepts (spec, state) arguments and validates some change was made. + + The "mutation" may return some state that should be available to any down-stream transitions + across the **entire** scenario. + + The ``stats`` parameter reflects a summary of actions in a given scenario like + how many blocks will be produced. This data can be useful to construct a valid + pre-state and so is provided at the setup stage. + """ + def _skip_epochs(epoch_producer): + def f(spec, state, _stats): + """ + The unoptimized spec implementation is too slow to advance via ``next_epoch``. + Instead, just overwrite the ``state.slot`` and continue... + """ + epochs_to_skip = epoch_producer(spec) + slots_to_skip = epochs_to_skip * spec.SLOTS_PER_EPOCH + state.slot += slots_to_skip + return f + + def _simulate_honest_execution(spec, state, _stats): + """ + Want to start tests not in a leak state; the finality data + may not reflect this condition with prior (arbitrary) mutations, + so this mutator addresses that fact. + """ + patch_state_to_non_leaking(spec, state) + + return ( + # NOTE: the block randomization function assumes at least 1 shard committee period + # so advance the state before doing anything else. + (_skip_epochs(epochs_for_shard_committee_period), no_op_validation), + (_simulate_honest_execution, no_op_validation), + (state_randomizer, ensure_state_has_validators_across_lifecycle), + ) + +# Run the generated tests: + + +# while the test implementation works via code-gen, +# references to helper code in this module are serialized as str names. +# to resolve this references at runtime, we need a reference to this module: +_this_module = sys.modules[__name__] + + +def _resolve_ref(ref): + if isinstance(ref, str): + return getattr(_this_module, ref) + return ref + + +def _iter_temporal(spec, description): + """ + Intended to advance some number of {epochs, slots}. + Caller can provide a constant integer or a callable deriving a number from + the ``spec`` under consideration. + """ + numeric = _resolve_ref(description) + if isinstance(numeric, Callable): + numeric = numeric(spec) + for i in range(numeric): + yield i + + +def _compute_statistics(scenario): + block_count = 0 + for transition in scenario["transitions"]: + block_producer = _resolve_ref(transition.get("block_producer", None)) + if block_producer and block_producer != no_block: + block_count += 1 + return { + "block_count": block_count, + } + + +def run_generated_randomized_test(spec, state, scenario): + stats = _compute_statistics(scenario) + if "setup" not in scenario: + state_randomizer = _resolve_ref(scenario.get("state_randomizer", randomize_state)) + scenario["setup"] = _randomized_scenario_setup(state_randomizer) + + scenario_state = {} + for mutation, validation in scenario["setup"]: + additional_state = mutation(spec, state, stats) + validation(spec, state) + if additional_state: + scenario_state.update(additional_state) + + yield "pre", state + + blocks = [] + for transition in scenario["transitions"]: + epochs_to_skip = _iter_temporal(spec, transition["epochs_to_skip"]) + for _ in epochs_to_skip: + next_epoch(spec, state) + slots_to_skip = _iter_temporal(spec, transition["slots_to_skip"]) + for _ in slots_to_skip: + next_slot(spec, state) + + block_producer = _resolve_ref(transition["block_producer"]) + block = block_producer(spec, state, blocks, scenario_state) + if block: + signed_block = state_transition_and_sign_block(spec, state, block) + blocks.append(signed_block) + + validation = _resolve_ref(transition["validation"]) + assert validation(spec, state) + + yield "blocks", blocks + yield "post", state diff --git a/tests/core/pyspec/eth2spec/test/utils.py b/tests/core/pyspec/eth2spec/test/utils/utils.py similarity index 100% rename from tests/core/pyspec/eth2spec/test/utils.py rename to tests/core/pyspec/eth2spec/test/utils/utils.py diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 90c0aafc7..cfc86776d 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -80,26 +80,34 @@ checks: {: value} -- the assertions. `` is the field member or property of [`Store`](../../../specs/phase0/fork-choice.md#store) object that maintained by client implementation. Currently, the possible fields included: ```yaml -head: { -- Encoded 32-byte value from get_head(store) - slot: slot, - root: string, +head: { + slot: int, + root: string, -- Encoded 32-byte value from get_head(store) +} +time: int -- store.time +genesis_time: int -- store.genesis_time +justified_checkpoint: { + epoch: int, -- Integer value from store.justified_checkpoint.epoch + root: string, -- Encoded 32-byte value from store.justified_checkpoint.root +} +finalized_checkpoint: { + epoch: int, -- Integer value from store.finalized_checkpoint.epoch + root: string, -- Encoded 32-byte value from store.finalized_checkpoint.root +} +best_justified_checkpoint: { + epoch: int, -- Integer value from store.best_justified_checkpoint.epoch + root: string, -- Encoded 32-byte value from store.best_justified_checkpoint.root } -time: int -- store.time -genesis_time: int -- store.genesis_time -justified_checkpoint_root: string -- Encoded 32-byte value from store.justified_checkpoint.root -finalized_checkpoint_root: string -- Encoded 32-byte value from store.finalized_checkpoint.root -best_justified_checkpoint_root: string -- Encoded 32-byte value from store.best_justified_checkpoint.root ``` For example: ```yaml - checks: - time: 144 - genesis_time: 0 - head: {slot: 17, root: '0xd2724c86002f7e1f8656ab44a341a409ad80e6e70a5225fd94835566deebb66f'} - justified_checkpoint_root: '0xcea6ecd3d3188e32ebf611f960eebd45b6c6f477a7cff242fa567a42653bfc7c' - finalized_checkpoint_root: '0xcea6ecd3d3188e32ebf611f960eebd45b6c6f477a7cff242fa567a42653bfc7c' - best_justified_checkpoint: '0xcea6ecd3d3188e32ebf611f960eebd45b6c6f477a7cff242fa567a42653bfc7c' + time: 192 + head: {slot: 32, root: '0xdaa1d49d57594ced0c35688a6da133abb086d191a2ebdfd736fad95299325aeb'} + justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'} + finalized_checkpoint: {epoch: 2, root: '0x40d32d6283ec11c53317a46808bc88f55657d93b95a1af920403187accf48f4f'} + best_justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'} ``` *Note*: Each `checks` step may include one or multiple items. Each item has to be checked against the current store. diff --git a/tests/generators/epoch_processing/main.py b/tests/generators/epoch_processing/main.py index 7203bac93..4c1a68d0a 100644 --- a/tests/generators/epoch_processing/main.py +++ b/tests/generators/epoch_processing/main.py @@ -25,8 +25,7 @@ if __name__ == "__main__": } # also run the previous phase 0 tests # No epoch-processing changes in Merge and previous testing repeats with new types, so no additional tests required. - # TODO: rebase onto Altair testing later. - merge_mods = phase_0_mods + merge_mods = altair_mods # TODO Custody Game testgen is disabled for now # custody_game_mods = {**{key: 'eth2spec.test.custody_game.epoch_processing.test_process_' + key for key in [ diff --git a/tests/generators/finality/main.py b/tests/generators/finality/main.py index b4c8da39a..dbc58a809 100644 --- a/tests/generators/finality/main.py +++ b/tests/generators/finality/main.py @@ -5,7 +5,7 @@ from eth2spec.test.helpers.constants import PHASE0, ALTAIR, MERGE if __name__ == "__main__": phase_0_mods = {'finality': 'eth2spec.test.phase0.finality.test_finality'} altair_mods = phase_0_mods # No additional Altair specific finality tests - merge_mods = phase_0_mods # No additional Merge specific finality tests + merge_mods = altair_mods # No additional Merge specific finality tests all_mods = { PHASE0: phase_0_mods, diff --git a/tests/generators/fork_choice/main.py b/tests/generators/fork_choice/main.py index 0ebdbf2c0..148174120 100644 --- a/tests/generators/fork_choice/main.py +++ b/tests/generators/fork_choice/main.py @@ -9,8 +9,8 @@ if __name__ == "__main__": ]} # No additional Altair specific finality tests, yet. altair_mods = phase_0_mods - # No specific Merge tests yet. TODO: rebase onto Altair testing later. - merge_mods = phase_0_mods + # No specific Merge tests yet. + merge_mods = altair_mods all_mods = { PHASE0: phase_0_mods, diff --git a/tests/generators/operations/main.py b/tests/generators/operations/main.py index d2653d87d..3844ce186 100644 --- a/tests/generators/operations/main.py +++ b/tests/generators/operations/main.py @@ -12,9 +12,9 @@ if __name__ == "__main__": 'voluntary_exit', ]} altair_mods = { - **{key: 'eth2spec.test.altair.block_processing.sync_aggregate.test_process_' + key for key in [ - 'sync_aggregate', - 'sync_aggregate_random', + **{'sync_aggregate': [ + 'eth2spec.test.altair.block_processing.sync_aggregate.test_process_' + key + for key in ['sync_aggregate', 'sync_aggregate_random'] ]}, **phase_0_mods, } # also run the previous phase 0 tests @@ -23,7 +23,7 @@ if __name__ == "__main__": **{key: 'eth2spec.test.merge.block_processing.test_process_' + key for key in [ 'execution_payload', ]}, - **phase_0_mods, # TODO: runs phase0 tests. Rebase to include `altair_mods` testing later. + **altair_mods, } # TODO Custody Game testgen is disabled for now diff --git a/tests/generators/random/Makefile b/tests/generators/random/Makefile new file mode 100644 index 000000000..1b518bfde --- /dev/null +++ b/tests/generators/random/Makefile @@ -0,0 +1,8 @@ +all: + if ! test -d venv; then python3 -m venv venv; fi; + . ./venv/bin/activate + pip3 install -r requirements.txt + rm -f ../../core/pyspec/eth2spec/test/phase0/random/test_random.py + rm -f ../../core/pyspec/eth2spec/test/altair/random/test_random.py + python3 generate.py phase0 > ../../core/pyspec/eth2spec/test/phase0/random/test_random.py + python3 generate.py altair > ../../core/pyspec/eth2spec/test/altair/random/test_random.py diff --git a/tests/generators/random/README.md b/tests/generators/random/README.md new file mode 100644 index 000000000..fd1728441 --- /dev/null +++ b/tests/generators/random/README.md @@ -0,0 +1,31 @@ +# Randomized tests + +Randomized tests in the format of `sanity` blocks tests, with randomized operations. + +Information on the format of the tests can be found in the [sanity test formats documentation](../../formats/sanity/README.md). + +# To generate test sources + +```bash +$ make +``` + +The necessary commands are in the `Makefile`, as the only target. + +The generated files are committed to the repo so you should not need to do this. + +# To run tests + +Each of the generated test does produce a `pytest` test instance but by default is +currently skipped. Running the test via the generator (see next) will trigger any errors +that would arise during the running of `pytest`. + +# To generate spec tests (from the generated files) + +Run the test generator in the usual way. + +E.g. from the root of this repo, you can run: + +```bash +$ make gen_random +``` diff --git a/tests/generators/random/generate.py b/tests/generators/random/generate.py new file mode 100644 index 000000000..b880cb12b --- /dev/null +++ b/tests/generators/random/generate.py @@ -0,0 +1,258 @@ +""" +This test format currently uses code generation to assemble the tests +as the current test infra does not have a facility to dynamically +generate tests that can be seen by ``pytest``. + +This will likley change in future releases of the testing infra. + +NOTE: To add additional scenarios, add test cases below in ``_generate_randomized_scenarios``. +""" + +import sys +import random +import warnings +from typing import Callable +import itertools + +from eth2spec.test.utils.randomized_block_tests import ( + no_block, + no_op_validation, + randomize_state, + randomize_state_altair, + random_block, + random_block_altair_with_cycling_sync_committee_participation, + last_slot_in_epoch, + random_slot_in_epoch, + penultimate_slot_in_epoch, + epoch_transition, + slot_transition, + transition_with_random_block, + transition_to_leaking, + transition_without_leak, +) +from eth2spec.test.helpers.constants import PHASE0, ALTAIR + + +# Ensure this many blocks are present in *each* randomized scenario +BLOCK_TRANSITIONS_COUNT = 2 + + +def _normalize_transition(transition): + """ + Provide "empty" or "no op" sub-transitions + to a given transition. + """ + if isinstance(transition, Callable): + transition = transition() + if "epochs_to_skip" not in transition: + transition["epochs_to_skip"] = 0 + if "slots_to_skip" not in transition: + transition["slots_to_skip"] = 0 + if "block_producer" not in transition: + transition["block_producer"] = no_block + if "validation" not in transition: + transition["validation"] = no_op_validation + return transition + + +def _normalize_scenarios(scenarios): + """ + "Normalize" a "scenario" so that a producer of a test case + does not need to provide every expected key/value. + """ + for scenario in scenarios: + transitions = scenario["transitions"] + for i, transition in enumerate(transitions): + transitions[i] = _normalize_transition(transition) + + +def _flatten(t): + leak_transition = t[0] + result = [leak_transition] + for transition_batch in t[1]: + for transition in transition_batch: + if isinstance(transition, tuple): + for subtransition in transition: + result.append(subtransition) + else: + result.append(transition) + return result + + +def _generate_randomized_scenarios(block_randomizer): + """ + Generates a set of randomized testing scenarios. + Return a sequence of "scenarios" where each scenario: + 1. Provides some setup + 2. Provides a sequence of transitions that mutate the state in some way, + possibly yielding blocks along the way + NOTE: scenarios are "normalized" with empty/no-op elements before returning + to the test generation to facilitate brevity when writing scenarios by hand. + NOTE: the main block driver builds a block for the **next** slot, so + the slot transitions are offset by -1 to target certain boundaries. + """ + # go forward 0 or 1 epochs + epochs_set = ( + epoch_transition(n=0), + epoch_transition(n=1), + ) + # within those epochs, go forward to: + slots_set = ( + # the first slot in an epoch (see note in docstring about offsets...) + slot_transition(last_slot_in_epoch), + # the second slot in an epoch + slot_transition(n=0), + # some random number of slots, but not at epoch boundaries + slot_transition(random_slot_in_epoch), + # the last slot in an epoch (see note in docstring about offsets...) + slot_transition(penultimate_slot_in_epoch), + ) + # and produce a block... + blocks_set = ( + transition_with_random_block(block_randomizer), + ) + + rng = random.Random(1447) + all_skips = list(itertools.product(epochs_set, slots_set)) + randomized_skips = ( + rng.sample(all_skips, len(all_skips)) + for _ in range(BLOCK_TRANSITIONS_COUNT) + ) + + # build a set of block transitions from combinations of sub-transitions + transitions_generator = ( + itertools.product(prefix, blocks_set) + for prefix in randomized_skips + ) + block_transitions = zip(*transitions_generator) + + # and preface each set of block transitions with the possible leak transitions + leak_transitions = ( + transition_without_leak, + transition_to_leaking, + ) + scenarios = [ + {"transitions": _flatten(t)} + for t in itertools.product(leak_transitions, block_transitions) + ] + _normalize_scenarios(scenarios) + return scenarios + + +def _id_from_scenario(test_description): + """ + Construct a name for the scenario based its data. + """ + def _to_id_part(prefix, x): + suffix = str(x) + if isinstance(x, Callable): + suffix = x.__name__ + return f"{prefix}{suffix}" + + def _id_from_transition(transition): + return ",".join(( + _to_id_part("epochs:", transition["epochs_to_skip"]), + _to_id_part("slots:", transition["slots_to_skip"]), + _to_id_part("with-block:", transition["block_producer"]) + )) + + return "|".join(map(_id_from_transition, test_description["transitions"])) + + +test_imports_template = """\"\"\" +This module is generated from the ``random`` test generator. +Please do not edit this file manually. +See the README for that generator for more information. +\"\"\" + +from eth2spec.test.helpers.constants import {phase} +from eth2spec.test.context import ( + misc_balances_in_default_range_with_many_validators, + with_phases, + zero_activation_threshold, + only_generator, +) +from eth2spec.test.context import ( + always_bls, + spec_test, + with_custom_state, + single_phase, +) +from eth2spec.test.utils.randomized_block_tests import ( + run_generated_randomized_test, +)""" + +test_template = """ +@only_generator(\"randomized test for broad coverage, not point-to-point CI\") +@with_phases([{phase}]) +@with_custom_state( + balances_fn=misc_balances_in_default_range_with_many_validators, + threshold_fn=zero_activation_threshold +) +@spec_test +@single_phase +@always_bls +def test_randomized_{index}(spec, state): + # scenario as high-level, informal text: +{name_as_comment} + scenario = {scenario} # noqa: E501 + yield from run_generated_randomized_test( + spec, + state, + scenario, + )""" + + +def _to_comment(name, indent_level): + parts = name.split("|") + indentation = " " * indent_level + parts = [ + indentation + "# " + part for part in parts + ] + return "\n".join(parts) + + +def run_generate_tests_to_std_out(phase, state_randomizer, block_randomizer): + scenarios = _generate_randomized_scenarios(block_randomizer) + test_content = {"phase": phase.upper()} + test_imports = test_imports_template.format(**test_content) + test_file = [test_imports] + for index, scenario in enumerate(scenarios): + # required for setup phase + scenario["state_randomizer"] = state_randomizer.__name__ + + # need to pass name, rather than function reference... + transitions = scenario["transitions"] + for transition in transitions: + for name, value in transition.items(): + if isinstance(value, Callable): + transition[name] = value.__name__ + + test_content = test_content.copy() + name = _id_from_scenario(scenario) + test_content["name_as_comment"] = _to_comment(name, 1) + test_content["index"] = index + test_content["scenario"] = scenario + test_instance = test_template.format(**test_content) + test_file.append(test_instance) + print("\n\n".join(test_file)) + + +if __name__ == "__main__": + did_generate = False + if PHASE0 in sys.argv: + did_generate = True + run_generate_tests_to_std_out( + PHASE0, + state_randomizer=randomize_state, + block_randomizer=random_block, + ) + if ALTAIR in sys.argv: + did_generate = True + run_generate_tests_to_std_out( + ALTAIR, + state_randomizer=randomize_state_altair, + block_randomizer=random_block_altair_with_cycling_sync_committee_participation, + ) + if not did_generate: + warnings.warn("no phase given for test generation") diff --git a/tests/generators/random/main.py b/tests/generators/random/main.py new file mode 100644 index 000000000..f6f1b1847 --- /dev/null +++ b/tests/generators/random/main.py @@ -0,0 +1,18 @@ +from eth2spec.test.helpers.constants import PHASE0, ALTAIR +from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators + + +if __name__ == "__main__": + phase_0_mods = {key: 'eth2spec.test.phase0.random.test_' + key for key in [ + 'random', + ]} + altair_mods = {key: 'eth2spec.test.altair.random.test_' + key for key in [ + 'random', + ]} + + all_mods = { + PHASE0: phase_0_mods, + ALTAIR: altair_mods, + } + + run_state_test_generators(runner_name="random", all_mods=all_mods) diff --git a/tests/generators/random/requirements.txt b/tests/generators/random/requirements.txt new file mode 100644 index 000000000..182248686 --- /dev/null +++ b/tests/generators/random/requirements.txt @@ -0,0 +1,2 @@ +pytest>=4.4 +../../../[generator] diff --git a/tests/generators/rewards/main.py b/tests/generators/rewards/main.py index a3072daca..6b6a7bc6f 100644 --- a/tests/generators/rewards/main.py +++ b/tests/generators/rewards/main.py @@ -14,7 +14,7 @@ if __name__ == "__main__": # No additional merge specific rewards tests, yet. # Note: Block rewards are non-epoch rewards and are tested as part of block processing tests. # Transaction fees are part of the execution-layer. - merge_mods = phase_0_mods + merge_mods = altair_mods all_mods = { PHASE0: phase_0_mods, diff --git a/tests/generators/sanity/main.py b/tests/generators/sanity/main.py index 8caedc8e5..f5a6ccb41 100644 --- a/tests/generators/sanity/main.py +++ b/tests/generators/sanity/main.py @@ -9,12 +9,10 @@ if __name__ == "__main__": ]} altair_mods = {**{key: 'eth2spec.test.altair.sanity.test_' + key for key in [ 'blocks', - ]}, **phase_0_mods} # also run the previous phase 0 tests - - # Altair-specific test cases are ignored, but should be included after the Merge is rebased onto Altair work. + ]}, **phase_0_mods} merge_mods = {**{key: 'eth2spec.test.merge.sanity.test_' + key for key in [ 'blocks', - ]}, **phase_0_mods} # TODO: Merge inherits phase0 tests for now. + ]}, **altair_mods} all_mods = { PHASE0: phase_0_mods,