EIP-7732 -- The Beacon Chain
Note: This document is a work-in-progress for researchers and implementers.
Introduction
This is the beacon chain specification of the enshrined proposer builder
separation feature.
Note: This specification is built upon
Electra and is under active development.
This feature adds new staked consensus participants called Builders and new
honest validators duties called payload timeliness attestations. The slot is
divided in four intervals. Honest validators gather signed bids (a
SignedExecutionPayloadHeader
) from builders and submit their consensus blocks
(a SignedBeaconBlock
) including accepted bids at the beginning of the slot. At
the start of the second interval, honest validators submit attestations just as
they do previous to this feature). At the start of the third interval,
aggregators aggregate these attestations and the builder broadcasts either a
full payload or a message indicating that they are withholding the payload (a
SignedExecutionPayloadEnvelope
). At the start of the fourth interval, some
validators selected to be members of the new Payload Timeliness Committee
(PTC) attest to the presence and timeliness of the builder's payload.
At any given slot, the status of the blockchain's head may be either
- A block from a previous slot (e.g. the current slot's proposer did not submit
its block).
- An empty block from the current slot (e.g. the proposer submitted a timely
block, but the builder did not reveal the payload on time).
- A full block for the current slot (both the proposer and the builder revealed
on time).
Constants
Domain types
Name |
Value |
DOMAIN_BEACON_BUILDER |
DomainType('0x1B000000') |
DOMAIN_PTC_ATTESTER |
DomainType('0x0C000000') |
Misc
Name |
Value |
BUILDER_PAYMENT_THRESHOLD_NUMERATOR |
uint64(6) |
BUILDER_PAYMENT_THRESHOLD_DENOMINATOR |
uint64(10) |
Preset
Misc
Name |
Value |
PTC_SIZE |
uint64(2**9) (=512) |
Max operations per block
Name |
Value |
MAX_PAYLOAD_ATTESTATIONS |
4 |
State list lengths
Name |
Value |
Unit |
BUILDER_PENDING_WITHDRAWALS_LIMIT |
uint64(2**20) (= 1,048,576) |
Builder pending withdrawals |
Withdrawal prefixes
Name |
Value |
Description |
BUILDER_WITHDRAWAL_PREFIX |
Bytes1('0x03') |
Withdrawal credential prefix for a builder |
Containers
New containers
BuilderPendingPayment
| class BuilderPendingPayment(Container):
weight: Gwei
withdrawal: BuilderPendingWithdrawal
|
BuilderPendingWithdrawal
| class BuilderPendingWithdrawal(Container):
fee_recipient: ExecutionAddress
amount: Gwei
builder_index: ValidatorIndex
withdrawable_epoch: Epoch
|
PayloadAttestationData
| class PayloadAttestationData(Container):
beacon_block_root: Root
slot: Slot
payload_present: boolean
blob_data_available: boolean
|
PayloadAttestation
| class PayloadAttestation(Container):
aggregation_bits: Bitvector[PTC_SIZE]
data: PayloadAttestationData
signature: BLSSignature
|
PayloadAttestationMessage
| class PayloadAttestationMessage(Container):
validator_index: ValidatorIndex
data: PayloadAttestationData
signature: BLSSignature
|
IndexedPayloadAttestation
| class IndexedPayloadAttestation(Container):
attesting_indices: List[ValidatorIndex, PTC_SIZE]
data: PayloadAttestationData
signature: BLSSignature
|
| class SignedExecutionPayloadHeader(Container):
message: ExecutionPayloadHeader
signature: BLSSignature
|
ExecutionPayloadEnvelope
| class ExecutionPayloadEnvelope(Container):
payload: ExecutionPayload
execution_requests: ExecutionRequests
builder_index: ValidatorIndex
beacon_block_root: Root
slot: Slot
blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
state_root: Root
|
SignedExecutionPayloadEnvelope
| class SignedExecutionPayloadEnvelope(Container):
message: ExecutionPayloadEnvelope
signature: BLSSignature
|
Modified containers
BeaconBlockBody
Note: The BeaconBlockBody
container is modified to contain a
SignedExecutionPayloadHeader
. The containers BeaconBlock
and
SignedBeaconBlock
are modified indirectly. The field execution_requests
is
removed from the beacon block body and moved into the signed execution payload
envelope.
| class BeaconBlockBody(Container):
randao_reveal: BLSSignature
eth1_data: Eth1Data
graffiti: Bytes32
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA]
attestations: List[Attestation, MAX_ATTESTATIONS_ELECTRA]
deposits: List[Deposit, MAX_DEPOSITS]
voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
sync_aggregate: SyncAggregate
# [Modified in EIP7732]
# Removed `execution_payload`
bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]
# [Modified in EIP7732]
# Removed `blob_kzg_commitments`
# [Modified in EIP7732]
# Removed `execution_requests`
# [New in EIP7732]
signed_execution_payload_header: SignedExecutionPayloadHeader
# [New in EIP7732]
payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS]
|
Note: The ExecutionPayloadHeader
is modified to only contain the block hash
of the committed ExecutionPayload
in addition to the builder's payment
information, gas limit and KZG commitments root to verify the inclusion proofs.
| class ExecutionPayloadHeader(Container):
parent_block_hash: Hash32
parent_block_root: Root
block_hash: Hash32
fee_recipient: ExecutionAddress
gas_limit: uint64
builder_index: ValidatorIndex
slot: Slot
value: Gwei
blob_kzg_commitments_root: Root
|
BeaconState
Note: The BeaconState
is modified to track the last withdrawals honored in
the CL. The latest_execution_payload_header
is modified semantically to refer
not to a past committed ExecutionPayload
but instead it corresponds to the
state's slot builder's bid. Another addition is to track the last committed
block hash and the last slot that was full, that is in which there were both
consensus and execution blocks included.
| class BeaconState(Container):
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee
latest_execution_payload_header: ExecutionPayloadHeader
next_withdrawal_index: WithdrawalIndex
next_withdrawal_validator_index: ValidatorIndex
historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
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: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
# [New in EIP7732]
execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT]
# [New in EIP7732]
builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH]
# [New in EIP7732]
builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT]
# [New in EIP7732]
latest_block_hash: Hash32
# [New in EIP7732]
latest_withdrawals_root: Root
|
Helper functions
Predicates
New has_builder_withdrawal_credential
| def has_builder_withdrawal_credential(validator: Validator) -> bool:
"""
Check if ``validator`` has an 0x03 prefixed "builder" withdrawal credential.
"""
return is_builder_withdrawal_credential(validator.withdrawal_credentials)
|
Modified has_compounding_withdrawal_credential
Note: the function has_compounding_withdrawal_credential
is modified to
return true for builders.
| def has_compounding_withdrawal_credential(validator: Validator) -> bool:
"""
Check if ``validator`` has an 0x02 or 0x03 prefixed withdrawal credential.
"""
return is_compounding_withdrawal_credential(
validator.withdrawal_credentials
) or is_builder_withdrawal_credential(validator.withdrawal_credentials)
|
New is_attestation_same_slot
| def is_attestation_same_slot(state: BeaconState, data: AttestationData) -> bool:
"""
Checks if the attestation was for the block proposed at the attestation slot
"""
if data.slot == 0:
return True
is_matching_blockroot = data.beacon_block_root == get_block_root_at_slot(state, Slot(data.slot))
is_current_blockroot = data.beacon_block_root != get_block_root_at_slot(
state, Slot(data.slot - 1)
)
return is_matching_blockroot and is_current_blockroot
|
New is_builder_withdrawal_credential
| def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool:
return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX
|
New is_valid_indexed_payload_attestation
| def is_valid_indexed_payload_attestation(
state: BeaconState, indexed_payload_attestation: IndexedPayloadAttestation
) -> bool:
"""
Check if ``indexed_payload_attestation`` is not empty, has sorted and unique indices and has
a valid aggregate signature.
"""
# Verify indices are non-empty and sorted
indices = indexed_payload_attestation.attesting_indices
if len(indices) == 0 or not indices == sorted(indices):
return False
# Verify aggregate signature
pubkeys = [state.validators[i].pubkey for i in indices]
domain = get_domain(state, DOMAIN_PTC_ATTESTER, None)
signing_root = compute_signing_root(indexed_payload_attestation.data, domain)
return bls.FastAggregateVerify(pubkeys, signing_root, indexed_payload_attestation.signature)
|
New is_parent_block_full
This function returns true if the last committed payload header was fulfilled
with a payload, this can only happen when both beacon block and payload were
present. This function must be called on a beacon state before processing the
execution payload header in the block.
| def is_parent_block_full(state: BeaconState) -> bool:
return state.latest_execution_payload_header.block_hash == state.latest_block_hash
|
Misc
New remove_flag
| def remove_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags:
flag = ParticipationFlags(2**flag_index)
return flags & ~flag
|
New compute_balance_weighted_selection
| def compute_balance_weighted_selection(
state: BeaconState,
indices: Sequence[ValidatorIndex],
seed: Bytes32,
size: uint64,
shuffle_indices: bool,
) -> Sequence[ValidatorIndex]:
"""
Return ``size`` indices sampled by effective balance, using ``indices``
as candidates. If ``shuffle_indices`` is ``True``, candidate indices
are themselves sampled from ``indices`` by shuffling it, otherwise
``indices`` is traversed in order.
"""
total = uint64(len(indices))
assert total > 0
selected: List[ValidatorIndex] = []
i = uint64(0)
while len(selected) < size:
next_index = i % total
if shuffle_indices:
next_index = compute_shuffled_index(next_index, total, seed)
candidate_index = indices[next_index]
if compute_balance_weighted_acceptance(state, candidate_index, seed, i):
selected.append(candidate_index)
i += 1
return selected
|
New compute_balance_weighted_acceptance
| def compute_balance_weighted_acceptance(
state: BeaconState, index: ValidatorIndex, seed: Bytes32, i: uint64
) -> bool:
"""
Return whether to accept the selection of the validator ``index``, with probability
proportional to its ``effective_balance``, and randomness given by ``seed`` and ``i``.
"""
MAX_RANDOM_VALUE = 2**16 - 1
random_bytes = hash(seed + uint_to_bytes(i // 16))
offset = i % 16 * 2
random_value = bytes_to_uint64(random_bytes[offset : offset + 2])
effective_balance = state.validators[index].effective_balance
return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value
|
Modified compute_proposer_indices
Note: compute_proposer_indices
is refactored to use
compute_balance_weighted_selection
as a helper for the balance-weighted
sampling process.
| def compute_proposer_indices(
state: BeaconState, epoch: Epoch, seed: Bytes32, indices: Sequence[ValidatorIndex]
) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]:
"""
Return the proposer indices for the given ``epoch``.
"""
start_slot = compute_start_slot_at_epoch(epoch)
seeds = [hash(seed + uint_to_bytes(Slot(start_slot + i))) for i in range(SLOTS_PER_EPOCH)]
return [
compute_balance_weighted_selection(state, indices, seed, size=1, shuffle_indices=True)[0]
for seed in seeds
]
|
Beacon State accessors
Modified get_next_sync_committee_indices
Note: get_next_sync_committee_indices
is refactored to use
compute_balance_weighted_selection
as a helper for the balance-weighted
sampling process.
| def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]:
"""
Return the sync committee indices, with possible duplicates, for the next sync committee.
"""
epoch = Epoch(get_current_epoch(state) + 1)
seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE)
indices = get_active_validator_indices(state, epoch)
return compute_balance_weighted_selection(
state, indices, seed, size=SYNC_COMMITTEE_SIZE, shuffle_indices=True
)
|
New get_attestation_participation_flag_indices
| def get_attestation_participation_flag_indices(
state: BeaconState, data: AttestationData, inclusion_delay: uint64
) -> Sequence[int]:
"""
Return the flag indices that are satisfied by an attestation.
"""
if data.target.epoch == get_current_epoch(state):
justified_checkpoint = state.current_justified_checkpoint
else:
justified_checkpoint = state.previous_justified_checkpoint
# Matching roots
is_matching_source = data.source == justified_checkpoint
is_matching_target = is_matching_source and data.target.root == get_block_root(
state, data.target.epoch
)
is_matching_blockroot = is_matching_target and data.beacon_block_root == get_block_root_at_slot(
state, Slot(data.slot)
)
is_matching_payload = False
if is_attestation_same_slot(state, data):
assert data.index == 0
is_matching_payload = True
else:
is_matching_payload = (
data.index
== state.execution_payload_availability[data.slot % SLOTS_PER_HISTORICAL_ROOT]
)
is_matching_head = is_matching_blockroot and is_matching_payload
assert is_matching_source
participation_flag_indices = []
if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH):
participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX)
if is_matching_target and inclusion_delay <= SLOTS_PER_EPOCH:
participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX)
if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY:
participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX)
return participation_flag_indices
|
New get_ptc
| def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
"""
Get the payload timeliness committee for the given ``slot``
"""
epoch = compute_epoch_at_slot(slot)
seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
indices: List[ValidatorIndex] = []
# Concatenate all committees for this slot in order
committees_per_slot = get_committee_count_per_slot(state, epoch)
for i in range(committees_per_slot):
committee = get_beacon_committee(state, slot, CommitteeIndex(i))
indices.extend(committee)
return compute_balance_weighted_selection(
state, indices, seed, size=PTC_SIZE, shuffle_indices=False
)
|
New get_indexed_payload_attestation
| def get_indexed_payload_attestation(
state: BeaconState, slot: Slot, payload_attestation: PayloadAttestation
) -> IndexedPayloadAttestation:
"""
Return the indexed payload attestation corresponding to ``payload_attestation``.
"""
ptc = get_ptc(state, slot)
attesting_indices = [
index for i, index in enumerate(ptc) if payload_attestation.aggregation_bits[i]
]
return IndexedPayloadAttestation(
attesting_indices=sorted(attesting_indices),
data=payload_attestation.data,
signature=payload_attestation.signature,
)
|
Beacon chain state transition function
Note: state transition is fundamentally modified in EIP-7732. The full state
transition is broken in two parts, first importing a signed block and then
importing an execution payload.
The post-state corresponding to a pre-state state
and a signed beacon block
signed_block
is defined as state_transition(state, signed_block)
. State
transitions that trigger an unhandled exception (e.g. a failed assert
or an
out-of-range list access) are considered invalid. State transitions that cause a
uint64
overflow or underflow are also considered invalid.
The post-state corresponding to a pre-state state
and a signed execution
payload envelope signed_envelope
is defined as
process_execution_payload(state, signed_envelope)
. State transitions that
trigger an unhandled exception (e.g. a failed assert
or an out-of-range list
access) are considered invalid. State transitions that cause an uint64
overflow or underflow are also considered invalid.
Modified process_slot
Note: process_slot
is modified to unset the payload availability bit.
| def process_slot(state: BeaconState) -> None:
# Cache state root
previous_state_root = hash_tree_root(state)
state.state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root
# Cache latest block header state root
if state.latest_block_header.state_root == Bytes32():
state.latest_block_header.state_root = previous_state_root
# Cache block root
previous_block_root = hash_tree_root(state.latest_block_header)
state.block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root
# [New in EIP7732]
# Unset the next payload availability
state.execution_payload_availability[(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT] = 0b0
|
Epoch processing
Modified process_epoch
Note: The function process_epoch
is modified to process the builder
payments.
| def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state)
process_inactivity_updates(state)
process_rewards_and_penalties(state)
process_registry_updates(state)
process_slashings(state)
process_eth1_data_reset(state)
process_pending_deposits(state)
process_pending_consolidations(state)
process_effective_balance_updates(state)
process_slashings_reset(state)
process_randao_mixes_reset(state)
process_historical_summaries_update(state)
process_participation_flag_updates(state)
process_sync_committee_updates(state)
# [New in EIP7732]
process_builder_pending_payments(state)
|
New process_builder_pending_payments
| def process_builder_pending_payments(state: BeaconState) -> None:
"""
Processes the builder pending payments from the previous epoch.
"""
quorum = (
get_total_active_balance(state) // SLOTS_PER_EPOCH * BUILDER_PAYMENT_THRESHOLD_NUMERATOR
)
quorum //= BUILDER_PAYMENT_THRESHOLD_DENOMINATOR
for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]:
if payment.weight > quorum:
exit_queue_epoch = compute_exit_epoch_and_update_churn(state, payment.withdrawal.amount)
payment.withdrawal.withdrawable_epoch = Epoch(
exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
)
state.builder_pending_withdrawals.append(payment.withdrawal)
state.builder_pending_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:] + [
BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)
]
|
Block processing
Note: The function process_block
is modified to call the new and updated
functions and removes the call to process_execution_payload
.
| def process_block(state: BeaconState, block: BeaconBlock) -> None:
process_block_header(state, block)
# [Modified in EIP7732]
process_withdrawals(state)
# [Modified in EIP7732]
# Removed `process_execution_payload`
# [New in EIP7732]
process_execution_payload_header(state, block)
process_randao(state, block.body)
process_eth1_data(state, block.body)
# [Modified in EIP7732]
process_operations(state, block.body)
process_sync_aggregate(state, block.body.sync_aggregate)
|
Withdrawals
New is_builder_payment_withdrawable
| def is_builder_payment_withdrawable(
state: BeaconState, withdrawal: BuilderPendingWithdrawal
) -> bool:
"""
Check if the builder is slashed and not yet withdrawable.
"""
builder = state.validators[withdrawal.builder_index]
current_epoch = compute_epoch_at_slot(state.slot)
return builder.withdrawable_epoch >= current_epoch or not builder.slashed
|
Modified get_expected_withdrawals
Note: The function get_expected_withdrawals
is modified to include builder
payments.
| def get_expected_withdrawals(state: BeaconState) -> Tuple[Sequence[Withdrawal], uint64, uint64]:
epoch = get_current_epoch(state)
withdrawal_index = state.next_withdrawal_index
validator_index = state.next_withdrawal_validator_index
withdrawals: List[Withdrawal] = []
processed_partial_withdrawals_count = 0
processed_builder_withdrawals_count = 0
# [New in EIP7732]
# Sweep for builder payments
for withdrawal in state.builder_pending_withdrawals:
if (
withdrawal.withdrawable_epoch > epoch
or len(withdrawals) + 1 == MAX_WITHDRAWALS_PER_PAYLOAD
):
break
if is_builder_payment_withdrawable(state, withdrawal):
total_withdrawn = sum(
w.amount for w in withdrawals if w.validator_index == withdrawal.builder_index
)
balance = state.balances[withdrawal.builder_index] - total_withdrawn
builder = state.validators[withdrawal.builder_index]
if builder.slashed:
withdrawable_balance = min(balance, withdrawal.amount)
elif balance > MIN_ACTIVATION_BALANCE:
withdrawable_balance = min(balance - MIN_ACTIVATION_BALANCE, withdrawal.amount)
else:
withdrawable_balance = 0
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=withdrawal.builder_index,
address=withdrawal.fee_recipient,
amount=withdrawable_balance,
)
)
withdrawal_index += WithdrawalIndex(1)
processed_builder_withdrawals_count += 1
# Sweep for pending partial withdrawals
bound = min(
len(withdrawals) + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
MAX_WITHDRAWALS_PER_PAYLOAD - 1,
)
for withdrawal in state.pending_partial_withdrawals:
if withdrawal.withdrawable_epoch > epoch or len(withdrawals) == bound:
break
validator = state.validators[withdrawal.validator_index]
has_sufficient_effective_balance = validator.effective_balance >= MIN_ACTIVATION_BALANCE
total_withdrawn = sum(
w.amount for w in withdrawals if w.validator_index == withdrawal.validator_index
)
balance = state.balances[withdrawal.validator_index] - total_withdrawn
has_excess_balance = balance > MIN_ACTIVATION_BALANCE
if (
validator.exit_epoch == FAR_FUTURE_EPOCH
and has_sufficient_effective_balance
and has_excess_balance
):
withdrawable_balance = min(balance - MIN_ACTIVATION_BALANCE, withdrawal.amount)
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=withdrawal.validator_index,
address=ExecutionAddress(validator.withdrawal_credentials[12:]),
amount=withdrawable_balance,
)
)
withdrawal_index += WithdrawalIndex(1)
processed_partial_withdrawals_count += 1
# Sweep for remaining.
bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP)
for _ in range(bound):
validator = state.validators[validator_index]
total_withdrawn = sum(w.amount for w in withdrawals if w.validator_index == validator_index)
balance = state.balances[validator_index] - total_withdrawn
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 += WithdrawalIndex(1)
elif is_partially_withdrawable_validator(validator, balance):
withdrawals.append(
Withdrawal(
index=withdrawal_index,
validator_index=validator_index,
address=ExecutionAddress(validator.withdrawal_credentials[12:]),
amount=balance - get_max_effective_balance(validator),
)
)
withdrawal_index += WithdrawalIndex(1)
if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD:
break
validator_index = ValidatorIndex((validator_index + 1) % len(state.validators))
return (
withdrawals,
processed_builder_withdrawals_count,
processed_partial_withdrawals_count,
)
|
Modified process_withdrawals
Note: This is modified to take only the state
as parameter. Withdrawals are
deterministic given the beacon state, any execution payload that has the
corresponding block as parent beacon block is required to honor these
withdrawals in the execution layer. This function must be called before
process_execution_payload_header
as this latter function affects validator
balances.
| def process_withdrawals(state: BeaconState) -> None:
# return early if the parent block was empty
if not is_parent_block_full(state):
return
withdrawals, processed_builder_withdrawals_count, processed_partial_withdrawals_count = (
get_expected_withdrawals(state)
)
withdrawals_list = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawals)
state.latest_withdrawals_root = hash_tree_root(withdrawals_list)
for withdrawal in withdrawals:
decrease_balance(state, withdrawal.validator_index, withdrawal.amount)
# Update the pending builder withdrawals
state.builder_pending_withdrawals = [
w
for w in state.builder_pending_withdrawals[:processed_builder_withdrawals_count]
if not is_builder_payment_withdrawable(state, w)
] + state.builder_pending_withdrawals[processed_builder_withdrawals_count:]
# Update pending partial withdrawals
state.pending_partial_withdrawals = state.pending_partial_withdrawals[
processed_partial_withdrawals_count:
]
# Update the next withdrawal index if this block contained withdrawals
if len(withdrawals) != 0:
latest_withdrawal = withdrawals[-1]
state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1)
# Update the next validator index to start the next withdrawal sweep
if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD:
# Next sweep starts after the latest withdrawal's validator index
next_validator_index = ValidatorIndex(
(withdrawals[-1].validator_index + 1) % len(state.validators)
)
state.next_withdrawal_validator_index = next_validator_index
else:
# Advance sweep by the max length of the sweep if there was not a full set of withdrawals
next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP
next_validator_index = ValidatorIndex(next_index % len(state.validators))
state.next_withdrawal_validator_index = next_validator_index
|
| def verify_execution_payload_header_signature(
state: BeaconState, signed_header: SignedExecutionPayloadHeader
) -> bool:
# Check the signature
builder = state.validators[signed_header.message.builder_index]
signing_root = compute_signing_root(
signed_header.message, get_domain(state, DOMAIN_BEACON_BUILDER)
)
return bls.Verify(builder.pubkey, signing_root, signed_header.signature)
|
| def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None:
# Verify the header signature
signed_header = block.body.signed_execution_payload_header
assert verify_execution_payload_header_signature(state, signed_header)
header = signed_header.message
builder_index = header.builder_index
builder = state.validators[builder_index]
assert is_active_validator(builder, get_current_epoch(state))
assert not builder.slashed
amount = header.value
# For self-builds, amount must be zero regardless of withdrawal credential prefix
if builder_index == block.proposer_index:
assert amount == 0
else:
# Non-self builds require builder withdrawal credential
assert has_builder_withdrawal_credential(builder)
# Check that the builder is active, non-slashed, and has funds to cover the bid
pending_payments = sum(
payment.withdrawal.amount
for payment in state.builder_pending_payments
if payment.withdrawal.builder_index == builder_index
)
pending_withdrawals = sum(
withdrawal.amount
for withdrawal in state.builder_pending_withdrawals
if withdrawal.builder_index == builder_index
)
assert (
amount == 0
or state.balances[builder_index]
>= amount + pending_payments + pending_withdrawals + MIN_ACTIVATION_BALANCE
)
# Verify that the bid is for the current slot
assert header.slot == block.slot
# Verify that the bid is for the right parent block
assert header.parent_block_hash == state.latest_block_hash
assert header.parent_block_root == block.parent_root
# Record the pending payment
pending_payment = BuilderPendingPayment(
weight=0,
withdrawal=BuilderPendingWithdrawal(
fee_recipient=header.fee_recipient,
amount=amount,
builder_index=builder_index,
),
)
state.builder_pending_payments[SLOTS_PER_EPOCH + header.slot % SLOTS_PER_EPOCH] = (
pending_payment
)
# Cache the signed execution payload header
state.latest_execution_payload_header = header
|
Operations
Modified process_operations
Note: process_operations
is modified to process PTC attestations and removes
calls to process_deposit_request
, process_withdrawal_request
, and
process_consolidation_request
.
| 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)
# [Modified in EIP7732]
for_ops(body.attestations, process_attestation)
for_ops(body.deposits, process_deposit)
for_ops(body.voluntary_exits, process_voluntary_exit)
for_ops(body.bls_to_execution_changes, process_bls_to_execution_change)
# [Modified in EIP7732]
# Removed `process_deposit_request`
# [Modified in EIP7732]
# Removed `process_withdrawal_request`
# [Modified in EIP7732]
# Removed `process_consolidation_request`
# [New in EIP7732]
for_ops(body.payload_attestations, process_payload_attestation)
|
Attestations
Modified process_attestation
Note: The function is modified to track the weight for pending builder
payments and to use the index
field in the AttestationData
to signal the
payload availability.
| def process_attestation(state: BeaconState, attestation: Attestation) -> None:
data = attestation.data
assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state))
assert data.target.epoch == compute_epoch_at_slot(data.slot)
assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot
# [Modified in EIP7732]
assert data.index < 2
committee_indices = get_committee_indices(attestation.committee_bits)
committee_offset = 0
for committee_index in committee_indices:
assert committee_index < get_committee_count_per_slot(state, data.target.epoch)
committee = get_beacon_committee(state, data.slot, committee_index)
committee_attesters = set(
attester_index
for i, attester_index in enumerate(committee)
if attestation.aggregation_bits[committee_offset + i]
)
assert len(committee_attesters) > 0
committee_offset += len(committee)
# Bitfield length matches total number of participants
assert len(attestation.aggregation_bits) == committee_offset
# Participation flag indices
participation_flag_indices = get_attestation_participation_flag_indices(
state, data, state.slot - data.slot
)
# Verify signature
assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))
# Update epoch participation flags
current_epoch_target = True
if data.target.epoch == get_current_epoch(state):
epoch_participation = state.current_epoch_participation
payment = state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH]
else:
epoch_participation = state.previous_epoch_participation
payment = state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH]
current_epoch_target = False
proposer_reward_numerator = 0
for index in get_attesting_indices(state, attestation):
# [New in EIP7732]
# For same-slot attestations, check if we're setting any new flags
# If we are, this validator hasn't contributed to this slot's quorum yet
will_set_new_flag = False
for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS):
if flag_index in participation_flag_indices and not has_flag(
epoch_participation[index], flag_index
):
epoch_participation[index] = add_flag(epoch_participation[index], flag_index)
proposer_reward_numerator += get_base_reward(state, index) * weight
will_set_new_flag = True
# [New in EIP7732]
# Add weight for same-slot attestations when any new flag is set
# This ensures each validator contributes exactly once per slot
if will_set_new_flag and is_attestation_same_slot(state, data):
payment.weight += state.validators[index].effective_balance
# Reward proposer
proposer_reward_denominator = (
(WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT
)
proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator)
increase_balance(state, get_beacon_proposer_index(state), proposer_reward)
# Update builder payment weight
if current_epoch_target:
state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] = payment
else:
state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] = payment
|
Payload Attestations
New process_payload_attestation
| def process_payload_attestation(
state: BeaconState, payload_attestation: PayloadAttestation
) -> None:
# Check that the attestation is for the parent beacon block
data = payload_attestation.data
assert data.beacon_block_root == state.latest_block_header.parent_root
# Check that the attestation is for the previous slot
assert data.slot + 1 == state.slot
# Verify signature
indexed_payload_attestation = get_indexed_payload_attestation(
state, data.slot, payload_attestation
)
assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation)
|
Modified is_merge_transition_complete
is_merge_transition_complete
is modified only for testing purposes to add the
blob kzg commitments root for an empty list
| def is_merge_transition_complete(state: BeaconState) -> bool:
header = ExecutionPayloadHeader()
kzgs = List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]()
header.blob_kzg_commitments_root = kzgs.hash_tree_root()
return state.latest_execution_payload_header != header
|
Modified validate_merge_block
validate_merge_block
is modified to use the new
signed_execution_payload_header
message in the Beacon Block Body
| def validate_merge_block(block: BeaconBlock) -> None:
"""
Check the parent PoW block of execution payload is a valid terminal PoW block.
Note: Unavailable PoW block(s) may later become available,
and a client software MAY delay a call to ``validate_merge_block``
until the PoW block(s) become available.
"""
if TERMINAL_BLOCK_HASH != Hash32():
# If `TERMINAL_BLOCK_HASH` is used as an override, the activation epoch must be reached.
assert compute_epoch_at_slot(block.slot) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
assert (
block.body.signed_execution_payload_header.message.parent_block_hash
== TERMINAL_BLOCK_HASH
)
return
# [Modified in EIP7732]
pow_block = get_pow_block(block.body.signed_execution_payload_header.message.parent_block_hash)
# Check if `pow_block` is available
assert pow_block is not None
pow_parent = get_pow_block(pow_block.parent_hash)
# Check if `pow_parent` is available
assert pow_parent is not None
# Check if `pow_block` is a valid terminal PoW block
assert is_valid_terminal_pow_block(pow_block, pow_parent)
|
Execution payload processing
New verify_execution_payload_envelope_signature
| def verify_execution_payload_envelope_signature(
state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope
) -> bool:
builder = state.validators[signed_envelope.message.builder_index]
signing_root = compute_signing_root(
signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER)
)
return bls.Verify(builder.pubkey, signing_root, signed_envelope.signature)
|
New process_execution_payload
Note: process_execution_payload
is now an independent check in state
transition. It is called when importing a signed execution payload proposed by
the builder of the current slot.
| def process_execution_payload(
state: BeaconState,
signed_envelope: SignedExecutionPayloadEnvelope,
execution_engine: ExecutionEngine,
verify: bool = True,
) -> None:
# Verify signature
if verify:
assert verify_execution_payload_envelope_signature(state, signed_envelope)
envelope = signed_envelope.message
payload = envelope.payload
# Cache latest block header state root
previous_state_root = hash_tree_root(state)
if state.latest_block_header.state_root == Root():
state.latest_block_header.state_root = previous_state_root
# Verify consistency with the beacon block
assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header)
assert envelope.slot == state.slot
# Verify consistency with the committed header
committed_header = state.latest_execution_payload_header
assert envelope.builder_index == committed_header.builder_index
assert committed_header.blob_kzg_commitments_root == hash_tree_root(
envelope.blob_kzg_commitments
)
# Verify the withdrawals root
assert hash_tree_root(payload.withdrawals) == state.latest_withdrawals_root
# Verify the gas_limit
assert committed_header.gas_limit == payload.gas_limit
# Verify the block hash
assert committed_header.block_hash == payload.block_hash
# Verify consistency of the parent hash with respect to the previous execution payload
assert payload.parent_hash == state.latest_block_hash
# Verify prev_randao
assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state))
# Verify timestamp
assert payload.timestamp == compute_time_at_slot(state, state.slot)
# Verify commitments are under limit
assert len(envelope.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK
# Verify the execution payload is valid
versioned_hashes = [
kzg_commitment_to_versioned_hash(commitment) for commitment in envelope.blob_kzg_commitments
]
requests = envelope.execution_requests
assert execution_engine.verify_and_notify_new_payload(
NewPayloadRequest(
execution_payload=payload,
versioned_hashes=versioned_hashes,
parent_beacon_block_root=state.latest_block_header.parent_root,
execution_requests=requests,
)
)
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)
# Queue the builder payment
payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH]
exit_queue_epoch = compute_exit_epoch_and_update_churn(state, payment.withdrawal.amount)
payment.withdrawal.withdrawable_epoch = Epoch(
exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
)
state.builder_pending_withdrawals.append(payment.withdrawal)
state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = (
BuilderPendingPayment()
)
# Cache the execution payload hash
state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1
state.latest_block_hash = payload.block_hash
# Verify the state root
if verify:
assert envelope.state_root == hash_tree_root(state)
|