# 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. ```python 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. ```python 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: ```python 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`. ```python 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.