diff --git a/presets/mainnet/capella.yaml b/presets/mainnet/capella.yaml index c5dfe1d4b8..c3eedc5c1e 100644 --- a/presets/mainnet/capella.yaml +++ b/presets/mainnet/capella.yaml @@ -1 +1,24 @@ -# Minimal preset - Capella +# 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 +WITHDRAWALS_QUEUE_LIMIT: 1099511627776 + + +# Max operations per block +# --------------------------------------------------------------- +# 2**4 (= 16) +MAX_BLS_TO_EXECUTION_CHANGES: 16 + + +# Execution +# --------------------------------------------------------------- +# 2**4 (= 16) withdrawals +MAX_WITHDRAWALS_PER_PAYLOAD: 16 diff --git a/presets/minimal/capella.yaml b/presets/minimal/capella.yaml index c5dfe1d4b8..a3c5e8f1db 100644 --- a/presets/minimal/capella.yaml +++ b/presets/minimal/capella.yaml @@ -1 +1,24 @@ # 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 +WITHDRAWALS_QUEUE_LIMIT: 1099511627776 + + +# Max operations per block +# --------------------------------------------------------------- +# 2**4 (= 16) +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: 16 diff --git a/specs/capella/beacon-chain.md b/specs/capella/beacon-chain.md index f940b3a273..da67a8fcb6 100644 --- a/specs/capella/beacon-chain.md +++ b/specs/capella/beacon-chain.md @@ -11,6 +11,7 @@ - [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) @@ -30,10 +31,13 @@ - [Beacon state mutators](#beacon-state-mutators) - [`withdraw`](#withdraw) - [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) - - [Withdrawals](#withdrawals) + - [Full withdrawals](#full-withdrawals) + - [Partial withdrawals](#partial-withdrawals) - [Block processing](#block-processing) - [New `process_withdrawals`](#new-process_withdrawals) - [Modified `process_execution_payload`](#modified-process_execution_payload) @@ -48,7 +52,8 @@ Capella is a consensus-layer upgrade containing a number of features related to validator withdrawals. Including: * Automatic withdrawals of `withdrawable` validators -* Partial withdrawals during block proposal +* Partial withdrawals sweep for validators with 0x01 withdrawal + credentials and balances in exceess of `MAX_EFFECTIVE_BALANCE` * Operation to change from `BLS_WITHDRAWAL_PREFIX` to `ETH1_ADDRESS_WITHDRAWAL_PREFIX` versioned withdrawal credentials to enable withdrawals for a validator @@ -70,11 +75,17 @@ 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 | Duration | | - | - | :-: | :-: | -| `WITHDRAWALS_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state| +| `WITHDRAWAL_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state| ### Max operations per block @@ -245,8 +256,9 @@ class BeaconState(Container): # Execution latest_execution_payload_header: ExecutionPayloadHeader # Withdrawals - withdrawal_index: WithdrawalIndex - withdrawals_queue: List[Withdrawal, WITHDRAWALS_QUEUE_LIMIT] # [New in Capella] + 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] ``` ## Helpers @@ -261,16 +273,26 @@ def withdraw_balance(state: BeaconState, index: ValidatorIndex, amount: Gwei) -> decrease_balance(state, index, amount) # Create a corresponding withdrawal receipt withdrawal = Withdrawal( - index=state.withdrawal_index, - address=state.validators[index].withdrawal_credentials[12:], + index=state.next_withdrawal_index, + address=ExecutionAddress(state.validators[index].withdrawal_credentials[12:]), amount=amount, ) - state.withdrawal_index = WithdrawalIndex(state.withdrawal_index + 1) - state.withdrawals_queue.append(withdrawal) + state.next_withdrawal_index = WithdrawalIndex(state.next_withdrawal_index + 1) + state.withdrawal_queue.append(withdrawal) ``` ### Predicates +#### `has_eth1_withdrawal_credential` + +```python +def has_eth1_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has an 0x01 prefixed "eth1" withdrawal credential + """ + return validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX +``` + #### `is_fully_withdrawable_validator` ```python @@ -278,8 +300,22 @@ def is_fully_withdrawable_validator(validator: Validator, epoch: Epoch) -> bool: """ Check if ``validator`` is fully withdrawable. """ - is_eth1_withdrawal_prefix = validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX - return is_eth1_withdrawal_prefix and validator.withdrawable_epoch <= epoch < validator.fully_withdrawn_epoch + return ( + has_eth1_withdrawal_credential(validator) + and validator.withdrawable_epoch <= epoch < validator.fully_withdrawn_epoch + ) +``` + +#### `is_partially_withdrawable_validator` + +```python +def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: + """ + Check if ``validator`` is partially withdrawable. + """ + has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE + has_excess_balance = balance > MAX_EFFECTIVE_BALANCE + return has_eth1_withdrawal_credential(validator) and has_max_effective_balance and has_excess_balance ``` ## Beacon chain state transition function @@ -301,9 +337,11 @@ def process_epoch(state: BeaconState) -> None: process_participation_flag_updates(state) process_sync_committee_updates(state) process_full_withdrawals(state) # [New in Capella] + process_partial_withdrawals(state) # [New in Capella] + ``` -#### Withdrawals +#### Full withdrawals *Note*: The function `process_full_withdrawals` is new. @@ -317,6 +355,31 @@ def process_full_withdrawals(state: BeaconState) -> None: validator.fully_withdrawn_epoch = current_epoch ``` +#### 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 @@ -335,15 +398,15 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: ```python def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: - num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue)) - dequeued_withdrawals = state.withdrawals_queue[:num_withdrawals] + num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawal_queue)) + dequeued_withdrawals = state.withdrawal_queue[:num_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.withdrawals_queue = state.withdrawals_queue[num_withdrawals:] + state.withdrawal_queue = state.withdrawal_queue[num_withdrawals:] ``` #### Modified `process_execution_payload` diff --git a/specs/capella/fork.md b/specs/capella/fork.md index 5f015a4ff2..0d8283e569 100644 --- a/specs/capella/fork.md +++ b/specs/capella/fork.md @@ -89,8 +89,9 @@ def upgrade_to_capella(pre: bellatrix.BeaconState) -> BeaconState: # Execution-layer latest_execution_payload_header=pre.latest_execution_payload_header, # Withdrawals - withdrawal_index=WithdrawalIndex(0), - withdrawals_queue=[], + withdrawal_queue=[], + next_withdrawal_index=WithdrawalIndex(0), + next_partial_withdrawal_validator_index=ValidatorIndex(0), ) for pre_validator in pre.validators: diff --git a/specs/capella/validator.md b/specs/capella/validator.md index 8c6c860a38..85dbd7e00b 100644 --- a/specs/capella/validator.md +++ b/specs/capella/validator.md @@ -61,8 +61,8 @@ helper `get_expected_withdrawals`) and passed into the `ExecutionEngine` within ```python def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: - num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue)) - return state.withdrawals_queue[:num_withdrawals] + num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawal_queue)) + return state.withdrawal_queue[:num_withdrawals] ``` *Note*: The only change made to `prepare_execution_payload` is to call 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 204816c994..26ace24b3a 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 @@ -7,8 +7,8 @@ from eth2spec.test.helpers.state import next_slot -def prepare_withdrawals_queue(spec, state, num_withdrawals): - pre_queue_len = len(state.withdrawals_queue) +def prepare_withdrawal_queue(spec, state, num_withdrawals): + pre_queue_len = len(state.withdrawal_queue) for i in range(num_withdrawals): withdrawal = spec.Withdrawal( @@ -16,9 +16,9 @@ def prepare_withdrawals_queue(spec, state, num_withdrawals): address=b'\x42' * 20, amount=200000 + i, ) - state.withdrawals_queue.append(withdrawal) + state.withdrawal_queue.append(withdrawal) - assert len(state.withdrawals_queue) == num_withdrawals + pre_queue_len + assert len(state.withdrawal_queue) == num_withdrawals + pre_queue_len def run_withdrawals_processing(spec, state, execution_payload, valid=True): @@ -30,8 +30,8 @@ def run_withdrawals_processing(spec, state, execution_payload, valid=True): If ``valid == False``, run expecting ``AssertionError`` """ - pre_withdrawals_queue = state.withdrawals_queue.copy() - num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(pre_withdrawals_queue)) + pre_withdrawal_queue = state.withdrawal_queue.copy() + num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(pre_withdrawal_queue)) yield 'pre', state yield 'execution_payload', execution_payload @@ -45,18 +45,18 @@ def run_withdrawals_processing(spec, state, execution_payload, valid=True): yield 'post', state - if len(pre_withdrawals_queue) == 0: - assert len(state.withdrawals_queue) == 0 - elif len(pre_withdrawals_queue) <= num_withdrawals: - assert len(state.withdrawals_queue) == 0 + 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.withdrawals_queue == pre_withdrawals_queue[num_withdrawals:] + assert state.withdrawal_queue == pre_withdrawal_queue[num_withdrawals:] @with_capella_and_later @spec_state_test def test_success_empty_queue(spec, state): - assert len(state.withdrawals_queue) == 0 + assert len(state.withdrawal_queue) == 0 next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -67,7 +67,7 @@ def test_success_empty_queue(spec, state): @with_capella_and_later @spec_state_test def test_success_one_in_queue(spec, state): - prepare_withdrawals_queue(spec, state, 1) + prepare_withdrawal_queue(spec, state, 1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -78,7 +78,7 @@ def test_success_one_in_queue(spec, state): @with_capella_and_later @spec_state_test def test_success_max_per_slot_in_queue(spec, state): - prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) + prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -89,7 +89,7 @@ def test_success_max_per_slot_in_queue(spec, state): @with_capella_and_later @spec_state_test def test_success_a_lot_in_queue(spec, state): - prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -104,7 +104,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.withdrawals_queue) == 0 + assert len(state.withdrawal_queue) == 0 next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -121,7 +121,7 @@ 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_withdrawals_queue(spec, state, 1) + prepare_withdrawal_queue(spec, state, 1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -133,7 +133,7 @@ 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_withdrawals_queue(spec, state, 1) + prepare_withdrawal_queue(spec, state, 1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -145,7 +145,7 @@ 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_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) + prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -157,7 +157,7 @@ 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_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -173,7 +173,7 @@ 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_withdrawals_queue(spec, state, 1) + prepare_withdrawal_queue(spec, state, 1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -185,7 +185,7 @@ def test_fail_incorrect_dequeue_index(spec, state): @with_capella_and_later @spec_state_test def test_fail_incorrect_dequeue_address(spec, state): - prepare_withdrawals_queue(spec, state, 1) + prepare_withdrawal_queue(spec, state, 1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -197,7 +197,7 @@ def test_fail_incorrect_dequeue_address(spec, state): @with_capella_and_later @spec_state_test def test_fail_incorrect_dequeue_amount(spec, state): - prepare_withdrawals_queue(spec, state, 1) + prepare_withdrawal_queue(spec, state, 1) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -209,7 +209,7 @@ 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_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) @@ -227,7 +227,7 @@ 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_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + prepare_withdrawal_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) next_slot(spec, state) execution_payload = build_empty_execution_payload(spec, state) 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 index 305f6e1baa..1498666bb6 100644 --- 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 @@ -17,8 +17,8 @@ def set_validator_withdrawable(spec, state, index, withdrawable_epoch=None): def run_process_full_withdrawals(spec, state, num_expected_withdrawals=None): - pre_withdrawal_index = state.withdrawal_index - pre_withdrawals_queue = state.withdrawals_queue + 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, spec.get_current_epoch(state)) @@ -26,6 +26,8 @@ def run_process_full_withdrawals(spec, state, num_expected_withdrawals=None): 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 from run_epoch_processing_with(spec, state, 'process_full_withdrawals') @@ -34,8 +36,8 @@ def run_process_full_withdrawals(spec, state, num_expected_withdrawals=None): assert validator.fully_withdrawn_epoch == spec.get_current_epoch(state) assert state.balances[index] == 0 - assert len(state.withdrawals_queue) == len(pre_withdrawals_queue) + num_expected_withdrawals - assert state.withdrawal_index == pre_withdrawal_index + num_expected_withdrawals + 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 @@ -65,10 +67,10 @@ def test_single_withdrawal(spec, state): # Make one validator withdrawable set_validator_withdrawable(spec, state, 0) - assert state.withdrawal_index == 0 + assert state.next_withdrawal_index == 0 yield from run_process_full_withdrawals(spec, state, 1) - assert state.withdrawal_index == 1 + assert state.next_withdrawal_index == 1 @with_capella_and_later 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 new file mode 100644 index 0000000000..bf2e73fa18 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_partial_withdrawals.py @@ -0,0 +1,224 @@ +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 + + +def set_validator_partially_withdrawable(spec, state, index, rng=random.Random(666)): + validator = state.validators[index] + validator.withdrawal_credentials = spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] + validator.effective_balance = spec.MAX_EFFECTIVE_BALANCE + state.balances[index] = spec.MAX_EFFECTIVE_BALANCE + rng.randint(1, 100000000) + + assert spec.is_partially_withdrawable_validator(validator, state.balances[index]) + + +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_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 no 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) + + # 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/helpers/epoch_processing.py b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py index 8c27480c04..e18c239f1e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py +++ b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py @@ -29,6 +29,7 @@ def get_process_calls(spec): ), 'process_sync_committee_updates', # altair 'process_full_withdrawals', # capella + 'process_partial_withdrawals', # capella # TODO: add sharding processing functions when spec stabilizes. ] diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 5e70666324..30f7a03946 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -29,8 +29,8 @@ def build_empty_execution_payload(spec, state, randao_mix=None): transactions=empty_txs, ) if spec.fork not in FORKS_BEFORE_CAPELLA: - num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue)) - payload.withdrawals = state.withdrawals_queue[:num_withdrawals] + num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawal_queue)) + payload.withdrawals = state.withdrawal_queue[:num_withdrawals] # 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"))