From 1f34ef9b565116322acc234fa1b2b8dec9c5e270 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 26 Aug 2021 10:50:50 -0700 Subject: [PATCH 1/2] modularize the random deposit helpers --- .../eth2spec/test/helpers/multi_operations.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py b/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py index 14b281a95..18c18194c 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/multi_operations.py @@ -113,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(1, 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 = [] @@ -132,15 +136,19 @@ 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 @@ -191,10 +199,7 @@ def get_random_sync_aggregate(spec, state, slot, fraction_participated=1.0, rng= ) -def build_random_block_from_state_for_next_slot(spec, state, rng=Random(2188)): - # 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) proposer_slashings = get_random_proposer_slashings(spec, state, rng) block.body.proposer_slashings = proposer_slashings @@ -204,7 +209,8 @@ def build_random_block_from_state_for_next_slot(spec, state, rng=Random(2188)): ] 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([ @@ -223,7 +229,9 @@ 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 - block = build_random_block_from_state_for_next_slot(spec, state, rng) + # 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 From 9474f0a051aaf25c4fa119b8fb1651ff6a2828a1 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 26 Aug 2021 10:52:02 -0700 Subject: [PATCH 2/2] construct and supply scenario-wide state to facilitate deposit processing --- .../test/utils/randomized_block_tests.py | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py index 44dab0e0e..f9d68221c 100644 --- a/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py +++ b/tests/core/pyspec/eth2spec/test/utils/randomized_block_tests.py @@ -10,6 +10,7 @@ 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, @@ -28,13 +29,35 @@ from eth2spec.test.helpers.state import ( # state -def randomize_state(spec, state, exit_fraction=0.1, slash_fraction=0.1): +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): - randomize_state(spec, state, exit_fraction=0.1, slash_fraction=0.1) +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 @@ -67,7 +90,7 @@ def penultimate_slot_in_epoch(spec): # blocks -def no_block(_spec, _pre_state, _signed_blocks): +def no_block(_spec, _pre_state, _signed_blocks, _scenario_state): return None @@ -77,9 +100,10 @@ BLOCK_ATTEMPTS = 32 def _warn_if_empty_operations(block): - if len(block.body.deposits) == 0: - warnings.warn(f"deposits missing in block at slot {block.slot}") - + """ + 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}") @@ -93,7 +117,13 @@ def _warn_if_empty_operations(block): warnings.warn(f"voluntary exits missing in block at slot {block.slot}") -def random_block(spec, state, _signed_blocks): +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 @@ -118,7 +148,8 @@ def random_block(spec, state, _signed_blocks): next_slot(spec, state) next_slot(spec, temp_state) else: - block = build_random_block_from_state_for_next_slot(spec, state) + 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: @@ -130,8 +161,9 @@ SYNC_AGGREGATE_PARTICIPATION_BUCKETS = 4 def random_block_altair_with_cycling_sync_committee_participation(spec, state, - signed_blocks): - block = random_block(spec, state, signed_blocks) + 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 @@ -146,7 +178,7 @@ def random_block_altair_with_cycling_sync_committee_participation(spec, # validations -def no_op_validation(spec, state): +def no_op_validation(_spec, _state): return True @@ -211,12 +243,20 @@ def transition_with_random_block(block_randomizer): def _randomized_scenario_setup(state_randomizer): """ - Return a sequence of pairs of ("mutation", "validation"), - a function that accepts (spec, state) arguments and performs some change - and a function that accepts (spec, state) arguments and validates some change was made. + 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): + 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... @@ -226,7 +266,7 @@ def _randomized_scenario_setup(state_randomizer): state.slot += slots_to_skip return f - def _simulate_honest_execution(spec, state): + 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, @@ -286,14 +326,29 @@ def _iter_temporal(spec, description): 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"]: - mutation(spec, state) + additional_state = mutation(spec, state, stats) validation(spec, state) + if additional_state: + scenario_state.update(additional_state) yield "pre", state @@ -307,7 +362,7 @@ def run_generated_randomized_test(spec, state, scenario): next_slot(spec, state) block_producer = _resolve_ref(transition["block_producer"]) - block = block_producer(spec, state, blocks) + block = block_producer(spec, state, blocks, scenario_state) if block: signed_block = state_transition_and_sign_block(spec, state, block) blocks.append(signed_block)