diff --git a/presets/mainnet/capella.yaml b/presets/mainnet/capella.yaml index f04bdd06f..62306c8dc 100644 --- a/presets/mainnet/capella.yaml +++ b/presets/mainnet/capella.yaml @@ -1,23 +1,11 @@ # Mainnet preset - Capella # Misc -# --------------------------------------------------------------- -# 2**8 (= 256) withdrawals -MAX_PARTIAL_WITHDRAWALS_PER_EPOCH: 256 - - -# State list lengths -# --------------------------------------------------------------- -# 2**40 (= 1,099,511,627,776) withdrawals -WITHDRAWAL_QUEUE_LIMIT: 1099511627776 - - # Max operations per block # --------------------------------------------------------------- # 2**4 (= 16) MAX_BLS_TO_EXECUTION_CHANGES: 16 - # Execution # --------------------------------------------------------------- # 2**4 (= 16) withdrawals diff --git a/presets/minimal/capella.yaml b/presets/minimal/capella.yaml index 0476172a1..1ac8a806c 100644 --- a/presets/minimal/capella.yaml +++ b/presets/minimal/capella.yaml @@ -1,17 +1,5 @@ # Minimal preset - Capella -# Misc -# --------------------------------------------------------------- -# [customized] 16 for more interesting tests at low validator count -MAX_PARTIAL_WITHDRAWALS_PER_EPOCH: 16 - - -# State list lengths -# --------------------------------------------------------------- -# 2**40 (= 1,099,511,627,776) withdrawals -WITHDRAWAL_QUEUE_LIMIT: 1099511627776 - - # Max operations per block # --------------------------------------------------------------- # 2**4 (= 16) @@ -20,5 +8,5 @@ MAX_BLS_TO_EXECUTION_CHANGES: 16 # Execution # --------------------------------------------------------------- -# [customized] Lower than MAX_PARTIAL_WITHDRAWALS_PER_EPOCH so not all processed in one block -MAX_WITHDRAWALS_PER_PAYLOAD: 8 +# [customized] 2**2 (= 4) +MAX_WITHDRAWALS_PER_PAYLOAD: 4 diff --git a/specs/capella/beacon-chain.md b/specs/capella/beacon-chain.md index 3b6dc4453..90a64925f 100644 --- a/specs/capella/beacon-chain.md +++ b/specs/capella/beacon-chain.md @@ -11,11 +11,8 @@ - [Constants](#constants) - [Domain types](#domain-types) - [Preset](#preset) - - [Misc](#misc) - - [State list lengths](#state-list-lengths) - [Max operations per block](#max-operations-per-block) - [Execution](#execution) -- [Configuration](#configuration) - [Containers](#containers) - [New containers](#new-containers) - [`Withdrawal`](#withdrawal) @@ -27,17 +24,13 @@ - [`BeaconBlockBody`](#beaconblockbody) - [`BeaconState`](#beaconstate) - [Helpers](#helpers) - - [Beacon state mutators](#beacon-state-mutators) - - [`withdraw_balance`](#withdraw_balance) - [Predicates](#predicates) - [`has_eth1_withdrawal_credential`](#has_eth1_withdrawal_credential) - [`is_fully_withdrawable_validator`](#is_fully_withdrawable_validator) - [`is_partially_withdrawable_validator`](#is_partially_withdrawable_validator) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - - [Epoch processing](#epoch-processing) - - [Full withdrawals](#full-withdrawals) - - [Partial withdrawals](#partial-withdrawals) - [Block processing](#block-processing) + - [New `get_expected_withdrawals`](#new-get_expected_withdrawals) - [New `process_withdrawals`](#new-process_withdrawals) - [Modified `process_execution_payload`](#modified-process_execution_payload) - [Modified `process_operations`](#modified-process_operations) @@ -75,18 +68,6 @@ We define the following Python custom types for type hinting and readability: ## Preset -### Misc - -| Name | Value | -| - | - | -| `MAX_PARTIAL_WITHDRAWALS_PER_EPOCH` | `uint64(2**8)` (= 256) | - -### State list lengths - -| Name | Value | Unit | -| - | - | :-: | -| `WITHDRAWAL_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state | - ### Max operations per block | Name | Value | @@ -99,8 +80,6 @@ We define the following Python custom types for type hinting and readability: | - | - | - | | `MAX_WITHDRAWALS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum amount of withdrawals allowed in each payload | -## Configuration - ## Containers ### New containers @@ -241,32 +220,12 @@ class BeaconState(Container): # Execution latest_execution_payload_header: ExecutionPayloadHeader # Withdrawals - withdrawal_queue: List[Withdrawal, WITHDRAWAL_QUEUE_LIMIT] # [New in Capella] next_withdrawal_index: WithdrawalIndex # [New in Capella] - next_partial_withdrawal_validator_index: ValidatorIndex # [New in Capella] + next_withdrawal_validator_index: ValidatorIndex # [New in Capella] ``` ## Helpers -### Beacon state mutators - -#### `withdraw_balance` - -```python -def withdraw_balance(state: BeaconState, validator_index: ValidatorIndex, amount: Gwei) -> None: - # Decrease the validator's balance - decrease_balance(state, validator_index, amount) - # Create a corresponding withdrawal receipt - withdrawal = Withdrawal( - index=state.next_withdrawal_index, - validator_index=validator_index, - address=ExecutionAddress(state.validators[validator_index].withdrawal_credentials[12:]), - amount=amount, - ) - state.next_withdrawal_index = WithdrawalIndex(state.next_withdrawal_index + 1) - state.withdrawal_queue.append(withdrawal) -``` - ### Predicates #### `has_eth1_withdrawal_credential` @@ -307,66 +266,6 @@ def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> ## Beacon chain state transition function -### Epoch processing - -```python -def process_epoch(state: BeaconState) -> None: - process_justification_and_finalization(state) - process_inactivity_updates(state) - process_rewards_and_penalties(state) - process_registry_updates(state) - process_slashings(state) - process_eth1_data_reset(state) - process_effective_balance_updates(state) - process_slashings_reset(state) - process_randao_mixes_reset(state) - process_historical_roots_update(state) - process_participation_flag_updates(state) - process_sync_committee_updates(state) - process_full_withdrawals(state) # [New in Capella] - process_partial_withdrawals(state) # [New in Capella] - -``` - -#### Full withdrawals - -*Note*: The function `process_full_withdrawals` is new. - -```python -def process_full_withdrawals(state: BeaconState) -> None: - current_epoch = get_current_epoch(state) - for index in range(len(state.validators)): - balance = state.balances[index] - validator = state.validators[index] - if is_fully_withdrawable_validator(validator, balance, current_epoch): - withdraw_balance(state, ValidatorIndex(index), balance) -``` - -#### Partial withdrawals - -*Note*: The function `process_partial_withdrawals` is new. - -```python -def process_partial_withdrawals(state: BeaconState) -> None: - partial_withdrawals_count = 0 - # Begin where we left off last time - validator_index = state.next_partial_withdrawal_validator_index - for _ in range(len(state.validators)): - balance = state.balances[validator_index] - validator = state.validators[validator_index] - if is_partially_withdrawable_validator(validator, balance): - withdraw_balance(state, validator_index, balance - MAX_EFFECTIVE_BALANCE) - partial_withdrawals_count += 1 - - # Iterate to next validator to check for partial withdrawal - validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) - # Exit if performed maximum allowable withdrawals - if partial_withdrawals_count == MAX_PARTIAL_WITHDRAWALS_PER_EPOCH: - break - - state.next_partial_withdrawal_validator_index = validator_index -``` - ### Block processing ```python @@ -377,23 +276,58 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in Capella] process_randao(state, block.body) process_eth1_data(state, block.body) - process_operations(state, block.body) + process_operations(state, block.body) # [Modified in Capella] process_sync_aggregate(state, block.body.sync_aggregate) ``` +#### New `get_expected_withdrawals` + +```python +def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: + epoch = get_current_epoch(state) + withdrawal_index = state.next_withdrawal_index + validator_index = state.next_withdrawal_validator_index + withdrawals: List[Withdrawal] = [] + for _ in range(len(state.validators)): + validator = state.validators[validator_index] + balance = state.balances[validator_index] + if is_fully_withdrawable_validator(validator, balance, epoch): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance, + )) + withdrawal_index += WithdrawalIndex(1) + elif is_partially_withdrawable_validator(validator, balance): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance - MAX_EFFECTIVE_BALANCE, + )) + withdrawal_index += WithdrawalIndex(1) + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + break + validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) + return withdrawals +``` + #### New `process_withdrawals` ```python def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: - num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawal_queue)) - dequeued_withdrawals = state.withdrawal_queue[:num_withdrawals] + expected_withdrawals = get_expected_withdrawals(state) + assert len(payload.withdrawals) == len(expected_withdrawals) - assert len(dequeued_withdrawals) == len(payload.withdrawals) - for dequeued_withdrawal, withdrawal in zip(dequeued_withdrawals, payload.withdrawals): - assert dequeued_withdrawal == withdrawal - - # Remove dequeued withdrawals from state - state.withdrawal_queue = state.withdrawal_queue[num_withdrawals:] + for expected_withdrawal, withdrawal in zip(expected_withdrawals, payload.withdrawals): + assert withdrawal == expected_withdrawal + decrease_balance(state, withdrawal.validator_index, withdrawal.amount) + if len(expected_withdrawals) > 0: + latest_withdrawal = expected_withdrawals[-1] + state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) + next_validator_index = ValidatorIndex((latest_withdrawal.validator_index + 1) % len(state.validators)) + state.next_withdrawal_validator_index = next_validator_index ``` #### Modified `process_execution_payload` diff --git a/specs/capella/fork.md b/specs/capella/fork.md index bc3c95aed..e2493d33b 100644 --- a/specs/capella/fork.md +++ b/specs/capella/fork.md @@ -129,9 +129,8 @@ def upgrade_to_capella(pre: bellatrix.BeaconState) -> BeaconState: # Execution-layer latest_execution_payload_header=latest_execution_payload_header, # Withdrawals - withdrawal_queue=[], next_withdrawal_index=WithdrawalIndex(0), - next_partial_withdrawal_validator_index=ValidatorIndex(0), + next_withdrawal_validator_index=ValidatorIndex(0), ) return post diff --git a/specs/capella/validator.md b/specs/capella/validator.md index 90176e035..8eb0d3040 100644 --- a/specs/capella/validator.md +++ b/specs/capella/validator.md @@ -59,12 +59,8 @@ All validator responsibilities remain unchanged other than those noted below. expected withdrawals for the slot must be gathered from the `state` (utilizing the helper `get_expected_withdrawals`) and passed into the `ExecutionEngine` within `prepare_execution_payload`. - -```python -def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: - num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawal_queue)) - return state.withdrawal_queue[:num_withdrawals] -``` +*Note*: In this section, `state` is the state of the slot for the block proposal _without_ the block yet applied. +That is, `state` is the `previous_state` processed through any empty slots up to the assigned slot using `process_slots(previous_state, slot)`. *Note*: The only change made to `prepare_execution_payload` is to call `get_expected_withdrawals()` to set the new `withdrawals` field of `PayloadAttributes`. diff --git a/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py index 9bf70b56d..7b39f2b9d 100644 --- a/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py +++ b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py @@ -1,28 +1,54 @@ +import random + +from eth2spec.test.context import ( + spec_state_test, + expect_assertion_error, + with_capella_and_later, + with_presets, +) +from eth2spec.test.helpers.constants import MINIMAL from eth2spec.test.helpers.execution_payload import ( build_empty_execution_payload, ) - -from eth2spec.test.context import spec_state_test, expect_assertion_error, with_capella_and_later - -from eth2spec.test.helpers.state import next_slot +from eth2spec.test.helpers.random import ( + randomize_state, +) +from eth2spec.test.helpers.state import ( + next_epoch, + next_slot, +) +from eth2spec.test.helpers.withdrawals import ( + prepare_expected_withdrawals, + set_eth1_withdrawal_credential_with_balance, + set_validator_fully_withdrawable, + set_validator_partially_withdrawable, +) -def prepare_withdrawal_queue(spec, state, num_withdrawals): - pre_queue_len = len(state.withdrawal_queue) - validator_len = len(state.validators) - for i in range(num_withdrawals): - withdrawal = spec.Withdrawal( - index=i + 5, - validator_index=(i + 1000) % validator_len, - address=b'\x42' * 20, - amount=200000 + i, - ) - state.withdrawal_queue.append(withdrawal) +def verify_post_state(state, spec, expected_withdrawals, + fully_withdrawable_indices, partial_withdrawals_indices): + # Consider verifying also the condition when no withdrawals are expected. + if len(expected_withdrawals) == 0: + return - assert len(state.withdrawal_queue) == num_withdrawals + pre_queue_len + expected_withdrawals_validator_indices = [withdrawal.validator_index for withdrawal in expected_withdrawals] + assert state.next_withdrawal_index == expected_withdrawals[-1].index + 1 + next_withdrawal_validator_index = (expected_withdrawals_validator_indices[-1] + 1) % len(state.validators) + assert state.next_withdrawal_validator_index == next_withdrawal_validator_index + for index in fully_withdrawable_indices: + if index in expected_withdrawals_validator_indices: + assert state.balances[index] == 0 + else: + assert state.balances[index] > 0 + for index in partial_withdrawals_indices: + if index in expected_withdrawals_validator_indices: + assert state.balances[index] == spec.MAX_EFFECTIVE_BALANCE + else: + assert state.balances[index] > spec.MAX_EFFECTIVE_BALANCE -def run_withdrawals_processing(spec, state, execution_payload, valid=True): +def run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=None, + fully_withdrawable_indices=None, partial_withdrawals_indices=None, valid=True): """ Run ``process_execution_payload``, yielding: - pre-state ('pre') @@ -30,10 +56,12 @@ def run_withdrawals_processing(spec, state, execution_payload, valid=True): - post-state ('post'). If ``valid == False``, run expecting ``AssertionError`` """ + expected_withdrawals = spec.get_expected_withdrawals(state) + assert len(expected_withdrawals) <= spec.MAX_WITHDRAWALS_PER_PAYLOAD + if num_expected_withdrawals is not None: + assert len(expected_withdrawals) == num_expected_withdrawals - pre_withdrawal_queue = state.withdrawal_queue.copy() - num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(pre_withdrawal_queue)) - + pre_state = state.copy() yield 'pre', state yield 'execution_payload', execution_payload @@ -46,18 +74,23 @@ def run_withdrawals_processing(spec, state, execution_payload, valid=True): yield 'post', state - if len(pre_withdrawal_queue) == 0: - assert len(state.withdrawal_queue) == 0 - elif len(pre_withdrawal_queue) <= num_withdrawals: - assert len(state.withdrawal_queue) == 0 - else: - assert state.withdrawal_queue == pre_withdrawal_queue[num_withdrawals:] + if len(expected_withdrawals) == 0: + assert state == pre_state + elif len(expected_withdrawals) < spec.MAX_WITHDRAWALS_PER_PAYLOAD: + assert len(spec.get_expected_withdrawals(state)) == 0 + elif len(expected_withdrawals) > spec.MAX_WITHDRAWALS_PER_PAYLOAD: + raise ValueError('len(expected_withdrawals) should not be greater than MAX_WITHDRAWALS_PER_PAYLOAD') + + if fully_withdrawable_indices is not None or partial_withdrawals_indices is not None: + verify_post_state(state, spec, expected_withdrawals, fully_withdrawable_indices, partial_withdrawals_indices) + + return expected_withdrawals @with_capella_and_later @spec_state_test -def test_success_empty_queue(spec, state): - assert len(state.withdrawal_queue) == 0 +def test_success_zero_expected_withdrawals(spec, state): + assert len(spec.get_expected_withdrawals(state)) == 0 next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -67,35 +100,87 @@ def test_success_empty_queue(spec, state): @with_capella_and_later @spec_state_test -def test_success_one_in_queue(spec, state): - prepare_withdrawal_queue(spec, state, 1) +def test_success_one_full_withdrawal(spec, state): + fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals( + spec, state, num_full_withdrawals=1) + assert len(fully_withdrawable_indices) == 1 + assert len(partial_withdrawals_indices) == 0 next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) - yield from run_withdrawals_processing(spec, state, execution_payload) + yield from run_withdrawals_processing( + spec, state, execution_payload, + fully_withdrawable_indices=fully_withdrawable_indices, + partial_withdrawals_indices=partial_withdrawals_indices) @with_capella_and_later @spec_state_test -def test_success_max_per_slot_in_queue(spec, state): - prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) +def test_success_one_partial_withdrawal(spec, state): + fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals( + spec, state, num_partial_withdrawals=1) + assert len(fully_withdrawable_indices) == 0 + assert len(partial_withdrawals_indices) == 1 + for index in partial_withdrawals_indices: + assert state.balances[index] > spec.MAX_EFFECTIVE_BALANCE next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) - yield from run_withdrawals_processing(spec, state, execution_payload) + yield from run_withdrawals_processing( + spec, state, execution_payload, + fully_withdrawable_indices=fully_withdrawable_indices, + partial_withdrawals_indices=partial_withdrawals_indices + ) @with_capella_and_later @spec_state_test -def test_success_a_lot_in_queue(spec, state): - prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) +def test_success_max_per_slot(spec, state): + num_full_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD // 2 + num_partial_withdrawals = spec.MAX_WITHDRAWALS_PER_PAYLOAD - num_full_withdrawals + fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals( + spec, state, + num_full_withdrawals=num_full_withdrawals, num_partial_withdrawals=num_partial_withdrawals) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) - yield from run_withdrawals_processing(spec, state, execution_payload) + yield from run_withdrawals_processing( + spec, state, execution_payload, + fully_withdrawable_indices=fully_withdrawable_indices, + partial_withdrawals_indices=partial_withdrawals_indices) + + +@with_capella_and_later +@spec_state_test +def test_success_all_fully_withdrawable(spec, state): + fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals( + spec, state, num_full_withdrawals=len(state.validators)) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing( + spec, state, execution_payload, + fully_withdrawable_indices=fully_withdrawable_indices, + partial_withdrawals_indices=partial_withdrawals_indices) + + +@with_capella_and_later +@spec_state_test +def test_success_all_partially_withdrawable(spec, state): + fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals( + spec, state, num_partial_withdrawals=len(state.validators)) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing( + spec, state, execution_payload, + fully_withdrawable_indices=fully_withdrawable_indices, + partial_withdrawals_indices=partial_withdrawals_indices) # @@ -104,9 +189,7 @@ def test_success_a_lot_in_queue(spec, state): @with_capella_and_later @spec_state_test -def test_fail_empty_queue_non_empty_withdrawals(spec, state): - assert len(state.withdrawal_queue) == 0 - +def test_fail_non_withdrawable_non_empty_withdrawals(spec, state): next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) withdrawal = spec.Withdrawal( @@ -122,8 +205,8 @@ def test_fail_empty_queue_non_empty_withdrawals(spec, state): @with_capella_and_later @spec_state_test -def test_fail_one_in_queue_none_in_withdrawals(spec, state): - prepare_withdrawal_queue(spec, state, 1) +def test_fail_one_expected_full_withdrawal_and_none_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -134,8 +217,20 @@ def test_fail_one_in_queue_none_in_withdrawals(spec, state): @with_capella_and_later @spec_state_test -def test_fail_one_in_queue_two_in_withdrawals(spec, state): - prepare_withdrawal_queue(spec, state, 1) +def test_fail_one_expected_partial_withdrawal_and_none_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = [] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_one_expected_full_withdrawal_and_duplicate_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=2) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -146,8 +241,20 @@ def test_fail_one_in_queue_two_in_withdrawals(spec, state): @with_capella_and_later @spec_state_test -def test_fail_max_per_slot_in_queue_one_less_in_withdrawals(spec, state): - prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) +def test_fail_two_expected_partial_withdrawal_and_duplicate_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=2) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals.append(execution_payload.withdrawals[0].copy()) + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_max_per_slot_full_withdrawals_and_one_less_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -158,8 +265,45 @@ def test_fail_max_per_slot_in_queue_one_less_in_withdrawals(spec, state): @with_capella_and_later @spec_state_test -def test_fail_a_lot_in_queue_too_few_in_withdrawals(spec, state): - prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) +def test_fail_max_per_slot_partial_withdrawals_and_one_less_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = execution_payload.withdrawals[:-1] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_a_lot_fully_withdrawable_too_few_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = execution_payload.withdrawals[:-1] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_a_lot_partially_withdrawable_too_few_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = execution_payload.withdrawals[:-1] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_a_lot_mixed_withdrawable_in_queue_too_few_in_withdrawals(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4, + num_partial_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -174,8 +318,8 @@ def test_fail_a_lot_in_queue_too_few_in_withdrawals(spec, state): @with_capella_and_later @spec_state_test -def test_fail_incorrect_dequeue_index(spec, state): - prepare_withdrawal_queue(spec, state, 1) +def test_fail_incorrect_withdrawal_index(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -186,8 +330,8 @@ def test_fail_incorrect_dequeue_index(spec, state): @with_capella_and_later @spec_state_test -def test_fail_incorrect_dequeue_address(spec, state): - prepare_withdrawal_queue(spec, state, 1) +def test_fail_incorrect_address_full(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -198,8 +342,20 @@ def test_fail_incorrect_dequeue_address(spec, state): @with_capella_and_later @spec_state_test -def test_fail_incorrect_dequeue_amount(spec, state): - prepare_withdrawal_queue(spec, state, 1) +def test_fail_incorrect_address_partial(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals[0].address = b'\xff' * 20 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_incorrect_amount_full(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -210,8 +366,20 @@ def test_fail_incorrect_dequeue_amount(spec, state): @with_capella_and_later @spec_state_test -def test_fail_one_of_many_dequeued_incorrectly(spec, state): - prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) +def test_fail_incorrect_amount_partial(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals[0].amount += 1 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_one_of_many_incorrectly_full(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -228,8 +396,26 @@ def test_fail_one_of_many_dequeued_incorrectly(spec, state): @with_capella_and_later @spec_state_test -def test_fail_many_dequeued_incorrectly(spec, state): - prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) +def test_fail_one_of_many_incorrectly_partial(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + num_withdrawals = len(execution_payload.withdrawals) + + # Pick withdrawal in middle of list and mutate + withdrawal = execution_payload.withdrawals[num_withdrawals // 2] + withdrawal.index += 1 + withdrawal.address = b'\x99' * 20 + withdrawal.amount += 4000000 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_many_incorrectly_full(spec, state): + prepare_expected_withdrawals(spec, state, num_full_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -242,3 +428,367 @@ def test_fail_many_dequeued_incorrectly(spec, state): withdrawal.amount += 1 yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_many_incorrectly_partial(spec, state): + prepare_expected_withdrawals(spec, state, num_partial_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + for i, withdrawal in enumerate(execution_payload.withdrawals): + if i % 3 == 0: + withdrawal.index += 1 + elif i % 3 == 1: + withdrawal.address = i.to_bytes(20, 'big') + else: + withdrawal.amount += 1 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +# +# More full withdrawal cases +# + +@with_capella_and_later +@spec_state_test +def test_withdrawable_epoch_but_0_balance(spec, state): + current_epoch = spec.get_current_epoch(state) + set_validator_fully_withdrawable(spec, state, 0, current_epoch) + + state.validators[0].effective_balance = 10000000000 + state.balances[0] = 0 + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0) + + +@with_capella_and_later +@spec_state_test +def test_withdrawable_epoch_but_0_effective_balance_0_balance(spec, state): + current_epoch = spec.get_current_epoch(state) + set_validator_fully_withdrawable(spec, state, 0, current_epoch) + + state.validators[0].effective_balance = 0 + state.balances[0] = 0 + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0) + + +@with_capella_and_later +@spec_state_test +def test_withdrawable_epoch_but_0_effective_balance_nonzero_balance(spec, state): + current_epoch = spec.get_current_epoch(state) + set_validator_fully_withdrawable(spec, state, 0, current_epoch) + + state.validators[0].effective_balance = 0 + state.balances[0] = 100000000 + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1) + + +@with_capella_and_later +@spec_state_test +def test_no_withdrawals_but_some_next_epoch(spec, state): + current_epoch = spec.get_current_epoch(state) + + # Make a few validators withdrawable at the *next* epoch + for index in range(3): + set_validator_fully_withdrawable(spec, state, index, current_epoch + 1) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0) + + +@with_capella_and_later +@spec_state_test +def test_all_withdrawal(spec, state): + # Make all validators withdrawable + for index in range(len(state.validators)): + set_validator_fully_withdrawable(spec, state, index) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing( + spec, state, execution_payload, + num_expected_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD) + + +def run_random_full_withdrawals_test(spec, state, rng): + randomize_state(spec, state, rng) + for index in range(len(state.validators)): + # 50% withdrawable + if rng.choice([True, False]): + set_validator_fully_withdrawable(spec, state, index) + validator = state.validators[index] + # 12.5% unset credentials + if rng.randint(0, 7) == 0: + validator.withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] + # 12.5% not enough balance + if rng.randint(0, 7) == 0: + state.balances[index] = 0 + # 12.5% not close enough epoch + if rng.randint(0, 7) == 0: + validator.withdrawable_epoch += 1 + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload) + + +@with_capella_and_later +@spec_state_test +def test_random_full_withdrawals_0(spec, state): + yield from run_random_full_withdrawals_test(spec, state, random.Random(444)) + + +@with_capella_and_later +@spec_state_test +def test_random_full_withdrawals_1(spec, state): + yield from run_random_full_withdrawals_test(spec, state, random.Random(420)) + + +@with_capella_and_later +@spec_state_test +def test_random_full_withdrawals_2(spec, state): + yield from run_random_full_withdrawals_test(spec, state, random.Random(200)) + + +@with_capella_and_later +@spec_state_test +def test_random_full_withdrawals_3(spec, state): + yield from run_random_full_withdrawals_test(spec, state, random.Random(2000000)) + + +# +# More partial withdrawal cases +# + +@with_capella_and_later +@spec_state_test +def test_success_no_max_effective_balance(spec, state): + validator_index = len(state.validators) // 2 + # To be partially withdrawable, the validator's effective balance must be maxed out + set_eth1_withdrawal_credential_with_balance(spec, state, validator_index, spec.MAX_EFFECTIVE_BALANCE - 1) + validator = state.validators[validator_index] + + assert validator.effective_balance < spec.MAX_EFFECTIVE_BALANCE + assert not spec.is_partially_withdrawable_validator(validator, state.balances[validator_index]) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0) + + +@with_capella_and_later +@spec_state_test +def test_success_no_excess_balance(spec, state): + validator_index = len(state.validators) // 2 + # To be partially withdrawable, the validator needs an excess balance + set_eth1_withdrawal_credential_with_balance(spec, state, validator_index, spec.MAX_EFFECTIVE_BALANCE) + validator = state.validators[validator_index] + + assert validator.effective_balance == spec.MAX_EFFECTIVE_BALANCE + assert not spec.is_partially_withdrawable_validator(validator, state.balances[validator_index]) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0) + + +@with_capella_and_later +@spec_state_test +def test_success_excess_balance_but_no_max_effective_balance(spec, state): + validator_index = len(state.validators) // 2 + set_validator_partially_withdrawable(spec, state, validator_index) + validator = state.validators[validator_index] + + # To be partially withdrawable, the validator needs both a maxed out effective balance and an excess balance + validator.effective_balance = spec.MAX_EFFECTIVE_BALANCE - 1 + + assert not spec.is_partially_withdrawable_validator(validator, state.balances[validator_index]) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=0) + + +@with_capella_and_later +@spec_state_test +def test_success_one_partial_withdrawable_not_yet_active(spec, state): + validator_index = len(state.validators) // 2 + state.validators[validator_index].activation_epoch += 4 + set_validator_partially_withdrawable(spec, state, validator_index) + + assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1) + + +@with_capella_and_later +@spec_state_test +def test_success_one_partial_withdrawable_in_exit_queue(spec, state): + validator_index = len(state.validators) // 2 + state.validators[validator_index].exit_epoch = spec.get_current_epoch(state) + 1 + set_validator_partially_withdrawable(spec, state, validator_index) + + assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state) + 1) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1) + + +@with_capella_and_later +@spec_state_test +def test_success_one_partial_withdrawable_exited(spec, state): + validator_index = len(state.validators) // 2 + state.validators[validator_index].exit_epoch = spec.get_current_epoch(state) + set_validator_partially_withdrawable(spec, state, validator_index) + + assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1) + + +@with_capella_and_later +@spec_state_test +def test_success_one_partial_withdrawable_active_and_slashed(spec, state): + validator_index = len(state.validators) // 2 + state.validators[validator_index].slashed = True + set_validator_partially_withdrawable(spec, state, validator_index) + + assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1) + + +@with_capella_and_later +@spec_state_test +def test_success_one_partial_withdrawable_exited_and_slashed(spec, state): + validator_index = len(state.validators) // 2 + state.validators[validator_index].slashed = True + state.validators[validator_index].exit_epoch = spec.get_current_epoch(state) + set_validator_partially_withdrawable(spec, state, validator_index) + + assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=1) + + +@with_capella_and_later +@spec_state_test +def test_success_two_partial_withdrawable(spec, state): + set_validator_partially_withdrawable(spec, state, 0) + set_validator_partially_withdrawable(spec, state, 1) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload, num_expected_withdrawals=2) + + +@with_capella_and_later +@spec_state_test +def test_success_max_partial_withdrawable(spec, state): + # Sanity check that this test works for this state + assert len(state.validators) >= spec.MAX_WITHDRAWALS_PER_PAYLOAD + + for i in range(spec.MAX_WITHDRAWALS_PER_PAYLOAD): + set_validator_partially_withdrawable(spec, state, i) + + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing( + spec, state, execution_payload, num_expected_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD) + + +@with_capella_and_later +@with_presets([MINIMAL], reason="not enough validators with mainnet config") +@spec_state_test +def test_success_max_plus_one_withdrawable(spec, state): + # Sanity check that this test works for this state + assert len(state.validators) >= spec.MAX_WITHDRAWALS_PER_PAYLOAD + 1 + + # More than MAX_WITHDRAWALS_PER_PAYLOAD partially withdrawable + for i in range(spec.MAX_WITHDRAWALS_PER_PAYLOAD + 1): + set_validator_partially_withdrawable(spec, state, i) + + execution_payload = build_empty_execution_payload(spec, state) + + # Should only have MAX_WITHDRAWALS_PER_PAYLOAD withdrawals created + yield from run_withdrawals_processing( + spec, state, execution_payload, num_expected_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD) + + +def run_random_partial_withdrawals_test(spec, state, rng): + for _ in range(rng.randint(0, 2)): + next_epoch(spec, state) + randomize_state(spec, state, rng) + + num_validators = len(state.validators) + state.next_withdrawal_validator_index = rng.randint(0, num_validators - 1) + + num_partially_withdrawable = rng.randint(0, num_validators - 1) + partially_withdrawable_indices = rng.sample(range(num_validators), num_partially_withdrawable) + for index in partially_withdrawable_indices: + set_validator_partially_withdrawable(spec, state, index, excess_balance=rng.randint(1, 1000000000)) + + execution_payload = build_empty_execution_payload(spec, state) + + # Note: due to the randomness and other block processing, some of these set as "partially withdrawable" + # may not be partially withdrawable once we get to ``process_withdrawals``, + # thus *not* using the optional third param in this call + yield from run_withdrawals_processing(spec, state, execution_payload) + + +@with_capella_and_later +@spec_state_test +def test_random_0(spec, state): + yield from run_random_partial_withdrawals_test(spec, state, random.Random(0)) + + +@with_capella_and_later +@spec_state_test +def test_random_partial_withdrawals_1(spec, state): + yield from run_random_partial_withdrawals_test(spec, state, random.Random(1)) + + +@with_capella_and_later +@spec_state_test +def test_random_partial_withdrawals_2(spec, state): + yield from run_random_partial_withdrawals_test(spec, state, random.Random(2)) + + +@with_capella_and_later +@spec_state_test +def test_random_partial_withdrawals_3(spec, state): + yield from run_random_partial_withdrawals_test(spec, state, random.Random(3)) + + +@with_capella_and_later +@spec_state_test +def test_random_partial_withdrawals_4(spec, state): + yield from run_random_partial_withdrawals_test(spec, state, random.Random(4)) + + +@with_capella_and_later +@spec_state_test +def test_random_partial_withdrawals_5(spec, state): + yield from run_random_partial_withdrawals_test(spec, state, random.Random(5)) diff --git a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/__init__.py b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_full_withdrawals.py b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_full_withdrawals.py deleted file mode 100644 index 35d2968cb..000000000 --- a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_full_withdrawals.py +++ /dev/null @@ -1,174 +0,0 @@ -from random import Random - -from eth2spec.test.context import ( - with_capella_and_later, - spec_state_test, -) -from eth2spec.test.helpers.random import ( - randomize_state, -) -from eth2spec.test.helpers.epoch_processing import ( - run_epoch_processing_to, -) -from eth2spec.test.helpers.withdrawals import ( - set_validator_fully_withdrawable, -) - - -def run_process_full_withdrawals(spec, state, num_expected_withdrawals=None): - run_epoch_processing_to(spec, state, 'process_full_withdrawals') - - pre_next_withdrawal_index = state.next_withdrawal_index - pre_withdrawal_queue = state.withdrawal_queue.copy() - to_be_withdrawn_indices = [ - index for index, validator in enumerate(state.validators) - if spec.is_fully_withdrawable_validator(validator, state.balances[index], spec.get_current_epoch(state)) - ] - - if num_expected_withdrawals is not None: - assert len(to_be_withdrawn_indices) == num_expected_withdrawals - else: - num_expected_withdrawals = len(to_be_withdrawn_indices) - - yield 'pre', state - spec.process_full_withdrawals(state) - yield 'post', state - - for index in to_be_withdrawn_indices: - assert state.balances[index] == 0 - - assert len(state.withdrawal_queue) == len(pre_withdrawal_queue) + num_expected_withdrawals - assert state.next_withdrawal_index == pre_next_withdrawal_index + num_expected_withdrawals - - -@with_capella_and_later -@spec_state_test -def test_no_withdrawable_validators(spec, state): - pre_validators = state.validators.copy() - yield from run_process_full_withdrawals(spec, state, 0) - - assert pre_validators == state.validators - - -@with_capella_and_later -@spec_state_test -def test_withdrawable_epoch_but_0_balance(spec, state): - current_epoch = spec.get_current_epoch(state) - set_validator_fully_withdrawable(spec, state, 0, current_epoch) - - state.validators[0].effective_balance = 10000000000 - state.balances[0] = 0 - - yield from run_process_full_withdrawals(spec, state, 0) - - -@with_capella_and_later -@spec_state_test -def test_withdrawable_epoch_but_0_effective_balance_0_balance(spec, state): - current_epoch = spec.get_current_epoch(state) - set_validator_fully_withdrawable(spec, state, 0, current_epoch) - - state.validators[0].effective_balance = 0 - state.balances[0] = 0 - - yield from run_process_full_withdrawals(spec, state, 0) - - -@with_capella_and_later -@spec_state_test -def test_withdrawable_epoch_but_0_effective_balance_nonzero_balance(spec, state): - current_epoch = spec.get_current_epoch(state) - set_validator_fully_withdrawable(spec, state, 0, current_epoch) - - state.validators[0].effective_balance = 0 - state.balances[0] = 100000000 - - yield from run_process_full_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_no_withdrawals_but_some_next_epoch(spec, state): - current_epoch = spec.get_current_epoch(state) - - # Make a few validators withdrawable at the *next* epoch - for index in range(3): - set_validator_fully_withdrawable(spec, state, index, current_epoch + 1) - - yield from run_process_full_withdrawals(spec, state, 0) - - -@with_capella_and_later -@spec_state_test -def test_single_withdrawal(spec, state): - # Make one validator withdrawable - set_validator_fully_withdrawable(spec, state, 0) - - assert state.next_withdrawal_index == 0 - yield from run_process_full_withdrawals(spec, state, 1) - - assert state.next_withdrawal_index == 1 - - -@with_capella_and_later -@spec_state_test -def test_multi_withdrawal(spec, state): - # Make a few validators withdrawable - for index in range(3): - set_validator_fully_withdrawable(spec, state, index) - - yield from run_process_full_withdrawals(spec, state, 3) - - -@with_capella_and_later -@spec_state_test -def test_all_withdrawal(spec, state): - # Make all validators withdrawable - for index in range(len(state.validators)): - set_validator_fully_withdrawable(spec, state, index) - - yield from run_process_full_withdrawals(spec, state, len(state.validators)) - - -def run_random_full_withdrawals_test(spec, state, rng): - randomize_state(spec, state, rng) - for index in range(len(state.validators)): - # 50% withdrawable - if rng.choice([True, False]): - set_validator_fully_withdrawable(spec, state, index) - validator = state.validators[index] - # 12.5% unset credentials - if rng.randint(0, 7) == 0: - validator.withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] - # 12.5% not enough balance - if rng.randint(0, 7) == 0: - state.balances[index] = 0 - # 12.5% not close enough epoch - if rng.randint(0, 7) == 0: - validator.withdrawable_epoch += 1 - - yield from run_process_full_withdrawals(spec, state, None) - - -@with_capella_and_later -@spec_state_test -def test_random_withdrawals_0(spec, state): - yield from run_random_full_withdrawals_test(spec, state, Random(444)) - - -@with_capella_and_later -@spec_state_test -def test_random_withdrawals_1(spec, state): - yield from run_random_full_withdrawals_test(spec, state, Random(420)) - - -@with_capella_and_later -@spec_state_test -def test_random_withdrawals_2(spec, state): - yield from run_random_full_withdrawals_test(spec, state, Random(200)) - - -@with_capella_and_later -@spec_state_test -def test_random_withdrawals_3(spec, state): - yield from run_random_full_withdrawals_test(spec, state, Random(2000000)) diff --git a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_partial_withdrawals.py b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_partial_withdrawals.py deleted file mode 100644 index 7569d2862..000000000 --- a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_partial_withdrawals.py +++ /dev/null @@ -1,262 +0,0 @@ -import random -from eth2spec.test.helpers.constants import MINIMAL -from eth2spec.test.context import ( - with_capella_and_later, - spec_state_test, - with_presets, -) -from eth2spec.test.helpers.epoch_processing import run_epoch_processing_to -from eth2spec.test.helpers.state import next_epoch -from eth2spec.test.helpers.random import randomize_state -from eth2spec.test.helpers.withdrawals import ( - set_validator_partially_withdrawable, - set_eth1_withdrawal_credential_with_balance, -) - - -def run_process_partial_withdrawals(spec, state, num_expected_withdrawals=None): - # Run rest of epoch processing before predicting partial withdrawals as - # balance changes can affect withdrawability - run_epoch_processing_to(spec, state, 'process_partial_withdrawals') - - pre_next_withdrawal_index = state.next_withdrawal_index - pre_withdrawal_queue = state.withdrawal_queue.copy() - - partially_withdrawable_indices = [ - index for index, validator in enumerate(state.validators) - if spec.is_partially_withdrawable_validator(validator, state.balances[index]) - ] - num_partial_withdrawals = min(len(partially_withdrawable_indices), spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH) - - if num_expected_withdrawals is not None: - assert num_partial_withdrawals == num_expected_withdrawals - else: - num_expected_withdrawals = num_partial_withdrawals - - yield 'pre', state - spec.process_partial_withdrawals(state) - yield 'post', state - - post_partially_withdrawable_indices = [ - index for index, validator in enumerate(state.validators) - if spec.is_partially_withdrawable_validator(validator, state.balances[index]) - ] - - assert len(partially_withdrawable_indices) - num_partial_withdrawals == len(post_partially_withdrawable_indices) - - assert len(state.withdrawal_queue) == len(pre_withdrawal_queue) + num_expected_withdrawals - assert state.next_withdrawal_index == pre_next_withdrawal_index + num_expected_withdrawals - - -@with_capella_and_later -@spec_state_test -def test_success_no_withdrawable(spec, state): - pre_validators = state.validators.copy() - yield from run_process_partial_withdrawals(spec, state, 0) - - assert pre_validators == state.validators - - -@with_capella_and_later -@spec_state_test -def test_success_no_max_effective_balance(spec, state): - validator_index = len(state.validators) // 2 - # To be partially withdrawable, the validator's effective balance must be maxed out - set_eth1_withdrawal_credential_with_balance(spec, state, validator_index, spec.MAX_EFFECTIVE_BALANCE - 1) - validator = state.validators[validator_index] - - assert validator.effective_balance < spec.MAX_EFFECTIVE_BALANCE - assert not spec.is_partially_withdrawable_validator(validator, state.balances[validator_index]) - - yield from run_process_partial_withdrawals(spec, state, 0) - - -@with_capella_and_later -@spec_state_test -def test_success_no_excess_balance(spec, state): - validator_index = len(state.validators) // 2 - # To be partially withdrawable, the validator needs an excess balance - set_eth1_withdrawal_credential_with_balance(spec, state, validator_index, spec.MAX_EFFECTIVE_BALANCE) - validator = state.validators[validator_index] - - assert validator.effective_balance == spec.MAX_EFFECTIVE_BALANCE - assert not spec.is_partially_withdrawable_validator(validator, state.balances[validator_index]) - - yield from run_process_partial_withdrawals(spec, state, 0) - - -@with_capella_and_later -@spec_state_test -def test_success_excess_balance_but_no_max_effective_balance(spec, state): - validator_index = len(state.validators) // 2 - set_validator_partially_withdrawable(spec, state, validator_index) - validator = state.validators[validator_index] - - # To be partially withdrawable, the validator needs both a maxed out effective balance and an excess balance - validator.effective_balance = spec.MAX_EFFECTIVE_BALANCE - 1 - - assert not spec.is_partially_withdrawable_validator(validator, state.balances[validator_index]) - - yield from run_process_partial_withdrawals(spec, state, 0) - - -@with_capella_and_later -@spec_state_test -def test_success_one_partial_withdrawable(spec, state): - validator_index = len(state.validators) // 2 - set_validator_partially_withdrawable(spec, state, validator_index) - - yield from run_process_partial_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_success_one_partial_withdrawable_not_yet_active(spec, state): - validator_index = len(state.validators) // 2 - state.validators[validator_index].activation_epoch += 4 - set_validator_partially_withdrawable(spec, state, validator_index) - - assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) - - yield from run_process_partial_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_success_one_partial_withdrawable_in_exit_queue(spec, state): - validator_index = len(state.validators) // 2 - state.validators[validator_index].exit_epoch = spec.get_current_epoch(state) + 1 - set_validator_partially_withdrawable(spec, state, validator_index) - - assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) - assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state) + 1) - - yield from run_process_partial_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_success_one_partial_withdrawable_exited(spec, state): - validator_index = len(state.validators) // 2 - state.validators[validator_index].exit_epoch = spec.get_current_epoch(state) - set_validator_partially_withdrawable(spec, state, validator_index) - - assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) - - yield from run_process_partial_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_success_one_partial_withdrawable_active_and_slashed(spec, state): - validator_index = len(state.validators) // 2 - state.validators[validator_index].slashed = True - set_validator_partially_withdrawable(spec, state, validator_index) - - assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) - - yield from run_process_partial_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_success_one_partial_withdrawable_exited_and_slashed(spec, state): - validator_index = len(state.validators) // 2 - state.validators[validator_index].slashed = True - state.validators[validator_index].exit_epoch = spec.get_current_epoch(state) - set_validator_partially_withdrawable(spec, state, validator_index) - - assert not spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) - - yield from run_process_partial_withdrawals(spec, state, 1) - - -@with_capella_and_later -@spec_state_test -def test_success_two_partial_withdrawable(spec, state): - set_validator_partially_withdrawable(spec, state, 0) - set_validator_partially_withdrawable(spec, state, 1) - - yield from run_process_partial_withdrawals(spec, state, 2) - - -@with_capella_and_later -@spec_state_test -def test_success_max_partial_withdrawable(spec, state): - # Sanity check that this test works for this state - assert len(state.validators) >= spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH - - for i in range(spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH): - set_validator_partially_withdrawable(spec, state, i) - - yield from run_process_partial_withdrawals(spec, state, spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH) - - -@with_capella_and_later -@with_presets([MINIMAL], reason="not enough validators with mainnet config") -@spec_state_test -def test_success_max_plus_one_withdrawable(spec, state): - # Sanity check that this test works for this state - assert len(state.validators) >= spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH + 1 - - # More than MAX_PARTIAL_WITHDRAWALS_PER_EPOCH partially withdrawable - for i in range(spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH + 1): - set_validator_partially_withdrawable(spec, state, i) - - # Should only have MAX_PARTIAL_WITHDRAWALS_PER_EPOCH withdrawals created - yield from run_process_partial_withdrawals(spec, state, spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH) - - -def run_random_partial_withdrawals_test(spec, state, rng): - for _ in range(rng.randint(0, 2)): - next_epoch(spec, state) - randomize_state(spec, state, rng) - - num_validators = len(state.validators) - state.next_partial_withdrawal_validator_index = rng.randint(0, num_validators - 1) - - num_partially_withdrawable = rng.randint(0, num_validators - 1) - partially_withdrawable_indices = rng.sample(range(num_validators), num_partially_withdrawable) - for index in partially_withdrawable_indices: - set_validator_partially_withdrawable(spec, state, index, excess_balance=rng.randint(1, 1000000000)) - - # Note: due to the randomness and other epoch processing, some of these set as "partially withdrawable" - # may not be partially withdrawable once we get to ``process_partial_withdrawals``, - # thus *not* using the optional third param in this call - yield from run_process_partial_withdrawals(spec, state) - - -@with_capella_and_later -@spec_state_test -def test_random_0(spec, state): - yield from run_random_partial_withdrawals_test(spec, state, random.Random(0)) - - -@with_capella_and_later -@spec_state_test -def test_random_1(spec, state): - yield from run_random_partial_withdrawals_test(spec, state, random.Random(1)) - - -@with_capella_and_later -@spec_state_test -def test_random_2(spec, state): - yield from run_random_partial_withdrawals_test(spec, state, random.Random(2)) - - -@with_capella_and_later -@spec_state_test -def test_random_3(spec, state): - yield from run_random_partial_withdrawals_test(spec, state, random.Random(3)) - - -@with_capella_and_later -@spec_state_test -def test_random_4(spec, state): - yield from run_random_partial_withdrawals_test(spec, state, random.Random(4)) - - -@with_capella_and_later -@spec_state_test -def test_random_5(spec, state): - yield from run_random_partial_withdrawals_test(spec, state, random.Random(5)) diff --git a/tests/core/pyspec/eth2spec/test/capella/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/capella/sanity/test_blocks.py index 28c20a2cd..f3ad843b1 100644 --- a/tests/core/pyspec/eth2spec/test/capella/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/capella/sanity/test_blocks.py @@ -6,12 +6,17 @@ from eth2spec.test.helpers.state import ( state_transition_and_sign_block, ) from eth2spec.test.helpers.block import ( - build_empty_block_for_next_slot, build_empty_block, + build_empty_block_for_next_slot, + build_empty_block, ) from eth2spec.test.helpers.bls_to_execution_changes import get_signed_address_change +from eth2spec.test.helpers.state import ( + next_slot, +) from eth2spec.test.helpers.withdrawals import ( set_validator_fully_withdrawable, set_validator_partially_withdrawable, + prepare_expected_withdrawals, ) from eth2spec.test.helpers.voluntary_exits import prepare_signed_exits @@ -45,6 +50,8 @@ def test_full_withdrawal_in_epoch_transition(spec, state): index = 0 current_epoch = spec.get_current_epoch(state) set_validator_fully_withdrawable(spec, state, index, current_epoch) + assert len(spec.get_expected_withdrawals(state)) == 1 + yield 'pre', state # trigger epoch transition @@ -55,6 +62,7 @@ def test_full_withdrawal_in_epoch_transition(spec, state): yield 'post', state assert state.balances[index] == 0 + assert len(spec.get_expected_withdrawals(state)) == 0 @with_capella_and_later @@ -63,7 +71,8 @@ def test_partial_withdrawal_in_epoch_transition(spec, state): index = state.next_withdrawal_index set_validator_partially_withdrawable(spec, state, index, excess_balance=1000000000000) pre_balance = state.balances[index] - pre_withdrawal_queue_len = len(state.withdrawal_queue) + + assert len(spec.get_expected_withdrawals(state)) == 1 yield 'pre', state @@ -77,21 +86,19 @@ def test_partial_withdrawal_in_epoch_transition(spec, state): assert state.balances[index] < pre_balance # Potentially less than due to sync committee penalty assert state.balances[index] <= spec.MAX_EFFECTIVE_BALANCE - # Withdrawal is processed within the context of the block so queue empty - assert len(state.withdrawal_queue) == pre_withdrawal_queue_len + assert len(spec.get_expected_withdrawals(state)) == 0 @with_capella_and_later @spec_state_test def test_many_partial_withdrawals_in_epoch_transition(spec, state): assert len(state.validators) > spec.MAX_WITHDRAWALS_PER_PAYLOAD - assert spec.MAX_PARTIAL_WITHDRAWALS_PER_EPOCH > spec.MAX_WITHDRAWALS_PER_PAYLOAD for i in range(spec.MAX_WITHDRAWALS_PER_PAYLOAD + 1): index = (i + state.next_withdrawal_index) % len(state.validators) set_validator_partially_withdrawable(spec, state, index, excess_balance=1000000000000) - pre_withdrawal_queue_len = len(state.withdrawal_queue) + assert len(spec.get_expected_withdrawals(state)) == spec.MAX_WITHDRAWALS_PER_PAYLOAD yield 'pre', state @@ -102,8 +109,7 @@ def test_many_partial_withdrawals_in_epoch_transition(spec, state): yield 'blocks', [signed_block] yield 'post', state - # All new partial withdrawals processed except 1 - assert len(state.withdrawal_queue) == pre_withdrawal_queue_len + 1 + assert len(spec.get_expected_withdrawals(state)) == 1 @with_capella_and_later @@ -133,3 +139,69 @@ def test_exit_and_bls_change(spec, state): assert not spec.is_fully_withdrawable_validator(validator, balance, current_epoch) assert validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH assert spec.is_fully_withdrawable_validator(validator, balance, validator.withdrawable_epoch) + + +def _perform_valid_withdrawal(spec, state): + fully_withdrawable_indices, partial_withdrawals_indices = prepare_expected_withdrawals( + spec, state, num_partial_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4, + num_full_withdrawals=spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + pre_next_withdrawal_index = state.next_withdrawal_index + + expected_withdrawals = spec.get_expected_withdrawals(state) + + pre_state = state.copy() + + # Block 1 + block = build_empty_block_for_next_slot(spec, state) + signed_block_1 = state_transition_and_sign_block(spec, state, block) + + withdrawn_indices = [withdrawal.validator_index for withdrawal in expected_withdrawals] + fully_withdrawable_indices = list(set(fully_withdrawable_indices).difference(set(withdrawn_indices))) + partial_withdrawals_indices = list(set(partial_withdrawals_indices).difference(set(withdrawn_indices))) + assert state.next_withdrawal_index == pre_next_withdrawal_index + spec.MAX_WITHDRAWALS_PER_PAYLOAD + + withdrawn_indices = [withdrawal.validator_index for withdrawal in expected_withdrawals] + fully_withdrawable_indices = list(set(fully_withdrawable_indices).difference(set(withdrawn_indices))) + partial_withdrawals_indices = list(set(partial_withdrawals_indices).difference(set(withdrawn_indices))) + assert state.next_withdrawal_index == pre_next_withdrawal_index + spec.MAX_WITHDRAWALS_PER_PAYLOAD + + return pre_state, signed_block_1, pre_next_withdrawal_index + + +@with_capella_and_later +@spec_state_test +def test_withdrawal_success_two_blocks(spec, state): + pre_state, signed_block_1, pre_next_withdrawal_index = _perform_valid_withdrawal(spec, state) + + yield 'pre', pre_state + + # Block 2 + block = build_empty_block_for_next_slot(spec, state) + signed_block_2 = state_transition_and_sign_block(spec, state, block) + + assert state.next_withdrawal_index == pre_next_withdrawal_index + spec.MAX_WITHDRAWALS_PER_PAYLOAD * 2 + + yield 'blocks', [signed_block_1, signed_block_2] + yield 'post', state + + +@with_capella_and_later +@spec_state_test +def test_withdrawal_fail_second_block_payload_isnt_compatible(spec, state): + _perform_valid_withdrawal(spec, state) + + # Block 2 + block = build_empty_block_for_next_slot(spec, state) + + # Modify state.next_withdrawal_index to incorrect number + state.next_withdrawal_index += 1 + + # Only need to output the state transition of signed_block_2 + yield 'pre', state + + signed_block_2 = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield 'blocks', [signed_block_2] + yield 'post', None diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 83162e1c2..93befaf5f 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -30,8 +30,7 @@ def build_empty_execution_payload(spec, state, randao_mix=None): transactions=empty_txs, ) if is_post_capella(spec): - num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawal_queue)) - payload.withdrawals = state.withdrawal_queue[:num_withdrawals] + payload.withdrawals = spec.get_expected_withdrawals(state) # TODO: real RLP + block hash logic would be nice, requires RLP and keccak256 dependency however. payload.block_hash = spec.Hash32(spec.hash(payload.hash_tree_root() + b"FAKE RLP HASH")) diff --git a/tests/core/pyspec/eth2spec/test/helpers/withdrawals.py b/tests/core/pyspec/eth2spec/test/helpers/withdrawals.py index 526ac0caa..35349839b 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/withdrawals.py +++ b/tests/core/pyspec/eth2spec/test/helpers/withdrawals.py @@ -1,3 +1,6 @@ +import random + + def set_validator_fully_withdrawable(spec, state, index, withdrawable_epoch=None): if withdrawable_epoch is None: withdrawable_epoch = spec.get_current_epoch(state) @@ -29,3 +32,19 @@ def set_validator_partially_withdrawable(spec, state, index, excess_balance=1000 validator = state.validators[index] assert spec.is_partially_withdrawable_validator(validator, state.balances[index]) + + +def prepare_expected_withdrawals(spec, state, + num_full_withdrawals=0, num_partial_withdrawals=0, rng=random.Random(5566)): + assert num_full_withdrawals + num_partial_withdrawals <= len(state.validators) + all_validator_indices = list(range(len(state.validators))) + sampled_indices = rng.sample(all_validator_indices, num_full_withdrawals + num_partial_withdrawals) + fully_withdrawable_indices = rng.sample(sampled_indices, num_full_withdrawals) + partial_withdrawals_indices = list(set(sampled_indices).difference(set(fully_withdrawable_indices))) + + for index in fully_withdrawable_indices: + set_validator_fully_withdrawable(spec, state, index) + for index in partial_withdrawals_indices: + set_validator_partially_withdrawable(spec, state, index) + + return fully_withdrawable_indices, partial_withdrawals_indices diff --git a/tests/generators/epoch_processing/main.py b/tests/generators/epoch_processing/main.py index 946a6c2c0..fc57fbe4e 100644 --- a/tests/generators/epoch_processing/main.py +++ b/tests/generators/epoch_processing/main.py @@ -27,11 +27,9 @@ if __name__ == "__main__": # so no additional tests required. bellatrix_mods = altair_mods - _new_capella_mods = {key: 'eth2spec.test.capella.epoch_processing.test_process_' + key for key in [ - 'full_withdrawals', - 'partial_withdrawals', - ]} - capella_mods = combine_mods(_new_capella_mods, altair_mods) + # No epoch-processing changes in Capella and previous testing repeats with new types, + # so no additional tests required. + capella_mods = bellatrix_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 [