Try   HackMD

ePBS Annotated Validator Spec

Validator assignment

PTC assignment

This section explains how a validator determines whether it performs PTC duty in the current or next epoch. Similar to determining if it is responsible for submitting beacon attestations during a slot across an epoch, PTC attestation is a subset type where the PTC committee is selected by choosing a few validators from the end of each beacon attestation committee.

def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
    """
    Get the ptc 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 = [] 
    for idx in range(committees_per_slot):
        beacon_committee = get_beacon_committee(state, slot, idx)
        validator_indices += beacon_committee[:members_per_committee]
    return validator_indices

Retrieving the assignment utilizing get_ptc through requested epoch and validator_index parameters. If the validator exists in the PTC then return the assignment slot is returned otherwise, false is returned. get_ptc_assignment should be called at the start of each epoch to get the assignment for the current and next epoch (current_epoch + 1). A validator should plan for future assignments by noting their assigned PTC committee slot and planning to participate in the PTC gossip subnet.

def get_ptc_assignment(state: BeaconState,
                             epoch: Epoch,
                             validator_index: ValidatorIndex
                             ) -> Optional[Slot]:
    """
    Returns the slot during the requested epoch in which the validator with index `validator_index`
    is a member of the PTC. Returns None if no assignment is found. 
    """
    next_epoch = Epoch(get_current_epoch(state) + 1)
    assert epoch <= next_epoch

    start_slot = compute_start_slot_at_epoch(epoch)
    for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH):
            if validator_index in get_ptc(state, Slot(slot)):
                return Slot(slot)
    return None

Beacon attesters and sync committee

Given intervals_per_slot modified from 3 to 4, beacon attesters and sync committee timing moved from 4s into the slot to 3s into the slot.

Beacon chain responsibilities

This section explains validator duty changes under ePBS. We'll divide them into the following subsections

  1. Modified: proposer block duty
  2. New: proposer inclusion list duty
  3. New: PTC attester duty

Proposer block duty

At the beginning of the slot, the proposer assigned for that slot prepares and broadcasts a SignedBeaconBlock. The high-level modifications are as follows:

  • Instead of including an ExecutionPayload on block body, the validator includes SignedExecutionPayloadHeader in the block body. The details of how to obtain the header is outside the scope of this document. A proposer can obtain the header through local buildings, p2p gossip networks, or other avenues like the builder's RPC ports. Strategies for dealing with the header will be discussed in the following proposer block header selection section.
  • BlobSidecars is no longer part of the block body. This duty is transferred to the builders.
  • PayloadAttestation is added to the block body. It consists of the aggregate of the PayloadAttestationData. The strategy for honest aggregation will be defined in the PTC attestation aggregation section.

Proposer block header selection

The proposer selects the SignedExecutionPayloadHeader to be included in the block. The header can be trusted by the local EL client or untrusted by P2P network or builder RPC interface.
If the header comes from a trusted source, the proposer can include it as is and broadcast both SignedBeaconBlock and SignedExecutionPayloadEnvelope in the respective gossip nets simultaneously.
If the header is coming from an untrusted source, the proposer should verify the header is valid before including it in their block. The header must pass the validation steps outlined in process_execution_payload_header, which are:

def validate_execution_payload_header(state: BeaconState, signed_header: `SignedExecutionPayloadHeader`) -> None:
	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

If the header is sourced from a p2p network, the p2p gossip validation pipeline has likely already performed the necessary checks, so there is no need to validate it again. However, if the header is obtained through builder RPC, the check should be performed separately to ensure its validity before inclusion in the block.

Proposer PTC attestation aggregation

The proposer aggregates recently verified PayloadAttestationMessage into PayloadAttestation for inclusion in the block body. Up to MAX_PAYLOAD_ATTESTATIONS can be included. The honest PTC selection and aggregation strategy are defined as follows:

  • Advance head state state to the proposal slot.
  • The proposer retrieves all PayloadAttestationMessage from the local mempool where data.slot + 1 == state.slot. It is assumed that the PTC attestations have been verified as they came through the P2P gossip network.
  • The proposer determines which payload_status to include by checking if the payload was supposed to be present in the previous slot using state.latest_full_slot == slot - 1. If the payload was supposed to be present, it filters for PayloadAttestationMessage where data.payload_status == PAYLOAD_PRESENT. If the payload is not supposed to be present, it filters for data.payload_status == PAYLOAD_ABSENT || data.payload_status == PAYLOAD_WITHHELD.
  • The proposer now has all the payload attestations that satisfy the slot and payload status requirements. It aggregates these attestations similarly to how beacon attestations are aggregated. It fills the payload_attestation.aggregation_bit s field using the relative position of the validator indices with respect to the PTC obtained from `get_ptc(state, slot—1)``.
  • Finally, the proposer fills the payload_attestation.signature field with the aggregated signature and includes it in the beacon block.

Note:

  1. Including a payload status that diverges from the canonical chain's view will result in the proposer being penalized by 2*proposer_penalty_numerator // proposer_reward_denominator per PTC attestation.
  2. In the beacon chain's state transition, the payload attestation processing function will unset the flags if they were set by an equivocating PTC attestation. The justification for this approach is that there are no slashing conditions for equivocations on the PTC attestations. Attesters are incentivized to submit both PAYLOAD_PRESENT and PAYLOAD_ABSENT attestations, and proposers are incentivized to include both, allowing them to be rewarded in either case.

PTC attestation duty

Suppose the attester is part of the PTC, as determined by get_ptc_assignment for your validator index in any of the slots of the current or next epoch. In that case, the PTC attester should prepare and broadcast a PayloadAttestationMessage at the SECONDS_PER_SLOT * 3 / INTERVALS_PER_SLOT mark of the slot. The PayloadAttestationData includes the beacon_block_root, which is the block root of the current slot, and the slot, which refers to the current slot. The payload_status is defined as follows.

Consensus block status Execution payload status Payload status
seen on time PAYLOAD_PRESENT
seen late PAYLOAD_ABSENT
seen withhold PAYLOAD_WITHHOLD
late on time PAYLOAD_PRESENT
late late PAYLOAD_ABSENT
late withhold PAYLOAD_WITHHOLD
skipped skipped do nothing

Assuming you have PayloadAttestationData, you can construct a PayloadAttestationMessage by signing over the PayloadAttestationData. Then, input validator_index as your index and signature as the result of the get_payload_attestation_data_signature.

def get_payload_attestation_data_signature(state: BeaconState, data: PayloadAttestationData, privkey: int) -> BLSSignature:
    domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(data.slot))
    signing_root = compute_signing_root(data, domain)
    return bls.Sign(privkey, signing_root)

No change to attester duty

If the validator, assumed to be an attester of a slot, is also part of the PTC, one might argue that attester duty can be skipped since the reward will not be counted. However, for implementation simplicity, it is recommended to just gossip the attestation anyway, as there is no penalty, and signing and verifying the signatures are inexpensive. The benefit is that the attestation's fork choice weight remains useful over P2P.