# Report: `process_withdrawals` Input/Output Space *Note*: This report is AI-generated and is not an authoritative source of truth. ## Function Signature ```python def process_withdrawals(state: BeaconState) -> None: ... ``` Location: [specs/gloas/beacon-chain.md](../../../../../../../specs/gloas/beacon-chain.md) ______________________________________________________________________ ## Input Space (State Fields Read) | Field | Type | Value Space | Purpose | | -------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `state.slot` | `Slot` | `uint64` in [0, 2^64-1]. Derives `epoch = slot // SLOTS_PER_EPOCH`. Compared against `withdrawable_epoch` fields. | Compute current epoch | | `state.latest_execution_payload_bid.block_hash` | `Hash32` | `Bytes32`. **Bound to** `latest_block_hash`: must equal for function to execute (early exit if not equal). | Check if parent block was full | | `state.latest_block_hash` | `Hash32` | `Bytes32`. **Bound to** `latest_execution_payload_bid.block_hash` for `is_parent_block_full()` check. | Compare with committed bid | | `state.next_withdrawal_index` | `WithdrawalIndex` | `uint64` in [0, 2^64-1]. Monotonically increasing. Assigned to each withdrawal created. | Starting withdrawal index | | `state.next_withdrawal_validator_index` | `ValidatorIndex` | `uint64` in \[0, `len(validators)`-1\]. **Bound to** `len(validators)`: wraps via modulo. | Starting validator sweep position | | `state.next_withdrawal_builder_index` | `BuilderIndex` | `uint64` in \[0, `len(builders)`-1\]. **Bound to** `len(builders)`: wraps via modulo. | Starting builder sweep position | | `state.builders` | `List[Builder, BUILDER_REGISTRY_LIMIT]` | Length in [0, 2^40]. Separate non-validating staked actors. | Builder registry | | (in `builders`) `.pubkey` | `BLSPubkey` | `Bytes48`. Builder's public key. | Builder identification | | (in `builders`) `.execution_address` | `ExecutionAddress` | `Bytes20`. Withdrawal destination for builder sweep withdrawals. | Sweep withdrawal destination | | (in `builders`) `.balance` | `Gwei` | `uint64`. Builder's staked balance. Decreased by withdrawal amounts. | Builder balance for withdrawals | | (in `builders`) `.withdrawable_epoch` | `Epoch` | `uint64`. **Bound to** `state.slot`: builder sweep withdrawal ready when `<= current_epoch`. | Check if builder sweep eligible | | `state.builder_pending_withdrawals` | `List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT]` | Length in [0, 2^20]. Processed first. Max `MAX_WITHDRAWALS_PER_PAYLOAD - 1` can produce withdrawals. | Iterate for builder pending withdrawals | | (in `builder_pending_withdrawals`) `.builder_index` | `BuilderIndex` | `uint64` in \[0, `len(builders)`-1\]. **Index into** `builders[]`. | Identify builder in registry | | (in `builder_pending_withdrawals`) `.fee_recipient` | `ExecutionAddress` | `Bytes20`. Withdrawal destination. Independent of builder's own `execution_address`. | Withdrawal destination | | (in `builder_pending_withdrawals`) `.amount` | `Gwei` | `uint64`. Requested amount. Actual = `min(amount, builder.balance)`. | Withdrawal amount | | `state.pending_partial_withdrawals` | `List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]` | Length in [0, 2^27]. Processed second. Max `MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP` (8) processed per block. | Iterate for partial withdrawals | | (in `pending_partial_withdrawals`) `.withdrawable_epoch` | `Epoch` | `uint64`. **Bound to** `state.slot`: ready when `<= current_epoch`. If `>`, breaks processing loop. | Check if withdrawal is ready | | (in `pending_partial_withdrawals`) `.validator_index` | `ValidatorIndex` | `uint64` in \[0, `len(validators)`-1\]. **Index into** `validators[]` and `balances[]`. | Identify validator | | (in `pending_partial_withdrawals`) `.amount` | `Gwei` | `uint64`. Requested amount. Actual = `min(balance - MIN_ACTIVATION_BALANCE, amount)`. | Withdrawal amount | | `state.balances[*]` | `Gwei` | `uint64` in [0, 2^64-1]. **Same length as** `validators[]`. Must have excess over `MIN_ACTIVATION_BALANCE` (32 ETH) for partial withdrawals. | Check available balance | | `state.validators` | `List[Validator, VALIDATOR_REGISTRY_LIMIT]` | Length in [0, 2^40]. **Same length as** `balances[]`. All index references must be < len. | Validator data | | (in `validators`) `[*].effective_balance` | `Gwei` | `uint64` in [0, 2048 ETH]. **Constraint**: partial withdrawals require `>= MIN_ACTIVATION_BALANCE` (32 ETH). | Check sufficient balance | | (in `validators`) `[*].exit_epoch` | `Epoch` | `uint64`. **Constraint**: partial withdrawals require `== FAR_FUTURE_EPOCH`. If set, validator is exiting and ineligible. | Check if validator exiting | | (in `validators`) `[*].withdrawable_epoch` | `Epoch` | `uint64`. **Bound to** `state.slot` for full withdrawal eligibility. Typically `FAR_FUTURE_EPOCH` if not exiting. | For full withdrawal check | | (in `validators`) `[*].withdrawal_credentials` | `Bytes32` | `Bytes32`. **Prefix byte**: `0x01`=ETH1, `0x02`=compounding. **Bytes [12:32]** = execution address. Validators need `0x01`/`0x02` prefix for withdrawal eligibility. | Extract withdrawal address | ______________________________________________________________________ ## Output Space (State Fields Modified) | Field | Type | Value Space | Modification | | --------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `state.payload_expected_withdrawals` | `List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]` | Length in \[0, `MAX_WITHDRAWALS_PER_PAYLOAD`\]. **Derived from** input `next_withdrawal_index` (sequential), pending lists, and sweeps. | **Set** to computed withdrawals list | | `state.balances[*]` | `Gwei` | `uint64`. **Decreased by** withdrawal amounts for each validator withdrawal. | **Decreased** by validator withdrawal amounts | | `state.builders[*].balance` | `Gwei` | `uint64`. **Decreased by** withdrawal amounts for each builder withdrawal. New value = old - min(withdrawal.amount, balance). | **Decreased** by builder withdrawal amounts | | `state.builder_pending_withdrawals` | `List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT]` | New length = old length - `processed_builder_withdrawals_count`. **Sliced** to remove processed items. | **Sliced** - removes first N processed items | | `state.pending_partial_withdrawals` | `List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]` | New length = old length - `processed_partial_withdrawals_count`. | **Sliced** - removes first N processed items | | `state.next_withdrawal_index` | `WithdrawalIndex` | New value = `payload_expected_withdrawals[-1].index + 1` if any withdrawals, else unchanged. | **Incremented** by number of withdrawals | | `state.next_withdrawal_builder_index` | `BuilderIndex` | **Bound to** `len(builders)` (wraps via modulo). Updated based on builder sweep progress. | **Updated** based on builder sweep progress | | `state.next_withdrawal_validator_index` | `ValidatorIndex` | **Bound to** `len(validators)` (wraps via modulo). Updated based on validator sweep progress. | **Updated** based on validator sweep progress | ______________________________________________________________________ ## Early Exit Condition If `is_parent_block_full(state)` returns `False` (i.e., `latest_execution_payload_bid.block_hash != latest_block_hash`), the function returns immediately with **no state modifications**. ______________________________________________________________________ ## Key Constants | Constant | Value | Description | | -------------------------------------------- | --------------------- | ------------------------------------- | | `MAX_WITHDRAWALS_PER_PAYLOAD` | 16 | Max withdrawals per execution payload | | `MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP` | 16,384 | Max validators checked per sweep | | `MAX_BUILDERS_PER_WITHDRAWALS_SWEEP` | 16,384 | Max builders checked per sweep | | `MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP` | 8 | Max partial withdrawals processed | | `VALIDATOR_REGISTRY_LIMIT` | 2^40 | Max validators in registry | | `BUILDER_REGISTRY_LIMIT` | 2^40 | Max builders in registry | | `BUILDER_PENDING_WITHDRAWALS_LIMIT` | 2^20 (1,048,576) | Max pending builder withdrawals | | `PENDING_PARTIAL_WITHDRAWALS_LIMIT` | 2^27 (134,217,728) | Max pending partial withdrawals | | `MIN_ACTIVATION_BALANCE` | 32 ETH (32x10^9 Gwei) | Minimum balance for active validator | | `FAR_FUTURE_EPOCH` | 2^64-1 | Sentinel for "not set" epoch | ______________________________________________________________________ ## Key Cross-Field Constraints 1. **Index Alignment**: - `len(validators) == len(balances)` always - All validator indices must be `< len(validators)` - All builder indices must be `< len(builders)` 2. **Epoch Gating**: All `withdrawable_epoch` fields are compared against `current_epoch = compute_epoch_at_slot(state.slot)`. 3. **Balance Thresholds by Withdrawal Type**: - **Builder pending**: `actual_amount = min(requested_amount, builder.balance)` (builder balance can go to zero) - **Builder sweep**: requires `builder.withdrawable_epoch <= epoch` AND `builder.balance > 0`; withdraws full `builder.balance` - **Partial**: requires `effective_balance >= MIN_ACTIVATION_BALANCE` AND `balance > MIN_ACTIVATION_BALANCE` AND `exit_epoch == FAR_FUTURE_EPOCH` - **Full**: requires `has_execution_withdrawal_credential` AND `withdrawable_epoch <= epoch` AND `balance > 0` 4. **Processing Order & Caps** (combined constraint on output): All withdrawal types except validator sweep are capped at `MAX_WITHDRAWALS_PER_PAYLOAD - 1` total, reserving 1 slot for validator sweep. Each function receives `prior_withdrawals` and can only fill up to this limit. - **Builder pending** (`get_builder_withdrawals`): processed first, capped at `MAX_WITHDRAWALS_PER_PAYLOAD - 1` - **Pending partial** (`get_pending_partial_withdrawals`): processed second, capped at `min(prior + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, MAX - 1)`. This means if builders took N slots, partials get at most `MAX - 1 - N` slots. - **Builder sweep** (`get_builders_sweep_withdrawals`): processed third, capped at `MAX_WITHDRAWALS_PER_PAYLOAD - 1` total (including prior) - **Validator sweep** (`get_validators_sweep_withdrawals`): processed last, gets remaining slots, capped by `MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP` - **Total output**: up to `MAX_WITHDRAWALS_PER_PAYLOAD` 5. **Early Exit Binding**: Function executes only when `latest_execution_payload_bid.block_hash == latest_block_hash`. ______________________________________________________________________ ## Builder Model (PR #4788) Builders are **separate non-validating staked actors** stored in `state.builders[]`. They are NOT validators with special credentials. **Key differences from validators**: - Builders have their own registry (`state.builders`) separate from `state.validators` - Builder balances are in `Builder.balance`, not `state.balances[]` - Builders cannot be slashed - Builders use `BuilderIndex` type, which is converted to/from `ValidatorIndex` using `convert_builder_index_to_validator_index()` and `convert_validator_index_to_builder_index()` - In `Withdrawal` output, builder withdrawals use a converted validator index (with `BUILDER_INDEX_FLAG` bit set) **Builder withdrawal types**: 1. **Builder Pending Withdrawals**: Pending builder withdrawals stored in `state.builder_pending_withdrawals`. Uses `fee_recipient` as destination. 2. **Builder Sweep Withdrawals**: Automatic withdrawals when `builder.withdrawable_epoch <= current_epoch`. Uses `builder.execution_address` as destination and withdraws full balance. ______________________________________________________________________ ## Functions Called Call stack for `process_withdrawals`. Sources: gloas functions are defined in [gloas/beacon-chain.md](../../../../../../../specs/gloas/beacon-chain.md); inherited functions are from earlier forks (capella, electra, phase0). - `process_withdrawals(state)` — gloas - `is_parent_block_full(state)` — gloas - `get_expected_withdrawals(state)` — gloas - `get_builder_withdrawals(state, ...)` — gloas - `convert_builder_index_to_validator_index(index)` — gloas - `get_pending_partial_withdrawals(state, ...)` — electra - `get_builders_sweep_withdrawals(state, ...)` — gloas - `get_current_epoch(state)` — phase0 - `convert_builder_index_to_validator_index(index)` — gloas - `get_validators_sweep_withdrawals(state, ...)` — electra - `apply_withdrawals(state, withdrawals)` — gloas - `is_builder_index(validator_index)` — gloas - `convert_validator_index_to_builder_index(index)` — gloas - `decrease_balance(state, index, amount)` — phase0 - `update_next_withdrawal_index(state, withdrawals)` — capella - `update_payload_expected_withdrawals(state, withdrawals)` — gloas - `update_builder_pending_withdrawals(state, count)` — gloas - `update_pending_partial_withdrawals(state, count)` — electra - `update_next_withdrawal_builder_index(state, count)` — gloas - `update_next_withdrawal_validator_index(state, withdrawals)` — capella ______________________________________________________________________ ## Summary Diagram ```text +-----------------------------------------------------------+ | INPUT (Read) | +-----------------------------------------------------------+ | latest_execution_payload_bid.block_hash : Bytes32 | | latest_block_hash : Bytes32 | | slot : uint64 | | next_withdrawal_index : uint64 | | next_withdrawal_validator_index : uint64 | | next_withdrawal_builder_index : uint64 | | builders[0..2^40] : Container[] | | .balance, .execution_address, .withdrawable_epoch | | builder_pending_withdrawals[0..2^20] : Container[] | | .builder_index, .fee_recipient, .amount | | pending_partial_withdrawals[0..2^27] : Container[] | | .withdrawable_epoch, .validator_index, .amount | | balances[0..2^40] : uint64[] | | validators[0..2^40] : Container[] | | .effective_balance, .exit_epoch, .withdrawable_epoch, | | .withdrawal_credentials | +---------------------------+-------------------------------+ | v +-----------------------------------------------------------+ | process_withdrawals() | | | | 1. Builder pending withdrawals (from fee_recipient) | | 2. Partial validator withdrawals | | 3. Builder sweep withdrawals (from execution_address) | | 4. Validator sweep withdrawals | +---------------------------+-------------------------------+ | v +-----------------------------------------------------------+ | OUTPUT (Modified) | +-----------------------------------------------------------+ | payload_expected_withdrawals[0..16] : Container[] | | .index, .validator_index, .address, .amount | | builders[*].balance : uint64 | | balances[*] : uint64 | | builder_pending_withdrawals : Container[] | | pending_partial_withdrawals : Container[] | | next_withdrawal_index : uint64 | | next_withdrawal_builder_index : uint64 | | next_withdrawal_validator_index : uint64 | +-----------------------------------------------------------+ ```