EIP-4895: Beacon chain withdrawals as system-level operations

the index starts at 0 and increments by 1 for each withdrawal.

the withdrawals are automatically made at the consensus layer where the logic to do this is enforced.

withdrawal indices will never duplicate – so you couldn’t get into the [0,1,2] then [0,1] situation you describe.

you have no (direct) control over the index so we will never see uint64.maxNumber - 1

This should be indicated in the specification. Right now it only says:

  • a monotonically increasing index as a uint64 value that uniquely identifies each withdrawal

It should be updated to include that this number starts at 0 for the very first withdraw, and each withdraw after that has an index that is 1 more than the previous withdraw.

It may also be valuable to note that this index is global, not per-block.

Yes, this was exactly my point, since indeed monotonically increasing does not mean that it +1s for each withdrawal, it can increase with any number (integer). And you are right, it should also specify that it should start at 0.

Just to verify: this monotonically increasing the withdrawal indices which start at 0 should not be checked by the execution layer, because it is enforced by the consensus layer?

EDIT: other question: to compute the withdrawals root, I assume we put (index, withdrawalRLP) in the trie, where withdrawalRLP is the RLPd version of the withdrawal in question?

I have started implementing this in EthereumJS: Implement EIP4895: Beacon Chain withdrawals by jochem-brouwer · Pull Request #2353 · ethereumjs/ethereumjs-monorepo · GitHub

because it is enforced by the consensus layer?

correct, the EL only needs to apply the balance update – the content has already been verified by the CL.

I assume we put (index, withdrawalRLP) in the trie, where withdrawalRLP is the RLPd version of the withdrawal in question?

yep

1 Like

I have added some extra context around the index value here: Update EIP-4895: `validator_index` and more info around `index` by ralexstokes · Pull Request #5835 · ethereum/EIPs · GitHub

The proposed withdrawal_root does not match the one that is currently included in the consensus specs ExecutionPayloadHeader.

In the CL:

withdrawals_root = hash_tree_root([SSZ.encode(Withdrawal), ...])

In the proposed EIP:

withdrawals_root = hexary_trie_root([RLP.encode(Withdrawal), ...])

Given that withdrawals_root and the Withdrawal structure are new additions, I’m wondering if it would be possible to use a consistent tree style across the two specs.

This would enable the EL block header to be derived from the CL ExecutionPayloadHeader, enabling light clients that are subscribed to the CL light clients gossip to follow the chain without requiring any additional network requests. Network requests would only be needed when encountering a block of interest via logs_bloom, or when requesting a state or transaction inclusion proof.

I have explored three other approaches to solve this inconsistency across EL and CL root formats for light clients.

  1. Including hexary transactions_root and withdrawals_root into the ExecutionPayload
    • This would require extensions to the BeaconBlock and BeaconState, hence is more invasive than it should be, just to make life easier for light clients.
    • May interfere with future changes to the trie format (statelessness / verkle tries).
  2. Have the CL implementation re-compute hexary transactions_root and withdrawals_root from the beacon block data when providing light client data.
    • This would require CL implementations to add software for hexary trie computation. Given that SSZ tends to be seen as more modern than hexary tries, it would be a step into the wrong direction.
    • May interfere with split block storage designs, because the CL would no longer have the raw transactions and withdrawals that are needed to compute the hexary trie root hashes. The CL may need to fetch that info via eth_getBlockByRoot(..., includeTransactions: false), and that call may compete with resources needed for ongoing validator duties or fork choice processing.
  3. Only provide the other ExecutionPayloadHeader fields to the light client, omitting the hexary transactions_root and withdrawals_root.
    • This would require light clients to issue a second network request to obtain those hexary trie roots, which are needed to validate inclusion of transactions. It would be great if light clients could follow the chain without additional network requests to individual peers, just by following gossip on light client topics (or the event stream from REST).
    • May introduce complexity by having not only full blocks and block headers, but also a partial block headers concept.

Hence, the request here, whether withdrawals_root and Withdrawals could be modernized with a change to SSZ. SSZ libraries should already be available for all programming languages used by major EL implementations. Going forward, new roots should become consistent across both the EL and CL.

Note that transactions_root is the other blocker currently preventing to convert ExecutionPayloadHeader to an EL block header. The situation is different, though, because the individual transactions are also RLP encoded in the CL. So, only the hexary transactions_root would need to be converted to a SSZ root, but not the individual transactions. If that is not feasible given the long-term existence of this field, the ExecutionPayload could be extended with the hexary transactions_root as a one-time exception.

I have read a lot of different things about the staking mechanism, but I’m still a little bit confused about the lifecycle.
As I understand it it has the following steps/operations

  1. create staking and validator depositing 32 ETH
  2. set an eth 1 address for withdrawals
  3. begin to retrieve rewards through the EIP-4895 mechanism (march 2023)
  4. signal a voluntary stake exit
  5. retrieve the 32 ETH staken

So my questions/confusions are

  • Is this a fair representation of the lifecycle
  • As I understand the eth 1 address at 2) is for both 3) and 5) and can only be a Externally Owned Address not a Contract Address (as no EVM). It this correct?
  • is 4) supported now or when will it be and how do you do it, is there a EIP for that?
  • as i understand EIP-4895 the amounts are added directly to the eth1 address 2), “out of thin air”
    Is that true for both 3) and 5). If so, what happens with the 32 ETH from 2) in deposit_contract.sol

Is this a fair representation of the lifecycle

ignoring many details, yes :slight_smile:

As I understand the eth 1 address at 2) is for both 3) and 5)

yes

can only be a Externally Owned Address not a Contract Address (as no EVM). It this correct?

no, the execution layer address can be any valid address, so either an EOA or a smart contract. the mechanism of EIP-4895 only increments the balance so one caveat is that if you are expecting any kind of execution from the smart contract, that simply won’t happen (so no logs, or ability to update internal smart contract state)

is 4) supported now

yes

Is that true for both 3) and 5)

yes

what happens with the 32 ETH from 2) in deposit_contract.sol

nothing. the ETH in this contract should be considered to be ‘burnt’ and/or taken out of supply. there is no way to access them any more

Many thanks for the response :slight_smile:

Can you tell me where this function 4) is described

the consensus client you are running likely has docs for how to create a signed voluntary exit which must be broadcast to the network

for example, here are the prysm docs: Exit your validator | Prysm

I’d suggest going to your consensus client’s discord for further help there

Since we are handling a large number of validators, we are thinking about having multiple validators using the same withdrawal address, for speed and cost reasons of the further handling of funds. However that introduces a potential accounting traceability problem.
Will it be possible to somehow enumerate through all the

execution_payload.withdrawals   ...  List of [index, validator_index, address, amount]

specified in EIP-4895 performed on the execution layer, to trace the specific transactions per validator ?

yes, you can just scan each block which has distinct withdrawals

Block subsidy is not done by all relayes FWIW

for the staking steps

Is it correct understod that

  1. cost normal smart contract gas as it is done on the execution layer
  2. and 4. is send directly to the beacon chain using a validator and does not cost anything (gas etc)
  3. and 5. is send as extra “free” payload on the execution block and does not cost any gas

If you have 10.000 validators that you want to either set an address 2) or signal an exit 4)
How fast would that be able to execute ?

Hey @ralexstokes! Are validator gas tips included in the automatic reward push on top of the 32 ETH or are they withdrawn in a different way? Also, can rewards and principal be withdrawn simultaneously? Thanks!

“gas tips” would fall into execution layer rewards and those go to the feeRecipient set in each block

entirely separately, validators get rewards for various duties performed at the consensus layer and those rewards are subject to withdrawals via this EIP-4895 mechanism

you can withdraw “rewards and principal” at the same time if you have exited the validator and it becomes withdrawable

Hey @ralexstokes I appreciate it!

Do partial withdrawals (where the remaining principal after the withdrawal is greater than 16 ETH) require the validator to be exited? If not, is this an immediate withdrawal to the beacon chain?

I was also wondering if the withdrawal operation will require the validator key and the withdrawal key or just the withdrawal key? Thanks!

Question (maybe naive).
With the recipient address on the Withdrawal object being any ETH valid address (including smart contracts), could this affect applications on the EVM assuming that the contract should never have an ETH balance?
Maybe it is just that because of SELFDESTRUCT this should simply not be assumed, but to me looks like something pretty important and new, if working like that.