# Eth2 to Eth1 Validator's withdrawal design [WIP]
Currently Ethereum 2 supports deposits with the only option, deposits from Ethereum 1 and there are no options for validator's stack or its part to be withdrawn. We want to add an option for Ethereum 1 withdrawals following validator exit as the simplest approach for validator owners to get their deposit back with rewards. No option for partial withdrawal is suggested due to higher complexity and lower safety of possible solutions and minor losses on reentry which should take about a day. Our highest priority is to make deposit-withdrawal processes handled with maximum level of autonomy and separation of validator and funder's roles to make possible creation of shared owned validators. Concept of Ethereum shared stacking pool [is created](https://github.com/zilm13/withdrawal-simulation) according to the specification and currently in testing.
Withdrawal implementation requires a set of changes in both ETH2 BeaconChain and ETH1.
Contents:
[1. Changes in BeaconChain](#Changes-in-BeaconChain-)
[1.1 Withdrawal credentials for Eth1 withdrawal](#Withdrawal-credentials-for-Eth1-withdrawal-)
[1.1.1 Message for changing withdrawal credentials](#Message-for-changing-withdrawal-credentials-)
[1.2 Withdrawals](#Withdrawals-)
[1.3 Withdrawal processing](#Withdrawal-processing-)
[1.4 JSON-RPC update](#JSON-RPC-update-)
[1.5 Partial withdrawals](#Partial-withdrawals-)
[1.6 Queues](#Queues-)
[2. Changes in ETH1](#Changes-in-ETH1-)
[2.1 Changes to opcodes](#Changes-to-opcodes-)
[2.2 System contract in Eth1](#System-contract-in-Eth1-)
[3. Discussion](#Discussion-)
**Warning**: This document is under development
## Changes in BeaconChain [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
### Withdrawal credentials for Eth1 withdrawal [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
Proposed by @djrtwo in [eth2.0-specs#2149](https://github.com/ethereum/eth2.0-specs/pull/2149), approved and merged. Adds `0x01`, `ETH1_ADDRESS_WITHDRAWAL_PREFIX` prefix for `withdrawal_credentials` field of `Deposit` and thereafter `Validator`, setting withdrawal target to specific ETH1 address without any possibility to change withdrawal credentials in future to different target including other ETH1 addresses as no specific public key is left for withdrawal for such deposits.
#### Message for changing withdrawal credentials [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
Proposed in [eth2.0-specs#2213](https://github.com/ethereum/eth2.0-specs/issues/2213), discussion. Message **BLSSetWithdrawal** could be issued and processed one time for each validator. It can't change one BLS withdrawal credentials to another. It can only change BLS creds to non-BLS creds. So, it will include change to the already approved ETH1 address withdrawal target right from the box.
### Withdrawals [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
We define `Withdrawal` object:
```python=
class Withdrawal(Container):
validator_index: ValidatorIndex
withdrawal_credentials: Bytes32
withdrawn_epoch: Epoch
amount: Gwei
```
We expect that `Validator`'s `pubkey` couldn't be reused after `Validator`'s exit and information about slashing and withdrawal epoch could be obtained from appropriate `Validator` object. Next, add `Withdrawal` objects list or similar structure to `BeaconState`.
```python=
class BeaconState(Container):
# [...]
# Registry
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
# Withdrawals
withdrawals: List[Withdrawal, WITHDRAWAL_REGISTRY_LIMIT]
# Randomness
# [...]
```
and define list limits:
| Name | Value | Unit | Duration |
| - | - | :-: | :-: |
| `WITHDRAWAL_REGISTRY_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals |
### Withdrawal processing [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
We have two options for withdrawal initiation:
- **Withdrawal message.** In this case it should be signed by `Validator` as withdrawals with ETH1 `withdrawal_credentials` already don't have a dedicated withdrawal key. On one side it simplifies withdrawal processing doing it only on request, but it's not clear whether it's `Validator`'s responsibility, as validator key owner could block withdrawal to any period of time with such ability.
- **Automatical processing.** We could check validator after exit if it's eligible for withdrawal processing and perform it. If not, keep it unchanged until withdrawal is possible. Looks better but there is a room for mistakes.
Therefore we choose automatic processing but it should conform to the following properties:
- no `Validator` could be processed to withdrawal 2 times successfully, no double withdrawal
- if not processed successfully, `Validator` record should remain unchanged to be processed in future
In the near term second case means that `Validator` with `withdrawal_credentials` starting with `BLS_WITHDRAWAL_PREFIX` after exit remains unchanged without successful withdrawal, but if `withdrawal_credentials` are changed to one with `ETH1_ADDRESS_WITHDRAWAL_PREFIX` using appropriate [message](#Message-for-changing-withdrawal-credentials-) it should be reprocessed after that, as withdrawal becomes possible.
We've started this approach with boolean flag, but later tend to particular epoch number to omit field duplication in `Validator` and `Withdrawal`. We update `Validator` to change withdrawn epoch from `FAR_FUTURE_EPOCH` after successful `Withdrawal` creation:
```python=
class Validator(Container):
pubkey: BLSPubkey
withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals
effective_balance: Gwei # Balance at stake
slashed: boolean
# Status epochs
activation_eligibility_epoch: Epoch # When criteria for activation were met
activation_epoch: Epoch
exit_epoch: Epoch
withdrawable_epoch: Epoch # When validator can withdraw funds
withdrawn_epoch: Epoch # When balance was withdrawn
```
We set `withdrawn_epoch` to 0 by default in order to compliment partial rewards withdrawals in future. There are several cases where even withdrawn validators could be updated, for example, in deposit processing, which should be avoided. All specifications should be checked and protected from this possibility.
Generally, we define withdrawal processing in the following manner:
```python=
def process_withdrawal(prefix: Byte1):
if prefix == ETH1_ADDRESS_WITHDRAWAL_PREFIX:
return process_eth1_withdrawal
else:
return lambda state, index: None
def process_withdrawals(state: BeaconState) -> None:
epoch = get_current_epoch(state)
eligible_validators = [
(i, v) for i, v in enumerate(state.validators)
if v.withdrawable_epoch <= epoch and v.withdrawn_epoch < v.withdrawable_epoch]
for (i, v) in eligible_validators:
process_withdrawal(Byte1(v.withdrawal_credentials[:1]))(state, i)
def process_eth1_withdrawal(state: BeaconState, index: ValidatorIndex) -> None
validator = state.validators[index]
epoch = get_current_epoch(state)
assert validator.withdrawable_epoch <= epoch
assert ETH1_ADDRESS_WITHDRAWAL_PREFIX == Byte1(validator.withdrawal_credentials[:1])
withdrawal = Withdrawal(
validator_index: index,
withdrawal_credentials: validator.withdrawal_credentials,
withdrawn_epoch: epoch,
amount: state.balances[index]
)
state.withdrawals.append(withdrawal)
state.validators[index].effective_balance = 0
state.validators[index].withdrawn_epoch = epoch
state.balances[index] = 0
```
### JSON-RPC update [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
In order to get proofs for Withdrawals, we should add an appropriate JSON-RPC method, so clients will be able to get all required data. Let's call it
```yaml=
'/eth/v1/beacon/states/{state_id}/withdrawal':
get:
operationId: getWithdrawal
summary: Get withdrawal
description: Retrieves withdrawal with provided pubkey hash.
tags:
- Beacon
parameters:
- name: state_id
in: path
required: true
example: head
schema:
type: string
description: |
State identifier.
Can be one of: "head" (canonical head in node's view), "genesis", "finalized", "justified", \<slot\>, \<hex encoded stateRoot with 0x prefix\>.
- name: validator_index
in: query
required: true
description: Corresponding validator's index.
schema:
type: string
example: '1'
responses:
'200':
description: Success
content:
application/json:
schema:
title: WithdrawalWithProofResponse
type: object
properties:
data:
type: object
description: 'Withdrawal object with proofs up to BeaconBlock root'
properties:
beacon_block_root:
allOf:
- type: string
example: '0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2'
pattern: '^0x[a-fA-F0-9]{64}$'
- description: Root of BeaconBlock, proofs are constructed at.
slot:
allOf:
- type: string
example: '1'
- description: The slot to which beacon_block_root corresponds.
proof:
description: 'Proof of Withdrawal object as described in [`Merkle multiproofs`](https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/merkle-proofs.md#merkle-multiproofs)'
type: array
items:
allOf:
- type: string
example: '0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2'
pattern: '^0x[a-fA-F0-9]{64}$'
- description: Bytes32
index:
allOf:
- type: string
example: '1'
- description: '[`Generalized index`](https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/merkle-proofs.md#generalized-merkle-tree-index) of Withdrawal object'
withdrawal:
type: object
description: 'Withdrawal object'
properties:
validator_index:
allOf:
- type: string
example: '1'
- description: validator index
withdrawal_credentials:
allOf:
- type: string
example: '0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2'
pattern: '^0x[a-fA-F0-9]{64}$'
- description: Withdrawal credentials of Validator
withdrawn_epoch:
allOf:
- type: string
example: '1'
- description: epoch of withdrawal
amount:
allOf:
- type: string
example: '1'
- description: amount of withdrawal in GWei
'400':
description: The state ID supplied could not be parsed
content:
application/json:
schema:
allOf:
- type: object
properties:
code:
description: Either specific error code in case of invalid request or http status code
type: number
example: 404
message:
description: Message describing error
type: string
stacktraces:
description: 'Optional stacktraces, sent when node is in debug mode'
type: array
items:
type: string
- example:
code: 400
message: 'Invalid state ID: current'
'404':
description: Either state ID or withdrawal not found
content:
application/json:
schema:
allOf:
- type: object
properties:
code:
description: Either specific error code in case of invalid request or http status code
type: number
example: 404
message:
description: Message describing error
type: string
stacktraces:
description: 'Optional stacktraces, sent when node is in debug mode'
type: array
items:
type: string
- example:
code: 404
message: State ID or withdrawal not found
'500':
description: Beacon node internal error.
content:
application/json:
schema:
type: object
properties:
code:
description: Either specific error code in case of invalid request or http status code
type: number
example: 404
message:
description: Message describing error
type: string
stacktraces:
description: 'Optional stacktraces, sent when node is in debug mode'
type: array
items:
type: string
example:
code: 500
message: Internal server error
```
Example implementation, for general proof methods check [Merkle proof formats](https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/merkle-proofs.md) spec:
```python=
def get_withdrawal(state: BeaconState, block: BeaconBlock, validator_index: ValidatorIndex):
assert state.hash_tree_root() == block.state_root
withdrawals = [
(i, w) for i, w in enumerate(state.withdrawals)
if w.validator_index == validator_index]
if withdrawals:
index = withdrawals[-1][0]
withdrawal = withdrawals[-1][1]
state_index = get_generalized_index(BeaconBlock, ['state_root'])
withdrawal_index = get_generalized_index(BeaconState, ['withdrawals', index])
return {
'root': block.hash_tree_root(),
'slot': block.slot,
'proof': [block.get_backing().getter(index).merkle_root() for index in get_helper_indices([state_index])] + [state.get_backing().getter(index).merkle_root() for index in get_helper_indices([withdrawal_index])],
'index': concat_generalized_indices(state_index, withdrawal_index),
'withdrawal': withdrawal
}
else:
return None
```
### Partial withdrawals [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
Let's check how difficult it could be to add partial withdrawals at the top of a full withdrawal scheme. Ability to process partial withdrawals should be examined, but there is a big demand for their implementation.
New operation and signed message is proposed for Eth2, `BLSWithdrawRewards`, which could inquire for rewards withdrawal, it requires `Validator`'s BLS withdrawal credentials:
```python
class BLSWithdrawRewards(Container):
withdrawal_credentials: Bytes32 # Withdrawal credentials for reward withdrawal
withdrawal_epoch: Epoch # Epoch at which message should be processed
validator_index: ValidatorIndex
```
And `SignedBLSWithdrawRewards` message:
```python
class SignedBLSWithdrawRewards(Container):
message: BLSWithdrawRewards
signature: BLSSignature # Signed by withdrawal BLS key
```
Domain:
| Name | Value |
| - | - |
| `DOMAIN_REWARDS_WITHDRAWAL` | `DomainType('0x07000000')` |
And withdrawal delay to be sure we are not spammed with withdrawal messages:
TODO: why this delay??
| Name | Value | Unit | Duration |
| - | - | :-: | :-: |
| `MIN_VALIDATOR_REWARDS_WITHDRAWAL_DELAY` | `uint64(2**14)` (= 16,384) | epochs | ~73 days
To initiate rewards withdrawal, `Validator` sends appropriate `SignedBLSWithdrawRewards` message. Next we have processing like this:
```python=
def process_rewards_withdrawal(state: BeaconState, signed_reward_withdrawal: SignedBLSWithdrawRewards) -> None:
"""
Processing SignedBLSWithdrawRewards with rewards withdrawal order
"""
rewards_withdrawal = signed_reward_withdrawal.message
validator = state.validators[rewards_withdrawal.validator_index]
# Verify the validator is active
assert is_active_validator(validator, get_current_epoch(state))
# Verify exit has not been initiated
assert validator.exit_epoch == FAR_FUTURE_EPOCH
# Withdrawals must specify an epoch when they become valid; they are not valid before then
assert get_current_epoch(state) >= rewards_withdrawal.withdrawal_epoch
# Verify the validator has been active long enough
assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
# Verify signature
domain = get_domain(state, DOMAIN_REWARDS_WITHDRAWAL, rewards_withdrawal.withdrawal_epoch)
signing_root = compute_signing_root(rewards_withdrawal, domain)
assert bls.Verify(validator.pubkey, signing_root, signed_rewards_withdrawal.signature)
# Initiate rewards withdrawal
initiate_rewards_withdrawal(state, rewards_withdrawal.validator_index)
def initiate_rewards_withdrawal(state: BeaconState, message: BLSWithdrawRewards) -> None:
"""
Initiate rewards withdrawal for the validator with index ``index``.
"""
# Return if validator already initiated exit
validator = state.validators[message.validator_index]
if validator.exit_epoch != FAR_FUTURE_EPOCH:
return
# Compute rewards value
rewards_value = state.balances[message.validator_index] - MAX_EFFECTIVE_BALANCE
if rewards_value <= 0:
return
# Check that there were no withdrawals recently
if validator.withdrawn_epoch + MIN_VALIDATOR_REWARDS_WITHDRAWAL_DELAY < get_current_epoch(state):
return
# Process partial withdrawal
process_validator_rewards_withdrawal(Byte1(message.withdrawal_credentials[:1]))(state, message)
def process_validator_rewards_withdrawal(prefix: Byte1):
"""
Returns rewards withdrawal function for provided withdrawal_credentials prefix
"""
if prefix == ETH1_ADDRESS_WITHDRAWAL_PREFIX:
return process_eth1_rewards_withdrawal
else:
return lambda state, message: None
def process_eth1_rewards_withdrawal(state: BeaconState, message: BLSWithdrawRewards) -> None:
"""
Process rewards withdrawal for withdrawal_credentials with Eth1 prefix
"""
index = message.validator_index
validator = state.validators[index]
epoch = get_current_epoch(state)
# Assert it's Eth1 withdawal
assert ETH1_ADDRESS_WITHDRAWAL_PREFIX == Byte1(message.withdrawal_credentials[:1])
# Assert we have something to withdraw
withdrawal_value = state.balances[index] - MAX_EFFECTIVE_BALANCE
assert withdrawal_value > 0
# Make withdrawal receipt and add it to withdrawals
withdrawal = Withdrawal(
validator_index: index,
withdrawal_credentials: message.withdrawal_credentials,
withdrawn_epoch: epoch,
amount: withdrawal_value
)
state.withdrawals.append(withdrawal)
# Update balance and withdrawn epoch
state.validators[index].withdrawn_epoch = epoch
state.balances[index] = MAX_EFFECTIVE_BALANCE
```
There are several uncertainties in this implementation, especially with dual usage of `withdrawn_epoch` field, which is used both for exit withdrawals and rewards withdrawals. It could be solved with 2 fields and extra dedicated `rewards_withdrawn_epoch` field to make both types of withdrawal less tricky.
The main downside of this implementation is inability to process rewards withdrawals for `Validators` with non-BLS prefixed `withdrawal_credentials`, necessity to sign messages with BLS withdrawal key, which is difficult to be shared and inability to restrict withdrawal targets in this case. Most of these disadvantages could be solved with implementation of several `Validator`'s keys as initially proposed by @vbuterin in [Adding PoS validator key changes](https://ethresear.ch/t/adding-pos-validator-key-changes/9264) (see "Control of key switching rights" section).
### Queues [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
TODO
Withdrawals by validator resign or non-automatic partial withdrawals requires message for each action. All messages are bounded by processing queues limits. [Estimates](https://ethresear.ch/t/exit-entry-queue-clogging-after-withdrawals-are-enabled/10400) predicts about 10 months of re-entrance queues after withdrawals are enabled, which is a serious issue and should be avoided. So, withdrawals design should be modified to avoid queues both in regular flow and decrease estimated queues or make a special case on withdrawals enable fork.
## Changes in ETH1 [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
### Changes to opcodes [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
We require only `BEACONBLOCKROOT` opcode for withdrawal operation.
`beaconblockroot(<Beacon block/slot number>)` returns root of appropriate `BeaconChain` block or `0x00*32` if data is not available.
### System contract in Eth1 [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
At address `0x00……ff` (TO BE DEFINED), add a system contract that expects input in the following format:
```
RLP(withdrawal: Withdrawal, proof: [Bytes32], index: UInt64, slot: UInt64)
```
After execution contract pushes the result to the stack, one of:
```
0x00..00 - FAILED
0x00..01 - SUCCESSFUL
0x00..02 - ALREADY_WITHDRAWN
```
System contract in Eth1 should consists of 3 parts:
- verification of proof
- saving withdrawal claim
- ether mint
Verification of proof should look something like this (example in python language, from [Merkle Proof formats](https://github.com/ethereum/eth2.0-specs/blob/dev/ssz/merkle-proofs.md)):
```python=
def verify_withdrawal(withdrawal: Withdrawal, proof: Sequence[Bytes32], index: GeneralizedIndex, slot: UInt64) -> bool:
return verify_merkle_proof(hash_tree_root(withdrawal), proof, index) == op_beacon_block_root(slot)
def verify_merkle_proof(leaf: Bytes32, proof: Sequence[Bytes32], index: GeneralizedIndex, root: Root) -> bool:
return calculate_merkle_root(leaf, proof, index) == root
def calculate_merkle_root(leaf: Bytes32, proof: Sequence[Bytes32], index: GeneralizedIndex) -> Root:
assert len(proof) == get_generalized_index_length(index)
for i, h in enumerate(proof):
if get_generalized_index_bit(index, i):
leaf = hash(h + leaf)
else:
leaf = hash(leaf + h)
return leaf
def get_generalized_index_length(index: GeneralizedIndex) -> int:
"""
Return the length of a path represented by a generalized index.
"""
return int(log2(index))
def get_generalized_index_bit(index: GeneralizedIndex, position: int) -> bool:
"""
Return the given bit of a generalized index.
"""
return (index & (1 << position)) > 0
```
After that we should save in contract storage consumption of this withdrawal receipt, so it couldn't be claimed anymore. We use `withdrawal_hash_tree_root = hash_tree_root(withdrawal)` for unique reference:
```solidity=
//** Solidity code **//
// Definition
mapping(bytes32 => bool) withdrawn;
// Assertion at the beginning of withdrawal processing
require(!withdrawn[withdrawal_hash_tree_root]);
// Successful withdrawal claim saved
withdrawn[withdrawal_hash_tree_root] = true;
```
and after that we "mint ether" like we do it for coinbase rewards ([trinity](https://github.com/ethereum/trinity) codebase example):
```python=
def mint_for_withdrawal(withdrawal: Withdrawal) -> None:
withdrawal_target = Address(decode_hex(withdrawal.withdrawal_credentials[12:]))
withdrawal_amount = withdrawal.amount * denoms.gwei
self.vm_state.account_db.delta_balance(withdrawal_target, withdrawal_amount)
```
## Discussion [^](#Eth2-to-Eth1-Validator’s-withdrawal-design-WIP)
There are number of design limitations if we want to use BeaconChain (ETH2) data in Execution Layer (ETH1), which were reviewed making this specification:
- we could not expect tight coupling of BeaconChain and Execution Layer clients in the near term and from architecture viewpoint we should avoid it
- most VM opcodes are executed in nanosecond/microsecond times, and we should reduce this period, no JSON-RPC or similar kind of interactions could fit in it
- any interaction between several pieces of software leads to sync choice and issues
It leads us to the safest way of interaction: provide some piece of data (several KBs max) with every query on block construction. Check [Rayonism merge specification#Consensus JSON RPC](https://notes.ethereum.org/@n0ble/rayonism-the-merge-spec#Consensus-JSON-RPC) for preliminary proposal on ETH2<->ETH1 JSON-RPC interaction protocol. `BeaconState` does not fit in it, `BeaconState` changes require stateful interaction. Even several parts of `BeaconState` are way too big for it. What we should definitely fit in it is `BeaconState` root, or, more precisely `BeaconBlock` root to formalize `BeaconState` view momentum. For more on ETH1<->ETH2 interaction, check [Executable Beacon Chain](https://ethresear.ch/t/executable-beacon-chain/8271) by @mkalinin.
Also there are several topics which should be solved in future but not yet covered in this specification:
- **Burn deposits**
In order to make correct deposit processing when putting money in Eth2 and later out of, we should adjust `Deposit` contract balance. We could do it either when `Deposit` is processed and money arrives in `BeaconChain`, or when cashout is processed. Burning it after processing looks like a better way as we could add several deposit and withdrawal options in the future and ethers do not actually exist in Eth1 after deposit is processed. Finally, we could have a penalty which will burn the whole validator’s stack in the future and the validator will not be able to make withdrawal at all, so the second option looks less viable.
- **Withdrawal list purge**
With the growing number of withdrawals `BeaconState` will grow over time. We could purge old withdrawals not affecting proof generation by collapsing old tree paths, the only caveat is that we could not (and should not) use data whether this withdrawal was already claimed or not, instead we are able only to define age or list quantity expiration thresholds. It should not be an issue for anyone as these limits could be defined per client and if someone wants to keep all withdrawals since the network establishing he would have no issues with it and any historical withdrawal proof will be legit at any time.
- **Withdrawal versioning**
Later we may want to introduce partial withdrawals or `Validator`'s balance withdrawal will require additional fields, for example, for L2 withdrawals. It will require changes in `Withdrawal` object and handling of all versions in all participating parties including Eth1 system contract. Appropriate mechanism should be developed.
- **Partial withdrawals**
With such withdrawal claim design we could have partial withdrawals with a few changes on top.
- **Good to have Eth1 features (useful for contract development in connection with Eth2)**
+ `bls.verify`, using [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537) or similar. It will help contracts to verify Eth2 credentials.
+ system contract providing utilities with Eth2 constants and support (ie updating and following all forks). Example: seconds to epochs calculation.