Merge pull request #3068 from potuz/withdrawals_without_queues

Withdrawals without queues
This commit is contained in:
Danny Ryan 2022-11-10 10:49:25 -07:00 committed by GitHub
commit 37996fc36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 762 additions and 655 deletions

View File

@ -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

View File

@ -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

View File

@ -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`

View File

@ -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

View File

@ -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`.

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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"))

View File

@ -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

View File

@ -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 [