Skip to content

Commit

Permalink
base design for partial withdrawals
Browse files Browse the repository at this point in the history
  • Loading branch information
djrtwo committed Mar 24, 2022
1 parent 4ac4158 commit f5c967f
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 3 deletions.
55 changes: 52 additions & 3 deletions specs/capella/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ 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 |
Expand Down Expand Up @@ -245,7 +251,8 @@ class BeaconState(Container):
# Execution
latest_execution_payload_header: ExecutionPayloadHeader
# Withdrawals
withdrawal_index: WithdrawalIndex
withdrawal_index: WithdrawalIndex # [New in Capella]
next_partial_withdrawal_index: ValidatorIndex # [New in Capella]
withdrawals_queue: List[Withdrawal, WITHDRAWALS_QUEUE_LIMIT] # [New in Capella]
```

Expand All @@ -262,7 +269,7 @@ def withdraw_balance(state: BeaconState, index: ValidatorIndex, amount: Gwei) ->
# Create a corresponding withdrawal receipt
withdrawal = Withdrawal(
index=state.withdrawal_index,
address=state.validators[index].withdrawal_credentials[12:],
address=ExecutionAddress(state.validators[index].withdrawal_credentials[12:]),
amount=amount,
)
state.withdrawal_index = WithdrawalIndex(state.withdrawal_index + 1)
Expand All @@ -282,6 +289,19 @@ def is_fully_withdrawable_validator(validator: Validator, epoch: Epoch) -> bool:
return is_eth1_withdrawal_prefix 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.
"""
is_eth1_withdrawal_prefix = validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX
has_max_effective_balance = validator.effective_balance == MAX_EFFECTIVE_BALANCE
has_excess_balance = balance > MAX_EFFECTIVE_BALANCE
return is_eth1_withdrawal_prefix and has_max_effective_balance and has_excess_balance
```

## Beacon chain state transition function

### Epoch processing
Expand All @@ -301,9 +321,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.

Expand All @@ -317,6 +339,33 @@ 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:
# consider it being a function of the validator set size

partial_withdrawals_count = 0
# Begin where we left off last time
validator_index = state.next_partial_withdrawal_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, ValidatorIndex(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_index = validator_index
```

### Block processing

```python
Expand Down
1 change: 1 addition & 0 deletions specs/capella/fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def upgrade_to_capella(pre: bellatrix.BeaconState) -> BeaconState:
latest_execution_payload_header=pre.latest_execution_payload_header,
# Withdrawals
withdrawal_index=WithdrawalIndex(0),
next_partial_withdrawal_index=ValidatorIndex(0),
withdrawals_queue=[],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from eth2spec.test.context import (
with_capella_and_later,
spec_state_test,
)
from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with


def set_validator_partiall_withdrawable(spec, state, index):
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 + 500 # make a random increase

assert spec.partially_withdrawable_indices(validator, state.balances[index])


def run_process_partial_withdrawals(spec, state, num_expected_withdrawals=None):
pre_withdrawal_index = state.withdrawal_index
pre_withdrawals_queue = state.withdrawals_queue
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 from run_epoch_processing_with(spec, state, 'process_partial_withdrawals')

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.withdrawals_queue) == len(pre_withdrawals_queue) + num_expected_withdrawals
assert state.withdrawal_index == pre_withdrawal_index + num_expected_withdrawals


@with_capella_and_later
@spec_state_test
def test_no_partial_withdrawals(spec, state):
pre_validators = state.validators.copy()
yield from run_process_partial_withdrawals(spec, state, 0)

assert pre_validators == state.validators

0 comments on commit f5c967f

Please sign in to comment.