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
| class SweepThresholdRequests(ProgressiveList[SetSweepThresholdRequest]):
"""
The set-sweep-threshold requests pertaining to a single execution
payload.
"""
|
New SweepThresholds
| 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
| class SetSweepThresholdRequest(Container):
source_address: ExecutionAddress
validator_pubkey: BLSPubkey
threshold: Gwei
|
Helper functions
Math
New bytes_to_uint16
| 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
| 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
| 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
|