eth2.0-specs/specs/_features/eipxxxx/beacon-chain.md

27 KiB

EIP-XXXX -- The Beacon Chain

Table of contents

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 these 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

Payload status

Name Value
PAYLOAD_ABSENT uint8(0)
PAYLOAD_PRESENT uint8(1)
PAYLOAD_WITHHELD uint8(2)
PAYLOAD_INVALID_STATUS uint8(3)

Preset

Misc

Name Value
PTC_SIZE uint64(2**9) (=512) # (New in EIP-XXXX)

Domain types

Name Value
DOMAIN_BEACON_BUILDER DomainType('0x1B000000') # (New in EIP-XXXX)
DOMAIN_PTC_ATTESTER DomainType('0x0C000000') # (New in EIP-XXXX)

Max operations per block

Name Value
MAX_PAYLOAD_ATTESTATIONS 2**2 (= 4) # (New in EIP-XXXX)

Containers

New containers

PayloadAttestationData

class PayloadAttestationData(Container):
    beacon_block_root: Root
    slot: Slot
    payload_status: uint8

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

SignedExecutionPayloadHeader

class SignedExecutionPayloadHeader(Container):
    message: ExecutionPayloadHeader
    signature: BLSSignature

ExecutionPayloadEnvelope

class ExecutionPayloadEnvelope(Container):
    payload: ExecutionPayload
    builder_index: ValidatorIndex
    beacon_block_root: Root
    blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
    payload_withheld: boolean
    state_root: Root

SignedExecutionPayloadEnvelope

class SignedExecutionPayloadEnvelope(Container):
    message: ExecutionPayloadEnvelope
    signature: BLSSignature

Modified containers

BeaconBlockBody

Note: The Beacon Block body is modified to contain a Signed ExecutionPayloadHeader. The containers BeaconBlock and SignedBeaconBlock are modified indirectly.

class BeaconBlockBody(Container):
    randao_reveal: BLSSignature
    eth1_data: Eth1Data  # Eth1 data vote
    graffiti: Bytes32  # Arbitrary data
    # Operations
    proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
    attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
    attestations: List[Attestation, MAX_ATTESTATIONS]
    deposits: List[Deposit, MAX_DEPOSITS]
    voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
    sync_aggregate: SyncAggregate
    # Execution
    # Removed execution_payload [Removed in EIP-XXXX]
    # Removed blob_kzg_commitments [Removed in EIP-XXXX]
    bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]
    # PBS
    signed_execution_payload_header: SignedExecutionPayloadHeader   # [New in EIP-XXXX]
    payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS]  # [New in EIP-XXXX]

ExecutionPayloadHeader

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
    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):
    # Versioning
    genesis_time: uint64
    genesis_validators_root: Root
    slot: Slot
    fork: Fork
    # History
    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]  # Frozen in Capella, replaced by historical_summaries
    # Eth1
    eth1_data: Eth1Data
    eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
    eth1_deposit_index: uint64
    # Registry
    validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
    balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
    # Randomness
    randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
    # Slashings
    slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]  # Per-epoch sums of slashed effective balances
    # Participation
    previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
    current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
    # Finality
    justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]  # Bit set for every recent justified epoch
    previous_justified_checkpoint: Checkpoint
    current_justified_checkpoint: Checkpoint
    finalized_checkpoint: Checkpoint
    # Inactivity
    inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]
    # Sync
    current_sync_committee: SyncCommittee
    next_sync_committee: SyncCommittee
    # Execution
    latest_execution_payload_header: ExecutionPayloadHeader
    # Withdrawals
    next_withdrawal_index: WithdrawalIndex
    next_withdrawal_validator_index: ValidatorIndex
    # Deep history valid from Capella onwards
    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_balance_deposits: List[PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT]
    pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
    pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
    # PBS
    latest_block_hash: Hash32  # [New in EIP-XXXX]
    latest_full_slot: Slot  # [New in EIP-XXXX]
    latest_withdrawals_root: Root  # [New in EIP-XXXX]

Helper functions

Math

bit_floor

def bit_floor(n: uint64) -> uint64:
    """
    if ``n`` is not zero, returns the largest power of `2` that is not greater than `n`.
    """
    if n == 0:
        return 0
    return uint64(1) << (n.bit_length() - 1)

Predicates

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 the data is valid
    if indexed_payload_attestation.data.payload_status >= PAYLOAD_INVALID_STATUS:
        return False

    # Verify indices are sorted and unique
    indices = indexed_payload_attestation.attesting_indices
    if len(indices) == 0 or not indices == sorted(set(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)

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

Beacon State accessors

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)
    committees_per_slot = bit_floor(min(get_committee_count_per_slot(state, epoch), PTC_SIZE))
    members_per_committee = PTC_SIZE // committees_per_slot
    
    validator_indices: List[ValidatorIndex] = [] 
    for idx in range(committees_per_slot):
        beacon_committee = get_beacon_committee(state, slot, CommitteeIndex(idx))
        validator_indices += beacon_committee[:members_per_committee]
    return validator_indices

Modified get_attesting_indices

get_attesting_indices is modified to ignore PTC votes

def get_attesting_indices(state: BeaconState, attestation: Attestation) -> Set[ValidatorIndex]:
    """
    Return the set of attesting indices corresponding to ``aggregation_bits`` and ``committee_bits``.
    """
    output: Set[ValidatorIndex] = set()
    committee_indices = get_committee_indices(attestation.committee_bits)
    committee_offset = 0
    for index in committee_indices:
        committee = get_beacon_committee(state, attestation.data.slot, index)
        committee_attesters = set(
            index for i, index in enumerate(committee) if attestation.aggregation_bits[committee_offset + i])
        output = output.union(committee_attesters)

        committee_offset += len(committee)

    ptc = get_ptc(state, attestation.data.slot)
    return Set([i for i in output if i not in ptc])

get_payload_attesting_indices

def get_payload_attesting_indices(state: BeaconState, slot: Slot, 
                                  payload_attestation: PayloadAttestation) -> Set[ValidatorIndex]:
    """
    Return the set of attesting indices corresponding to ``payload_attestation``.
    """
    ptc = get_ptc(state, slot)
    return set(index for i, index in enumerate(ptc) if payload_attestation.aggregation_bits[i])

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``.
    """
    attesting_indices = get_payload_attesting_indices(state, slot, payload_attestation)

    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-XXXX. 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 a uint64 overflow or underflow are also considered invalid.

Block processing

def process_block(state: BeaconState, block: BeaconBlock) -> None:
    process_block_header(state, block)
    process_withdrawals(state)  # [Modified in EIP-XXXX]
    process_execution_payload_header(state, block)  # [Modified in EIP-XXXX, removed process_execution_payload]
    process_randao(state, block.body)
    process_eth1_data(state, block.body)
    process_operations(state, block.body)  # [Modified in EIP-XXXX]
    process_sync_aggregate(state, block.body.sync_aggregate)

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, partial_withdrawals_count = get_expected_withdrawals(state)
    state.latest_withdrawals_root = hash_tree_root(withdrawals)
    for withdrawal in withdrawals:
        decrease_balance(state, withdrawal.validator_index, withdrawal.amount)

    # Update pending partial withdrawals
    state.pending_partial_withdrawals = state.pending_partial_withdrawals[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

New verify_execution_payload_header_signature

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)

New process_execution_payload_header

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)

    # Check that the builder has funds to cover the bid
    header = signed_header.message
    builder_index = header.builder_index
    amount = header.value
    assert state.balances[builder_index] >= amount

    # 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

    # Transfer the funds from the builder to the proposer
    decrease_balance(state, builder_index, amount)
    increase_balance(state, block.proposer_index, amount)

    # Cache the signed execution payload header 
    state.latest_execution_payload_header = header

Modified process_operations

Note: process_operations is modified to process PTC attestations

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)
    for_ops(body.voluntary_exits, process_voluntary_exit)
    for_ops(body.bls_to_execution_changes, process_bls_to_execution_change)
    # Removed `process_deposit_request` in EIP-XXXX
    # Removed `process_withdrawal_request` in EIP-XXXX
    # Removed `process_consolidation_request` in EIP-XXXX
    for_ops(body.payload_attestations, process_payload_attestation)  # [New in EIP-XXXX]
Payload Attestations
def remove_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags:
    flag = ParticipationFlags(2**flag_index)
    return flags & ~flag
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)

    if state.slot % SLOTS_PER_EPOCH == 0:
        epoch_participation = state.previous_epoch_participation
    else:
        epoch_participation = state.current_epoch_participation

    # Return early if the attestation is for the wrong payload status
    payload_was_present = data.slot == state.latest_full_slot
    voted_present = data.payload_status == PAYLOAD_PRESENT
    proposer_reward_denominator = (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT
    proposer_index = get_beacon_proposer_index(state)
    if voted_present != payload_was_present:
        # Unset the flags in case they were set by an equivocating ptc attestation
        proposer_penalty_numerator = 0
        for index in indexed_payload_attestation.attesting_indices:
            for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS):
                if has_flag(epoch_participation[index], flag_index):
                    epoch_participation[index] = remove_flag(epoch_participation[index], flag_index)
                    proposer_penalty_numerator += get_base_reward(state, index) * weight
        # Penalize the proposer
        proposer_penalty = Gwei(2 * proposer_penalty_numerator // proposer_reward_denominator)
        decrease_balance(state, proposer_index, proposer_penalty)
        return

    # Reward the proposer and set all the participation flags in case of correct attestations
    proposer_reward_numerator = 0
    for index in indexed_payload_attestation.attesting_indices:
        for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS):
            if 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

    # Reward proposer
    proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator)
    increase_balance(state, proposer_index, proposer_reward)

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)

Modified 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=True) -> None:
    # Verify signature
    if verify: 
        assert verify_execution_payload_envelope_signature(state, signed_envelope)
    envelope = signed_envelope.message
    payload = envelope.payload
    # Verify consistency with the beacon block
    assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header)

    # Verify consistency with the committed header
    committed_header = state.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)

    if not envelope.payload_withheld: 
        # Verify the withdrawals root
        assert hash_tree_root(payload.withdrawals) == state.last_withdrawals_root

        # Verify the gas_limit
        assert committed_header.gas_limit == payload.gas_limit

        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_timestamp_at_slot(state, state.slot)
        # Verify the execution payload is valid
        versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) 
                            for commitment in envelope.blob_kzg_commitments]
        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,
            )
        )

        # Process Electra operations
        def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
            for operation in operations:
                fn(state, operation)

        for_ops(payload.deposit_requests, process_deposit_request)
        for_ops(payload.withdrawal_requests, process_withdrawal_request)
        for_ops(payload, process_consolidation_request)

        # Cache the execution payload header and proposer
        state.latest_block_hash = payload.block_hash
        state.latest_full_slot = state.slot

    # Verify the state root
    if verify: 
        assert envelope.state_root == hash_tree_root(state)