# Deposit processing post-Merge Main downsides of existing `Eth1Data` poll are as follows: * Huge time gap between submitting a deposit transaction and corresponding validator activation * Implementation complexity of existing solution * Prone to liveness failures These features are dictated by necessity of maintaining a bridge between two disjoint blockchains which is no longer the case post-Merge. This opens up an opportunity to improve on deposit processing flow. ## Improvement proposal ### Execution Layer EL is responsible for surfacing receipts of deposit transactions and including them into an execution block. In particular, EL is collecting [`DepositEvents`](https://github.com/ethereum/consensus-specs/blob/dev/solidity_deposit_contract/deposit_contract.sol#L19) from transactions calling to deposit contract deployed in the network after transaction execution and either adds them to the block structure (when building a block) or runs necessery validations over the list of events. Computational cost of this addition should be marginal as EL does already make a pass over receipts to obtain `receipts_root`. Execution block header is equipped with `deposits_root` field (a root of a list of deposit operations, akin to `withdrawals_root` in the [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895)). Execution block body is equipped with the new `deposits` list containing elements of the following type: ```python= class Deposit(Container): pubkey: Bytes48 withdrawal_credentials: Bytes32 amount: uint64 signature: Bytes96 index: uint64 ``` When validating a block, EL runs two additional checks over `deposits_root`: * `deposits_root` value in a block header must be equal to the root of deposits list obtained from the the block execution * `deposits_root` must be consistent with `deposits` list of the corresponding block body Address of the deposit contract becomes a network configuration parameter on EL side. ### Consensus Layer The following structures, methods and fields become deprecated: * `Eth1Data` * `Deposit` * `process_eth1_data` * `process_eth1_data_reset` * `process_deposit` * `BeaconBlockBody.deposits` * `BeaconState.eth1_deposit_index` #### `ExecutionPayload` ```python= class DepositReceipt(Container): pubkey: BLSPubkey withdrawal_credentials: Bytes32 amount: Gwei signature: BLSSignature index: uint64 class ExecutionPayload(bellatrix.ExecutionPayload): deposits: List[DepositReceipt, MAX_TRANSACTIONS_PER_PAYLOAD] ``` #### `BeaconState` ```python= class BeaconState(bellatrix.BeaconState): pending_deposits: List[DepositReceipt, MAX_PENDING_DEPOSITS] ``` #### `process_pending_deposit` ```python= def get_validator_from_deposit(deposit: DepositReceipt) -> Validator: # Modified function body ... def process_pending_deposit(state: BeaconState, deposit: DepositReceipt) -> None: pubkey = deposit.pubkey amount = deposit.amount validator_pubkeys = [v.pubkey for v in state.validators] if pubkey not in validator_pubkeys: # Verify the deposit signature (proof of possession) which is not checked by the deposit contract deposit_message = DepositMessage( pubkey=deposit.pubkey, withdrawal_credentials=deposit.withdrawal_credentials, amount=deposit.amount, ) domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks signing_root = compute_signing_root(deposit_message, domain) if not bls.Verify(pubkey, signing_root, deposit.signature): return # Add validator and balance entries state.validators.append(get_validator_from_deposit(deposit)) state.balances.append(amount) else: # Increase balance by deposit amount index = ValidatorIndex(validator_pubkeys.index(pubkey)) increase_balance(state, index, amount) ``` #### `process_block` ```python= def process_deposit_receipt(state: BeaconState, deposit: DepositReceipt) -> None: state.pending_deposits.append(deposit) def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: # Verify that outstanding deposits are processed up to the maximum number of deposits assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index) def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: for operation in operations: fn(state, operation) for_ops(body.proposer_slashings, process_proposer_slashing) for_ops(body.attester_slashings, process_attester_slashing) for_ops(body.attestations, process_attestation) for_ops(body.deposits, process_deposit) # Used by transition logic for_ops(body.execution_payload.deposits, process_deposit_receipt) # New in Deposits for_ops(body.voluntary_exits, process_voluntary_exit) def process_pending_deposits(state: BeaconState, block: BeaconBlock) -> None: # We may not cap deposit processing at all # It's done for compatibility with the existing solution # Note: this scheme doesn't require merkle proof validation # which reduces computation complexity for deposit in state.pending_deposits[:MAX_DEPOSITS]: process_pending_deposit(state, deposit) state.pending_deposits = state.pending_deposits[MAX_DEPOSITS:] def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) process_randao(state, block.body) process_eth1_data(state, block.body) # Used by transition logic process_pending_deposits(state) # New in Deposits process_operations(state, block.body) ``` ### Transition During the period of transition from the old `Eth1Data` poll mechanism to the new one clients will have to run two machineries in parallel until the moment in time when `Eth1Data` poll period overlaps with the a span of blocks containing the new `deposits` list. #### `process_eth1_data` ```python= def process_eth1_data(state: BeaconState, body: BeaconBlockBody) -> None: # Stop voting on Eth1Data block = get_beacon_block_by_execution_block_hash(state.eth1_data.block_hash) if compute_epoch_at_slot(block.slot) >= DEPOSITS_FORK_EPOCH: return state.eth1_data_votes.append(body.eth1_data) if state.eth1_data_votes.count(body.eth1_data) * 2 > EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH: state.eth1_data = body.eth1_data ``` #### `process_pending_deposits` ```python= def process_pending_deposits(state: BeaconState, block: BeaconBlock) -> None: # Wait for the last Eth1Data poll to happen block = get_beacon_block_by_execution_block_hash(state.eth1_data.block_hash) if compute_epoch_at_slot(block.slot) < DEPOSITS_FORK_EPOCH: return # Wait for an old deposit queue to drain up if state.eth1_deposit_index < state.eth1_data.deposit_count: return # Filter overlapped deposit span out of the queue state.pending_deposits = [d for d in state.pending_deposits if d.index >= state.eth1_deposit_index] # We may not cap deposit processing at all # It's done for compatibility with the existing solution # Note: this scheme doesn't require merkle proof validation # which reduces computation complexity for deposit in state.pending_deposits[:MAX_DEPOSITS]: process_pending_deposit(state, deposit) state.pending_deposits = state.pending_deposits[MAX_DEPOSITS:] ``` ### Data complexity Given that no more merkle proof is required to process a deposit, data complexity per deposit is reduced roughly by `1Kb`. With a cost of deposit transaction roughly equal to 60k gas and gas limit at 30m gas, a block may have 500 deposit operations at max. This is `192*500 = ~94Kb` (`~47Kb` at a target block size) per block vs `1240*16 = ~19Kb` per block today. Alternatively, deposit queue may be moved from CL state to EL state to rate limit a number of deposits per block. There are ~410k deposits made until today. The size of overall deposit data would be `~75Mb`. ### Validators' pubkey cache The cache is used by all CL clients to optimise deposit processing flow and in other places. Recent entries of the pubkey cache will have to be invalidated upon re-org as a validator `(index, pubkey)` pair is fork dependent. ### Pros * No deposit cache * No `Eth1Data` and voting * Deposits are processed in the next block ### Cons * Data complexity increase on EL side * Special case transition logic ### Why not reuse eth1data machinery Another path was explored to reuse the `eth1data` machinery as much as possible. In this path, the block's `eth1data` vote would be passed to EL in the engine API as part of the EL validations. A goal of this path is also to reduce to voting period and follow distance. In this investigation we realized that although this would work in theory, in practice it no allow us to reduce the follow distance from the current amount. This is because CL clients maintain a deposit-cache for block production, and this deposit-cache, today, assumes no re-orgs at the depth of the cache. If we reduced the follow distance significantly (e.g. to 1 slot), it would put the deposit-cache in the zone of re-orgs and require significant reengineering on an already error prone component of CL client.