Part 2: Technical Overview
Deposits and Withdrawals
- The consensus layer commits to the state of the deposit contract after an 8 hour delay, with a 2048 slot voting period.
- The delay and voting are no longer necessary post-Merge and may be removed in future.
- When a new deposit root is voted in, proposers must include deposits in blocks.
- The block proposer makes an inclusion proof of the deposit against the contract's deposit root that all nodes can verify.
- Deposits for new public keys create new validator records.
- Deposits for existing public keys top up validators' balances.
The previous section on the deposit contract covered how deposits are handled on the execution layer. Now we shall look at how they get handed off to the consensus layer where the business of staking actually happens. A (valid) deposit into the execution layer deposit contract will either create a new validator on the consensus layer, or top up the balance of an existing validator.
There are two ways in which deposit information is transferred over from the execution layer to the consensus layer. One is the voting process by which the consensus layer comes to agreement on the state of the deposit contract at a particular execution block height. The other is validators directly importing deposit receipts from their attached Eth1 clients, which they will include in blocks and use to maintain their own copies of the deposit Merkle tree.
Both of these mechanisms are somewhat legacy, post-Merge, although still in place for now. There is a proposal to overhaul the whole consensus layer deposit handling workflow at some point, in the form of EIP-6110.
As mentioned above, Eth1 voting is a legacy of the pre-Merge era of the consensus layer. It is the means by which the beacon chain comes to agreement on a common view of the Ethereum 1.0 chain, and in particular, a common view of the state of the deposit contract. Post-Merge, execution payloads are included in beacon blocks, and by definition all correct beacon nodes now have a common view of the Eth1 chain.
The consensus layer's common view of the deposit contract is formed by a majority vote of beacon block proposers over a repeating cycle of 2048 slots (about 6.8 hours, see
EPOCHS_PER_ETH1_VOTING_PERIOD). Each proposer includes in its block an
Eth1Data vote as follows.
class Eth1Data(Container): deposit_root: Root deposit_count: uint64 block_hash: Hash32
The last field,
block_hash, identifies a particular block on the execution chain. The
deposit_count fields are set by calling the deposit contract's
get_deposit_root() method at that block. The consensus client does this via normal JSON RPC
eth_call invocations on the execution client.
During block processing, the beacon chain's
process_eth1_data() function counts up the votes for each instance of Eth1Data seen in the current period. The first set of Eth1Data to be supported by more than 1024 validators (more than half of the period's block proposers) is adopted by immediately updating
state.eth1_data. If no Eth1Data vote reaches the threshold during the voting period, then
state.eth1_data is not updated. A fresh voting period begins only when the earlier one has run its full course of 2048 slots, even if new Eth1Data was voted in early.
Block proposers choose their Eth1 votes as described in the honest validator guide. Here's a summary of the process. We set to be the wall clock time of the start of the current voting period. The comes from
ETH1_FOLLOW_DISTANCE (2048 Eth1 blocks) multiplied by
SECONDS_PER_ETH1_BLOCK (set to 14 as an approximate average value under proof of work)1.
- First, ask the Eth1 client for all the Eth1 blocks with timestamps in the interval, .
- Filter out any blocks that have a deposit count less than
state.eth1_data.deposit_count: we've already seen these.
- The proposer's default vote will be the Eth1Data from last block in this period, or if the list is empty (because Eth1 has stalled), then the winning vote from the previous voting period.
- We're seeking to find agreement as quickly as possible, so an honest proposer will discard any Eth1Data that has not been voted for by other proposers already during the current period.
- Finally, the honest proposer will cast a vote for the Eth1Data that already has the greatest support in the list that remains. If that list is empty (for example, if it is the first proposer in a voting period), it casts its default vote.
This algorithm has been refined considerably over time. Anecdotally, Eth1 data voting has been the source of significant numbers of issues on testnets over the years. It seems to be difficult to get right, probably because it is difficult to test. Getting Eth1Data voting correct is also not incentivised by the protocol. Rather, it is mildly disincentivised, since on-boarding new validators dilutes existing validators' rewards. In any case, it will be good to see the whole thing gone.
This delay serves two functions. Under proof of work there was always a chance that blocks near the tip of the chain could be reorged out. It would be very bad for the beacon chain to include deposits that were later reverted - people might even try to "double spend" the consensus layer.
For all practical purposes, a delay of a few blocks would probably have been sufficient to counter this, since Ethereum under proof of work never suffered reversions longer than two or three blocks. Setting the follow distance as long as 8 hours is more about providing devs with enough time to respond if there were to be an incident on the Eth1 chain that might affect the deposit process, such as a chain split. In any case, this delay is now redundant as, post-Merge, the beacon chain and the execution chain move in lock-step.
The upshot of all of this is that the absolute minimum time interval between sending a deposit to the deposit contract and the consensus layer processing that deposit is around 11.4 hours: 8 hours due to the follow distance, and 3.4 hours being half of the voting period, the least required to get a majority vote. Assuming that voting is working well, the average time will be just over 17 hours. This doesn't include subsequent time waiting for the deposit to be included in a block, the validator sitting in the activation queue, etc.
For an in-depth analysis of the Eth1 follow distance and the Eth1 voting period length, see Mikhail Kalinin's Ethresear.ch article, On the way to Eth1 finality. Note that both the follow distance and the voting period have been doubled in length since the article was written.
Let's say that new Eth1Data has been voted in, with the
deposit_count, , replacing the previous count, , in the beacon state. This means that subsequent block proposers have fresh deposits to include in blocks.
deposit_root in the Eth1Data is the root of the deposit Merkle after deposits. Block proposers must construct proofs that deposits , , , and are included in that Merkle root: proofs of inclusion in the Merkle tree.
In order to do this, each validator maintains its own deposit Merkle tree based on the deposit receipts it has downloaded from its attached Eth1 client. To construct a proof that deposit is included in the tree, I need to have already built the tree that has all deposits. Then I can easily provide the Merkle branch from leaf to the known value of
Beacon block proposers must include all available deposits, in consecutive order, along with their Merkle proofs of inclusion, up to a maximum of
MAX_DEPOSITS. If a block fails to include all available deposits in the correct order then the entire block is invalid.
The actual data that will be included in the proposer's beacon block for each deposit is a
class Deposit(Container): proof: Vector[Bytes32, DEPOSIT_CONTRACT_TREE_DEPTH + 1] # Merkle path to deposit root data: DepositData
DepositData is as follows,
class DepositData(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 amount: Gwei signature: BLSSignature
MAX_DEPOSITS (16) of these can be included per block.
Deposits are verified by all nodes during block processing in
apply_deposit(). In addition, the check that the block contains the expected number of deposits (the lesser of
MAX_DEPOSITS and the remaining number of deposits to be processed) is done in
For each deposit, the first thing to be checked is its Merkle proof of inclusion. The verification is performed against the deposit root from the Eth1Data that was voted in. If a deposit passes the check, it proves that it was included in the deposit contract's tree at the same leaf position. If this check fails for any deposit, then the whole block is invalid.
When the deposit is for a new validator – that is, its public key does not already exist in the validator set – then the deposit's signature is verified. Signature verification proves that the public key belongs to a genuine, known secret key in the possession of the depositor. Importantly, a deposit with an invalid signature does not invalidate the whole block. It is just ignored and processing moves on. This is because the deposit contract was not able to validate the signature, so it is possible for invalid deposits to be present in its Merkle tree.
If the public key in the deposit data does not already exist in the validator registry then a new validator record is created, and the deposit amount is credited to the validator's account. The deposit amount will usually be the full 32 ETH needed to activate a validator, but need not be. Later, at the end of the epoch, the validator's effective balance will be calculated - when the effective balance first becomes 32 ETH then the validator will be queued for activation, otherwise the account will just sit there inactive until the effective balance is raised to 32 ETH via a top-up deposit.
The validator's withdrawal credentials will also be set at this point. If they are
0x01 Eth1 withdrawal credentials, then they are permanent and cannot be changed in future. If they are
0x00 BLS withdrawal credentials then they may later be changed once to
0x01 credentials. See the next section for more on this.
It is also possible to make top-up deposits for pre-existing validators. Anyone may do this for any validator. Top-up deposits have exactly the same structure as normal deposits, except that the top-up deposit's BLS signature is not checked, and the withdrawal credentials are ignored.
The minimum top-up amount is 1 ETH. One might wish to send a top-up if a validator's effective balance has dropped below the maximum of 32 ETH. Since most rewards are proportional to effective balance, such a validator will be under-performing. For example, with a 31 ETH effective balance your expected rewards will be reduced by around 3%, and topping up to maintain a 32 ETH effective balance might be worthwhile. Not many top-ups have been performed to date, but there are a some examples.
As noted earlier, it is possible to build up a validator's stake over time, with an initial deposit that's less than 32 ETH, followed by one or more top-up deposits. The validator will become active when its effective balance reaches 32 ETH. However, if you plan to do this, watch out for a tricky edge case involving hysteresis when the final top-up is 1 ETH.
Largely of historic interest now, Mikhail Kalinin's article On the way to Eth1 finality is an exemplary analysis of the deposit bridge from the Eth1 to Eth2.
The relevant spec functions and data structures for deposits are as follows.
- The deposit contract.
apply_deposit(), all part of block processing.
- Eth1 data handling in the honest validator guide.
- This is actually 28,672 seconds, but 8 hours is close enough for explanatory purposes. What's 128 seconds between friends?↩