# 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-) [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). ## 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.