Partial withdrawals (#2862)

* t push base design for partial withdrawals

* moor tests

* clean up withdrawals naming

* make partial withdrawal randomized tests better

* Apply suggestions from code review

Co-authored-by: Alex Stokes <r.alex.stokes@gmail.com>
Co-authored-by: Hsiao-Wei Wang <hsiaowei.eth@gmail.com>

* fix mainnet brokn test

* name swap

* lint

Co-authored-by: Alex Stokes <r.alex.stokes@gmail.com>
Co-authored-by: Hsiao-Wei Wang <hsiaowei.eth@gmail.com>
This commit is contained in:
Danny Ryan 2022-06-08 13:16:12 -06:00 committed by GitHub
parent 1113aa608f
commit 74489d5523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 390 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,8 +7,8 @@ from eth2spec.test.context import spec_state_test, expect_assertion_error, with_
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)

View File

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

View File

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

View File

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

View File

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