--- tags: eips, withdrawals, research, ethereum --- # Partial withdrawals ## About Add new message type for the [`generalized message bus`](https://hackmd.io/@lido/BkiOdwcmK). This message type lets to partially withdraw from the balance of validator without exiting. Such method allows to remove additional load from the entry/exit queues associated with receiving the rewards accumulated by validators. ## Why - Partial withdrawals would be useful to remove a potential clog from the entry/exit queue due a routine validator operations; [discussion here](https://ethresear.ch/t/exit-entry-queue-clogging-after-withdrawals-are-enabled/10400) - The withdrawal credential holder might want to withdraw staking rewards over effective balance to compound, and should have an option to send a message to initiate partial withdrawals. ## The anatomy of the process The partial withdrawal for the specific validator is initialized through a contract call on the execution layer side. The call is transmitted to the consensus layer via GMB. Sender authentication checks are performed on the consensus layer side. #### The execution layer side An event is generated on the execution layer side to transmit a message with data for partial withdrawal. Separate types of messages are implemented for each type of withdrawal prefix: ```solidity // BLS withdrawal prefix struct PartialWithdrawal0x00MessageData { uint64 validatorIndex; bytes withdrawalCredProof; } ``` where * `uint64 validatorIndex`: the index of target validator * `bytes withdrawalCredProof`: the bls signature produced by a withdrawal cred private key for authorization of the sender ```solidity // Eth1 address withdrawal prefix struct PartialWithdrawal0x01MessageData { uint64 validatorIndex; address sender; } ``` where * `uint64 validatorIndex`: the index of target validator * `address sender`: the eth1 address of withdrawal credentials target; this field is equal to `msg.sender` for a call of `submit` A emitting of message on the execution layer does not have any validation steps. It only emits an event: ```solidity uint256 constant PARTIAL_WITHDRAWALS_0x00_TYPE = 11; uint256 constant PARTIAL_WITHDRAWALS_0x01_TYPE = 12; function submit0x00(uint64 validatorIndex, bytes memory withdrawalCredProof) external { PartialWithdrawal0x00MessageData memory data = PartialWithdrawal0x00MessageData( validatorIndex, withdrawalCredProof ); bytes memory payload = abi.encode(data); emit MessageEvent(Message(PARTIAL_WITHDRAWALS_0x00_TYPE, payload)); } function submit0x01(uint64 validatorIndex) external { PartialWithdrawal0x01MessageData memory data = PartialWithdrawal0x01MessageData( validatorIndex, msg.sender ); bytes memory payload = abi.encode(data); emit MessageEvent(Message(PARTIAL_WITHDRAWALS_0x01_TYPE, payload)); } ``` #### The consensus layer side Update the existed data structure with new properties and add new constants: ```python PARTIAL_WITHDAWALS_PER_EPOCH = 32 DOMAIN_PARTIAL_WITHDRAWALS = DomainType('0x07000000') class Validator: ... partial_withdrawal_epoch: int class BeaconState(Container): ... partial_withdrawals_amount: int ``` Add new messages types for the `GMB` part: ```python class MessageBusType(enum): ... PartialWithdrawals0x00: int = 11 PartialWithdrawals0x01: int = 12 ``` Partial withdrawals should be able only for validators with some accumulated amount of rewards above 32 eth. It protects the partial withdrawals mechanism from a spam messages flooding. ```python MINIMAL_COLLECTED_REWARD = 0.5 ether ``` Verify a bls signature for the 0x00 type or withdrawal target for 0x01 the type, check amount of collected rewards and submited partial withdrawals for the current epoch: ```python def process_messages( state: BeaconState, messages: List[MessageBusEvent] ) -> None: for msg in messages: if msg.message_type == ...: ... elif msg.message_type == MessageBusType.PartialWithdrawals0x00: process_partial_withdrawal_message_0x00(state, msg.data) elif msg.message_type == MessageBusType.PartialWithdrawals0x01: process_partial_withdrawal_message_0x01(state, msg.data) ... def decode_partial_withdrawal0x00_message_payload( data: bytes ) -> Tuple[ValidatorIndex, BLSSignature]: val_index, bls_signature = abi.decode(data) return val_index, bls_signature def process_partial_withdrawal_message_0x00( state: BeaconState, data: bytes ) -> None: ( validator_index, bls_signature ) = decode_partial_withdrawal0x00_message_payload( data ) validator = state.validators[validator_index] if validator.withdrawal_credentials[1] != BLS_WITHDRAWAL_PREFIX: return bls_pubkey = validator.withdrawal_credentials[1:] domain = get_domain(state, DOMAIN_PARTIAL_WITHDRAWALS) signing_root = compute_signing_root(validator_index, domain) assert bls.Verify(bls_pubkey, signing_root, bls_signature) process_partial_withdrawal_message(state, validator_index) def decode_partial_withdrawal0x01_message_payload( data: bytes ) -> Tuple[ValidatorIndex, AddressT]: val_index, sender = abi.decode(data) return val_index, sender def process_partial_withdrawal_message_0x01( state: BeaconState, data: bytes ) -> None: validator_index, sender = decode_partial_withdrawal0x01_message_payload( data ) validator = state.validators[validator_index] if validator.withdrawal_credentials[1] != ETH1_ADDRESS_WITHDRAWAL_PREFIX: return assert validator.withdrawal_credentials[12:] == sender process_partial_withdrawal_message(state, validator_index) def process_partial_withdrawal_message( state: BeaconState, validator_index: ValidatorIndex ) -> None: balance = state.balances[validator_index] if ( balance > MAX_EFFECTIVE_BALANCE + MINIMAL_COLLECTED_REWARD and state.partially_withdrawals_amount < PARTIAL_WITHDRAWALS_PER_EPOCH ): state.partially_withdrawals_amount += 1 validator = state.validators[validator_index] validator.partial_withdrawal_epoch = get_current_epoch(state) + 1 ``` The limit for processing messages per one epoch is protection against an overutilization of the partial withdrawal mechanism: ```python def process_epoch(state: BeaconState) -> None: ... process_partially_withdrawals(state) ... def process_partially_withdrawals(state: BeaconState) -> None: state.partially_withdrawals_amount = 0 current_epoch = get_current_epoch(state) for validator_index in get_active_validator_indices( state, current_epoch ): validator = state.validators[validator_index] if validator.partial_withdrawal_epoch == current_epoch: if validator.withdrawal_credentials[1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX: # yet unspecified mechanism # to send pw to respective # withdrawal credential's control ... ``` ### Option: unauthenticated partial withdrawals As partial withdrawals do not seem to be able to bring harm withdrawal credential holder or a validator, there is an option to make partial withdrawals unauthenticated. The only issue we see with this is the fact it can have effect on staker's tax reports, and authentification is cheap enough to implement just to limit potential application-layer gotchas and yet unforeseen side effects. ### Security consideration This proposal opens up the vector for DoS attacks through a partial witdrawals flood. For protection against this threat we limit the total amount of processed messages per one epoch and allow to be processed only validators with balances more than `MAX_EFFECTIVE_BALANCE` + some threshold. Note that using balance threshold limits the possible DoS effect by itself: if chosen high enough, it will take multiple months to prepare enough validators to fire the message in unison to have any chance at flooding beacon chain. Realistically, the only time period when this can happen is when partial withdrawals are first enabled. | Threat | Mitigation(s) | | -------- | -------- | | An attacker is able to utilize a partial withdrawal for DoS attacks through flooding. | 1. Add the limit of processed partial withdrawals per one epoch <br> 2. Reject requests for validators without collected rewards. |