Skip to content

EIP-8148 -- The Beacon Chain

Note: This document is a work-in-progress for researchers and implementers.

Introduction

This upgrade adds custom validator sweep threshold functionality to the beacon chain as part of the EIP-8148 upgrade.

This document specifies the beacon chain changes required to support these custom thresholds. The upgrade introduces a new request type within the execution payload, triggered by execution layer transactions, which updates a validator's sweep configuration in the beacon state. This allows validators to control their balance withdrawals more precisely.

Note: This specification is built upon Heze.

Types

New SweepThresholdRequests

1
2
3
4
5
class SweepThresholdRequests(ProgressiveList[SetSweepThresholdRequest]):
    """
    The set-sweep-threshold requests pertaining to a single execution
    payload.
    """

New SweepThresholds

1
2
3
4
class SweepThresholds(ProgressiveList[Gwei]):
    """
    Per-validator withdrawal sweep thresholds.
    """

Constants

New execution layer triggered request type

Name Value
SWEEP_THRESHOLD_REQUEST_TYPE Bytes1('0x05')

Sweep threshold validation

Name Value
SWEEP_THRESHOLD_CREDENTIAL_OFFSET Uint64(10)
SWEEP_THRESHOLD_CREDENTIAL_LENGTH Uint64(2)

Presets

Execution

Name Value Description
MAX_SET_SWEEP_THRESHOLD_REQUESTS_PER_PAYLOAD Uint64(2**4) (= 16) [New in EIP8148] Maximum number of execution layer set sweep threshold requests in each payload

Containers

Modified containers

BeaconState

class BeaconState(ProgressiveContainer):
    ACTIVE_FIELDS = active_fields(width=47)

    genesis_time: Uint64
    genesis_validators_root: Root
    slot: Slot
    fork: Fork
    latest_block_header: BeaconBlockHeader
    block_roots: BlockRoots
    state_roots: StateRoots
    historical_roots: HistoricalRoots
    eth1_data: Eth1Data
    eth1_data_votes: Eth1DataVotes
    eth1_deposit_index: Uint64
    validators: Validators
    balances: Balances
    randao_mixes: RandaoMixes
    slashings: Slashings
    previous_epoch_participation: EpochParticipation
    current_epoch_participation: EpochParticipation
    justification_bits: JustificationBits
    previous_justified_checkpoint: Checkpoint
    current_justified_checkpoint: Checkpoint
    finalized_checkpoint: Checkpoint
    inactivity_scores: InactivityScores
    current_sync_committee: SyncCommittee
    next_sync_committee: SyncCommittee
    latest_block_hash: Hash32
    next_withdrawal_index: WithdrawalIndex
    next_withdrawal_validator_index: ValidatorIndex
    historical_summaries: HistoricalSummaries
    deposit_requests_start_index: Uint64
    deposit_balance_to_consume: Gwei
    exit_balance_to_consume: Gwei
    earliest_exit_epoch: Epoch
    consolidation_balance_to_consume: Gwei
    earliest_consolidation_epoch: Epoch
    pending_deposits: PendingDeposits
    pending_partial_withdrawals: PendingPartialWithdrawals
    pending_consolidations: PendingConsolidations
    proposer_lookahead: ProposerLookahead
    builders: Builders
    next_withdrawal_builder_index: BuilderIndex
    execution_payload_availability: ExecutionPayloadAvailability
    builder_pending_payments: BuilderPendingPayments
    builder_pending_withdrawals: BuilderPendingWithdrawals
    latest_execution_payload_bid: ExecutionPayloadBid
    payload_expected_withdrawals: Withdrawals
    ptc_window: PayloadTimelinessCommitteeWindow
    # [New in EIP8148]
    validator_sweep_thresholds: SweepThresholds

ExecutionRequests

class ExecutionRequests(ProgressiveContainer):
    ACTIVE_FIELDS = active_fields(width=6)

    deposits: DepositRequests
    withdrawals: WithdrawalRequests
    consolidations: ConsolidationRequests
    builder_deposits: BuilderDepositRequests
    builder_exits: BuilderExitRequests
    # [New in EIP8148]
    sweep_thresholds: SweepThresholdRequests

New containers

SetSweepThresholdRequest

1
2
3
4
class SetSweepThresholdRequest(Container):
    source_address: ExecutionAddress
    validator_pubkey: BLSPubkey
    threshold: Gwei

Helper functions

Math

New bytes_to_uint16

1
2
3
4
5
def bytes_to_uint16(data: bytes) -> Uint16:
    """
    Return the integer deserialization of ``data`` interpreted as ``ENDIANNESS``-endian.
    """
    return Uint16(int.from_bytes(data, ENDIANNESS))

Predicates

Modified is_partially_withdrawable_validator

def is_partially_withdrawable_validator(
    validator: Validator, balance: Gwei, sweep_threshold: Gwei
) -> bool:
    """
    Check if ``validator`` is partially withdrawable.
    """
    # [Modified in EIP8148]
    effective_sweep_threshold = get_effective_sweep_threshold(validator, sweep_threshold)
    # [Modified in EIP8148]
    has_effective_sweep_threshold = validator.effective_balance >= effective_sweep_threshold
    # [Modified in EIP8148]
    has_excess_balance = balance > effective_sweep_threshold
    return (
        has_execution_withdrawal_credential(validator)
        # [Modified in EIP8148]
        and has_effective_sweep_threshold
        and has_excess_balance
    )

Misc

New get_initial_sweep_threshold

A validator may be created with a custom sweep threshold already in place, by encoding it in the compounding withdrawal credentials of the deposit that creates it. The SWEEP_THRESHOLD_CREDENTIAL_LENGTH bytes starting at SWEEP_THRESHOLD_CREDENTIAL_OFFSET, which are unused before this upgrade, hold the threshold in units of EFFECTIVE_BALANCE_INCREMENT, encoded as a little-endian integer:

Bytes Contents
0 COMPOUNDING_WITHDRAWAL_PREFIX (0x02)
1..9 Reserved
10..11 Threshold in EFFECTIVE_BALANCE_INCREMENT units
12..31 Execution address

Note: A threshold that is out of range is ignored rather than rejected, so that a deposit built by tooling unaware of this upgrade, or carrying a nonsensical value, still creates a validator with the default threshold. Only compounding credentials carry a threshold; for any other prefix this returns 0, which means the default applies.

def get_initial_sweep_threshold(withdrawal_credentials: Bytes32) -> Gwei:
    """
    Get the initial sweep threshold for a validator created with
    ``withdrawal_credentials``.
    """
    if not is_compounding_withdrawal_credential(withdrawal_credentials):
        return Gwei(0)

    start = SWEEP_THRESHOLD_CREDENTIAL_OFFSET
    end = start + SWEEP_THRESHOLD_CREDENTIAL_LENGTH
    increments = bytes_to_uint16(withdrawal_credentials[start:end])
    threshold = Gwei(increments) * EFFECTIVE_BALANCE_INCREMENT

    if threshold < MIN_ACTIVATION_BALANCE:
        return MAX_EFFECTIVE_BALANCE_ELECTRA
    if threshold > MAX_EFFECTIVE_BALANCE_ELECTRA:
        return MAX_EFFECTIVE_BALANCE_ELECTRA

    return threshold

New get_effective_sweep_threshold

1
2
3
4
5
6
7
8
def get_effective_sweep_threshold(validator: Validator, sweep_threshold: Gwei) -> Gwei:
    """
    Get effective sweep threshold for ``validator``.
    """
    if sweep_threshold != 0:
        return sweep_threshold
    else:
        return get_max_effective_balance(validator)

Validator registry

Modified add_validator_to_registry

Note: The function add_validator_to_registry is modified to initialize the item in the validator_sweep_thresholds list.

def add_validator_to_registry(
    state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: Gwei
) -> None:
    index = get_index_for_new_validator(state)
    validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount)
    set_or_append_list(state.validators, index, validator)
    set_or_append_list(state.balances, index, amount)
    set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000))
    set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000))
    set_or_append_list(state.inactivity_scores, index, Uint64(0))
    # [New in EIP8148]
    threshold = get_initial_sweep_threshold(withdrawal_credentials)
    set_or_append_list(state.validator_sweep_thresholds, index, threshold)

Beacon state mutators

Modified switch_to_compounding_validator

1
2
3
4
5
6
7
8
def switch_to_compounding_validator(state: BeaconState, index: ValidatorIndex) -> None:
    validator = state.validators[index]
    validator.withdrawal_credentials = Bytes32(
        COMPOUNDING_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:]
    )
    queue_excess_active_balance(state, index)
    # [New in EIP8148]
    state.validator_sweep_thresholds[index] = MAX_EFFECTIVE_BALANCE_ELECTRA

Beacon chain state transition function

Epoch processing

Modified process_effective_balance_updates

Note: The function process_effective_balance_updates is modified to use custom sweep thresholds.

def process_effective_balance_updates(state: BeaconState) -> None:
    # Update effective balances with hysteresis
    for index, validator in enumerate(state.validators):
        balance = state.balances[index]
        HYSTERESIS_INCREMENT = Uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT)
        DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER
        UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER
        # [Modified in EIP8148]
        sweep_threshold = state.validator_sweep_thresholds[index]
        effective_sweep_threshold = get_effective_sweep_threshold(validator, sweep_threshold)

        if (
            balance + DOWNWARD_THRESHOLD < validator.effective_balance
            or validator.effective_balance + UPWARD_THRESHOLD < balance
        ):
            # [Modified in EIP8148]
            validator.effective_balance = min(
                balance - balance % EFFECTIVE_BALANCE_INCREMENT, effective_sweep_threshold
            )

Block processing

Execution payload

Modified get_execution_requests_list

Note: Encodes execution requests as defined by EIP-7685.

def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequence[bytes]:
    requests: Sequence[Tuple[Bytes1, ProgressiveList]] = [
        (DEPOSIT_REQUEST_TYPE, execution_requests.deposits),
        (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals),
        (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations),
        (BUILDER_DEPOSIT_REQUEST_TYPE, execution_requests.builder_deposits),
        (BUILDER_EXIT_REQUEST_TYPE, execution_requests.builder_exits),
        # [New in EIP8148]
        (SWEEP_THRESHOLD_REQUEST_TYPE, execution_requests.sweep_thresholds),
    ]

    return [
        request_type + ssz_serialize(request_data)
        for request_type, request_data in requests
        if len(request_data) != 0
    ]

Withdrawals

Modified get_validators_sweep_withdrawals
def get_validators_sweep_withdrawals(
    state: BeaconState,
    withdrawal_index: WithdrawalIndex,
    prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, Uint64]:
    epoch = get_current_epoch(state)
    validators_limit = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP)
    withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD
    # There must be at least one space reserved for validator sweep withdrawals
    assert len(prior_withdrawals) < withdrawals_limit

    processed_count = Uint64(0)
    withdrawals: list[Withdrawal] = []
    validator_index = state.next_withdrawal_validator_index
    for _ in range(validators_limit):
        all_withdrawals = list(prior_withdrawals) + withdrawals
        has_reached_limit = len(all_withdrawals) >= withdrawals_limit
        if has_reached_limit:
            break

        validator = state.validators[validator_index]
        balance = get_balance_after_withdrawals(state, validator_index, all_withdrawals)
        # [New in EIP8148]
        sweep_threshold = state.validator_sweep_thresholds[validator_index]
        if is_fully_withdrawable_validator(validator, balance, epoch):
            withdrawals.append(
                Withdrawal(
                    index=withdrawal_index,
                    validator_index=validator_index,
                    address=ExecutionAddress(validator.withdrawal_credentials[12:]),
                    amount=balance,
                )
            )
            withdrawal_index += 1
        # [Modified in EIP8148]
        elif is_partially_withdrawable_validator(validator, balance, sweep_threshold):
            withdrawals.append(
                Withdrawal(
                    index=withdrawal_index,
                    validator_index=validator_index,
                    address=ExecutionAddress(validator.withdrawal_credentials[12:]),
                    # [Modified in EIP8148]
                    amount=balance - get_effective_sweep_threshold(validator, sweep_threshold),
                )
            )
            withdrawal_index += 1

        validator_index = (validator_index + 1) % len(state.validators)
        processed_count += 1

    return withdrawals, withdrawal_index, processed_count

Operations

New process_set_sweep_threshold_request

Note: A request is rejected if its threshold is below the validator's current balance. This prevents validators from gaming the sweep cycle to bypass the partial withdrawal queue and perform immediate withdrawals. To lower a threshold, validators must first request a partial withdrawal, wait for processing, then set the desired threshold.

def process_set_sweep_threshold_request(
    state: BeaconState, request: SetSweepThresholdRequest
) -> None:
    validator_pubkeys = [v.pubkey for v in state.validators]
    if request.validator_pubkey not in validator_pubkeys:
        return

    index = ValidatorIndex(validator_pubkeys.index(request.validator_pubkey))
    validator = state.validators[index]

    if not has_compounding_withdrawal_credential(validator):
        return
    if validator.withdrawal_credentials[12:] != request.source_address:
        return
    if validator.exit_epoch != FAR_FUTURE_EPOCH:
        return
    if state.validator_sweep_thresholds[index] == request.threshold:
        return
    if request.threshold < state.balances[index]:
        return
    if request.threshold % EFFECTIVE_BALANCE_INCREMENT != 0:
        return
    if request.threshold < MIN_ACTIVATION_BALANCE:
        return
    if request.threshold > MAX_EFFECTIVE_BALANCE_ELECTRA:
        return

    state.validator_sweep_thresholds[index] = request.threshold

Parent execution payload

Modified apply_parent_execution_payload

Note: This function processes the parent's execution requests, queues the builder payment, updates payload availability, and updates the latest block hash. It is called by process_parent_execution_payload during block processing and by the validator during block production before computing withdrawals.

def apply_parent_execution_payload(
    state: BeaconState,
    requests: ExecutionRequests,
) -> None:
    parent_bid = state.latest_execution_payload_bid
    parent_slot = state.latest_block_header.slot
    parent_epoch = compute_epoch_at_slot(parent_slot)

    assert len(requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD
    assert len(requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD
    assert len(requests.builder_deposits) <= MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD
    assert len(requests.builder_exits) <= MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD
    # [New in EIP8148]
    assert len(requests.sweep_thresholds) <= MAX_SET_SWEEP_THRESHOLD_REQUESTS_PER_PAYLOAD

    # Process execution requests from parent's payload. The execution
    # requests are processed at state.slot (child's slot), not the parent's slot.
    def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
        for operation in operations:
            fn(state, operation)

    for_ops(requests.deposits, process_deposit_request)
    for_ops(requests.withdrawals, process_withdrawal_request)
    for_ops(requests.consolidations, process_consolidation_request)
    for_ops(requests.builder_deposits, process_builder_deposit_request)
    for_ops(requests.builder_exits, process_builder_exit_request)
    # [New in EIP8148]
    for_ops(requests.sweep_thresholds, process_set_sweep_threshold_request)

    # Settle the builder payment
    if parent_epoch == get_current_epoch(state):
        payment_index = SLOTS_PER_EPOCH + parent_slot % SLOTS_PER_EPOCH
        settle_builder_payment(state, payment_index)
    elif parent_epoch == get_previous_epoch(state):
        payment_index = parent_slot % SLOTS_PER_EPOCH
        settle_builder_payment(state, payment_index)
    elif parent_bid.value > 0:
        # Parent is older than the previous epoch, its payment entry has been
        # evicted from builder_pending_payments. Append the withdrawal directly.
        state.builder_pending_withdrawals.append(
            BuilderPendingWithdrawal(
                fee_recipient=parent_bid.fee_recipient,
                amount=parent_bid.value,
                builder_index=parent_bid.builder_index,
            )
        )

    # Update parent payload availability and latest block hash
    state.execution_payload_availability[parent_slot % SLOTS_PER_HISTORICAL_ROOT] = Boolean(True)
    state.latest_block_hash = parent_bid.block_hash