# ePBS specification notes ## Introduction This document accompanies the ePBS specification found [here](https://github.com/potuz/consensus-specs/pull/2). It relies heavily in the design constrains outlined [here](https://ethresear.ch/t/epbs-design-constraints/18728). Some of the definitions and notations from that document are assumed throughout, although we will try to recall them here to make this document as self-contained as possible. For many reasons detailed below, ePBS requires EIP 7251 and EIP 7002, the changes needed to accommodate those two EIPs are simple but including them in this simplified specification clutters so much the reading that it makes it hard to distinguish the actual changes that are due to ePBS. We will make notice of the relevant areas that need to be changed with this regard. The specification solves the main problem detailed in the [design constraints](https://ethresear.ch/t/epbs-design-constraints/18728#h-22-what-is-the-problem-itself-7) > The main problem is that in the current implementation of PBS in Ethereum > 1. A proposer that wants to sell his right to build a payload **must** trust an intermediary. > 2. A builder that wants to buy the rights to build a payload **must** trust an intermediary. - It solves the problem by removing the **must** moniker and turning that into a **may**. - It does so *minimally* that is, it includes the minimum set of changes to keep the design as close as it currently works on consensus and execution clients. - The slot time remains at 12 seconds. - It guarantees some censor resistance with the use of forward forced inclusion lists in the spirit of EIP 7547. - Most changes are bound to the consensus layer with the only changes on the EL due to inclusion lists. - It guarantees proposer ex-anti and post-anti 1-slot reorg safety against colluding attackers of proposers and builders that control the network topology with up to $20\%$ fo the stake :::success 20% but the attacker looses a full payload, rendering this useless. ::: - It guarantees builders safety (both withholding and reveal) against colluding consecutive proposers controlling the network topology and up to $20\%$ of the stake. - It guarantees same slot unbundling for builders under all attacking conditions. - It maintains the ability for validators to self-build their payload. - It is composable with other constructions like slot auctions or execution ticket auctions. ### Extra benefits #### Better CPU pipelining The slot is more homogenously distributed with consensus validation happening at second 0, payload validation at second 6 and inclusion list validation happening throughout the slot. #### Better incentives for blob inclusion Blob data can be broadcast immediately as soon as the beacon block is revealed, before attestations are seen and before the payload is revealed. Data availability is no longer bound to the beacon block but to the payload, providing thus stronger forkchoice guarantees against head split views due to data not being available. #### Fast direct connection to builders Builders are allowed (and encouraged) to open direct lines of communications to proposers, avoiding thus extra latency of a trusted relay hop. These can now be trustless without any investment on the builder side. Bid request may be obfuscated optionally, allowing the auction to be effectively blind and discouraging the use of centralized trusted relay networks. #### Better and Faster Builder API The builder API loses half of its current complexity as there is no need for a blinded block pipeline through the builder and reassembling of a full block. The builder API reduces to simply requesting directly the bids. #### Direct access to non-censoring builder By having direct connections to builders validators can choose directly those that advertise as non-censoring. #### Automatic blacklisting of builders Clients can directly blacklist specific builders in liveness events as the ones that affected mainnet. #### Better testing and R&D framework All involved software is maintained by core devs and all edge cases can be tested in the current infrastructure: spectests, kurtosis, CI, etc. without having to rely on external teams nor blackboxed closed source software. ### Organization of this document In [A brief description](#A-brief-description) we give an overview of the main changes. In [Anatomy of a slot](#Anatomy-of-a-slot) we describe the events as they occur during the slot. We introduce the main new structures and the main changes as to how validators need to produce blocks and payloads. In [The beacon state](#The-beacon-state) we describe the new field additions to the beacon state structure. In [Beacon block's timeline](#Beacon-blocks-timeline) we describe the full procedure to sync a consensus layer block, starting from gossip validation until the block is fully validated by the beacon node. In [Inclusion list timeline](#Inclusion-lists-timeline) we go over the necessary validations for inclusion list, this is the main area where the execution layer is involved. In [Execution payload's timeline](#Execution-payloads-timeline) we cover the validation pipeline for execution blocks. In [Payload attestation' s timeline](#Payload-attestations-timeline) we go over the new *payload timeliness attestations* validation. In [Honest validator behavior](#Honest-validator-behavior) we describe how validators need to perform their duties, we pay particular attention to the changes in how to chose the head of the chain in order to propose and to attest for the timeliness of a payload. In [Honest builder behavior](#Honest-builder-behavior) we describe how honest builders need to prepare their bids and broadcast their payloads or *payload withheld messages*. In [Forkchoice considerations](#Forkchoice-considerations) we include several examples of how LMD votes are counted in forkchoice in the presence of PTC attestations, payload boosts and inclusion list availability conditions. In [Security analysis](#Security-analysis) we prove the above statements with regards to builder and proposer safety under certain reorg and withholding attacks. In [No trusted advantage](#No-trusted-advantage) we describe why this system incentivizes builders to open their own trustless endpoints instead of relying on a trusted network. In [Unconditional payment](#Unconditional-payment) we prove this property and delve on the simplifications of not having included EIP 7002 and EIP 7251 in this specification. In [Optional changes](#Optional-changes) we briefly mention some changes that are possible to this proposal without affecting it's core properties. In [Comparison with other approaches](#Comparison-with-other-approaches) we mention some advantages of the current proposal for ePBS versus some other sweeping changes that may be applied in the future. We end with a quick [FAQ](#FAQ). ### Acknowledgments Hardly nothing in this specification is originally due to the author. The core of this work goes back several years ago barely changed. An infinitude of researchers and core devs have helped throughout the years to pile up knowledge on the problems and the different design trade-offs that we face. We'd be surely insulting some by trying to name them all. You know who you are! ## A brief description The current proposal brings new duties or attributions to validators. These are - Builders: these are validators that submit bids to proposers to commit them to a specific execution payload. - PTC: This is a new committee that attest for the timely presence (and validity) of the builder's payload. The proposers collect bids from the builders and submit their beacon blocks with a signed commitment from the builder to reveal a payload. Validators immediately deduct the bid amount from the builder and pay the proposer. Attesters **only attest** to the consensus layer block. Builders later in the slot reveal and broadcast their full execution payload fulfilling thus the consensus block commitment. Slots can be either missed (no beacon block was produced), empty (a beacon block was produced but the corresponding execution payload was not revealed) and full (both blocks have been produced). The consensus layer attesters decide between the first and the latter two, while the PTC decides between the latest two. Proposers submit inclusion lists along with their blocks, these inclusion lists contain transactions that the builder of the next block **has to include** in order for the block to be valid. These are commonly known as *forward forced inclusion lists*. ## Anatomy of a slot ### Before the start of the slot There are three new components that are added in ePBS and two that are removed from the beacon block proposals in ePBS. The additions are *Inclusion Lists*, *Execution Payload Bids* and *Payload Attestations*. The removals are the full Execution Payloads and the Blob sidecars (that are now broadcast by the builders.) Before the start of the slot when the proposer is expected to construct and reveal his block, the proposer can start preparing the three new additions. #### Inclusion Lists As soon as the proposer knows that it will be proposing the next slot, and has figured out his head, for example if the current slot has been filled and is canonical, this can happen 6 seconds before the slot starts, he can requests a full inclusion list. This is an `Inclusion List` object. ```python class InclusionListSummary(Container) proposer_index: ValidatorIndex slot: Slot summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` ```python class SignedInclusionListSummary(Container): message: InclusionListSummary signature: BLSSignature ``` ```python class InclusionList(Container) signed_summary: SignedInclusionListSummary parent_block_hash: Hash32 transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` The list of transactions and the summary of addresses is obtained from the local EL by the engine API. The proposer then fills in the consensus information and signs the summary. :::info The proposer can immediately broadcast his IL, ahead of the slot start, for quick validation of the network. It can submit multiple, conflicting ILs if it so desires. ::: #### Builder's bids Before the slot starts, builders can start sending bids over the p2p network or the proposer can start requesting them directly from the builders. The bids consists of `SignedExecutionPayloadHeader` objects ```python class SignedExecutionPayloadHeader(Container): message: ExecutionPayloadHeader signature: BLSSignature ``` ```python class ExecutionPayloadHeader(Container): parent_block_hash: Hash32 parent_block_root: Root block_hash: Hash32 builder_index: ValidatorIndex slot: Slot value: Gwei blob_kzg_commitments_root: Root ``` They contain enough information to: - Guarantee unconditional payment to the proposer. - Guarantee a slashing condition by committing blob equivocations. - Guarantee consistency with the parent hash of the proposer's view. :::info Both parent block hash and parent block root are required because there may have been successful *empty* consensus blocks between the parent full block and the current one. The proposer's consensus block may take any of them as valid, and the corresponding state root computation will differ in each case. ::: #### Payload attestations There is a new committee in ePBS consisting of `PTC_SIZE` (512) validators that cast attestations every slot as seen below. They broadcast their attestations as `PayloadAttestationMessage` objects ```python class PayloadAttestationData(Container): beacon_block_root: Root slot: Slot payload_status: uint8 ``` ```python class PayloadAttestationMessage(Container): validator_index: ValidatorIndex data: PayloadAttestationData signature: BLSSignature ``` We opted for having a different type than the aggregated version included in blocks: ```python class PayloadAttestation(Container): aggregation_bits: BitVector[PTC_SIZE] data: PayloadAttestationData signature: BLSSignature ``` The proposer of the next slot listens for these payload attestation messages and aggregates them into `PayloadAttestation` objects to be packed in their `SignedBeaconBlock`. ### Second 0 At the beginning of the slot, the proposer assigned for that slot prepares and broadcasts a `SignedBeaconBlock`. In order to do so they first need to select a builder's bid and include the corresponding `SignedExecutionPayloadHeader` in their beacon block. The proposer requests an inclusion list from its EL and broadcasts it if he hasn't done so already. ### Between second 0 and second 3 During this time, the network has already gossiped the `SignedBeaconBlock` and the `InclusionList`. Validators will verify the proposer's signature of the `SignedInclusionListSummary` and forward the full inclusion list for the EL to validate. Validators independently run the state transition function on the consensus block to validate it. ### Second 3 3 seconds into the slot (or as soon as they have validated both consensus block and inclusion list), validators attest for the presence of the corresponding beacon block and inclusion list. Validation of this block is fast at pre-merge times given that there is no execution to validate at this time of the slot. Propagation is also faster given that there are no blobs to be required at this stage and the block itself is lighter without the execution payload. There is no new requirement for validators in what attestations are concerned. ### Second 6 Two things happen at the same time at half the slot time. - Aggregators listening to their subnets aggregate and submit attestation aggregates. - Builders broadcast their execution payload Nothing changes for aggregators except the tighter deadline from 8 seconds to 6 seconds. Builders on the other hand have been watching all subnets and by 6 seconds should have a clear idea if the consensus layer block has been seen by a large portion of the network. :::info Honest builders have an option to withhold their payload in the event they have seen a consensus block committing to their payload but which has not been thoroughly voted. More information about this is found below. ::: Builders send their execution payload as an `SignedExecutionPayloadEnvelope` object ```python class ExecutionPayload(Container): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper state_root: Bytes32 receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] prev_randao: Bytes32 # 'difficulty' in the yellow paper block_number: uint64 # 'number' in the yellow paper gas_limit: uint64 gas_used: uint64 timestamp: uint64 extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 # Extra payload fields block_hash: Hash32 # Hash of execution block transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] blob_gas_used: uint64 excess_blob_gas: uint64 inclusion_list_summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST]# [New in ePBS] ``` ```python class ExecutionPayloadEnvelope(Container): payload: ExecutionPayload builder_index: ValidatorIndex beacon_block_root: Root blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] inclusion_list_proposer_index: ValidatorIndex inclusion_list_slot: Slot inclusion_list_signature: BLSSignature payload_withheld: bool state_root: Root ``` ```python class SignedExecutionPayloadEnvelope(Container): message: ExecutionPayloadEnvelope signature: BLSSignature ``` The envelope contains all the information that is irrelevant to the EL, the beacon node requires this information to process the payload on the consensus side. Essentially it is enough information to reassemble the `SignedInclusionListSummary` to verify the BLS signature of the proposer, and the list of `blob_kzg_commitments` that are needed to verify data availability of blobs on the CL. Crucially, the envelope contains a new field `payload_withheld` to allow the builder to honestly withhold their execution payload and signal that they have seen a consensus block that was not timely. The execution payload itself is essentially unchanged from Cancun: we add an inclusion list summary (just the list of addresses) that is enough information for the EL to check if this summary is satisfied with the current payload. :::info The `state_root` is required because processing the execution payload is now a new independent state transition function in the beacon execution. This is taken out of the payload header since it can only be computed after the consensus block has been successfully synced. This also implies that a builder **cannot** broadcast their payload until they have seen and fully validated a consensus block. ::: ### Second 9 Every slot, 512 validators are chosen to be part of a *payload timeliness committee* (PTC). Those validators attest, at 9 seconds into the slot if they have seen the expected execution payload timely. This committee can vote in three different ways - If they have seen a consensus block for the current slot and the corresponding payload was seen timely with `payload_withheld = False` in the envelope, they vote `PAYLOAD_PRESENT`. - If they have seen a consensus block for the current slot and the corresponding payload was seen timely with `payload_withheld = True` in the envelope, they vote `PAYLOAD_WITHHELD`. - If they have not seen any consensus block in the current slot, of if they have seen one but the corresponding payload was not seen they vote `PAYLOAD_ABSENT`. These attestations are gossiped on a global topic. ### End of the slot By the end of the slot, validators have thus imported and validated an inclusion list, a consensus block, single bit attestations and aggregated attestations, payload attestations and a full execution payload. They can evaluate the new head of the blockchain. This can now be either of the three options: - A *full* block, that is both the consensus block and the corresponding execution payload have been imported. - An *empty* block, that is the consensus block was imported, but the execution payload was not revealed on time. - A *skipped* slot, that is when the consensus block was imported. ## The beacon state The beacon chain state is modified every time we sync a beacon block and an execution payload header. The main structure `BeaconState` is modified in ePBS with the following additions and one removal: ```python class BeaconState(Container): ... # latest_execution_payload_header: ExecutionPayloadHeader # [Removed in ePBS] ... # PBS previous_inclusion_list_proposer: ValidatorIndex # [New in ePBS] previous_inclusion_list_slot: Slot # [New in ePBS] latest_inclusion_list_proposer: ValidatorIndex # [New in ePBS] latest_inclusion_list_slot: Slot # [New in ePBS] latest_block_hash: Hash32 # [New in ePBS] latest_full_slot: Slot # [New in ePBS] execution_payload_header: ExecutionPayloadHeader # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] ``` The first four entries are necessary to deal with forward inclusion lists. The new entry `latest_block_hash` replaces the old `latest_execution_payload_header` which was never used in full. The entry `latest_full_slot` records the last slot for which this state had an execution payload processed. The entry `signed_execution_payload_header` keeps the last builder's bid that is committed to the state to enforce unconditional payment to the proposer. The last addition is the `last_withdrawals_root` which records the hash tree root of the latest withdrawal messages that have been deducted in the beacon chain but have not been yet fulfilled in the execution layer. The need for each of these fields will be duly explained in the next sections. ## Beacon block's timeline ### gossip A `SignedBeaconBlock` first enters clients through it's gossip (or RPC) pipeline. There are no new validations made on the block, and all validations pertaining the execution payload or KZG commitments are removed. The only additions are the requirements that the parent beacon block is valid. This was removed in Bellatrix due to optimistic sync, but we can require this again given that a node that is optimistically syncing would still have a valid consensus block as parent. In the case of fully synced nodes, we require that the parent execution payload is fully validated. ### `on_block` handler After gossip validation, a node performs extra validations on the beacon block. A block has two different parents, it has a parent consensus layer block, referenced by `block.parent_root` and it has a parent execution layer block which can be extracted from the `signed_execution_payload_header` entry in the beacon block body. This structure has changed as follows: ```python 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 ePBS] # Removed blob_kzg_commitments [Removed in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # PBS signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] # [New in ePBS] ``` From the `header.parent_block_hash` entry we find which execution block is the parent payload of the current block. From the parent's consensus block's committed execution payload header we retrieve two different hashes, `parent_header.block_hash` and `parent_header.parent_block_hash`. There are two possible scenarios for `header.parent_block_hash`: - it equals `parent_header.block_hash`, in which case this block's parent is a *full* block. - it equals `parent_header.parent_block_hash`, in which case this block's parent is an empty block Any other outcome means the incoming block is invalid. If the parent block is a *full block* (resp. an *empty* block) we fetch the post state after syncing the parent payload (resp. the parent beacon block), this is the pre state for our incoming beacon block. ### State transition After fetching the parent state, we can perform the state transition function. There following functions are modified in ePBS ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) # removed process_withdrawals(state) [Modified in ePBS] process_execution_payload_header(state, block) # [Modified in ePBS, removed process_execution_payload] process_randao(state, block.body) process_eth1_data(state, block.body) process_operations(state, block.body) # [Modified in ePBS] process_sync_aggregate(state, block.body.sync_aggregate) ``` #### Withdrawals Withdrawals are processed in two steps. When processing a consensus block, during state transition, the validators balances are deducted their withdrawal amounts. The next execution payload that is included **must fulfill these withdrawals**. :::info The reason for this two-step approach rather than simply processing withdrawals with the execution payload is that withdrawals are beacon state based and can only be known at the moment of preparing the payload. However, the payload's hash is committed in the beacon block body which is revealed before the prestate for the payload is known. ::: Thus, withdrawals are then obtained directly from the beacon state, not from the beacon block, and processed immediately on the consensus block. If the parent block is empty, no withdrawals are processed because the last processed withdrawals cannot have been fulfilled. Otherwise processing is exactly as in Capella. The `hash_tree_root` of the list of processed withdrawals is recorded in the beacon state, to later compare with the execution payload that needs to fulfill them. ```python def process_withdrawals(state: BeaconState) -> None: ## return early if the parent block was empty if !is_parent_block_full(state): return withdrawals = get_expected_withdrawals(state) state.last_withdrawals_root = hash_tree_root(withdrawals) for withdrawal in withdrawals: decrease_balance(state, withdrawal.validator_index, withdrawal.amount) ... ``` #### Execution payload header The beacon block no longer has a full execution payload, but rather includes only a signed execution payload header. Validation of this header consists basically on the following - We check that the signature of the builder is valid. - We check that the header's parent hash corresponds to the current block's parent hash. - We check that the builder has enough funds to pay the bid and immediately transfer the amount to the proposer. :::warning The above immediate transfer is a simplification in this version of the spec stripped out of EIP 7002 and EIP 7251. With Max EB large validator transfers can be implemented by this mechanism and this is something that can be exploited to transfer funds from slashed validators (or soon to be slashed validators) for example. A simple modification is to deduct immediately the builder, but add the amount into the deposit queue for the proposer. In case the builder is slashed extra mechanisms can be added at epoch processing. ::: The last step of execution payload header processing is the record of the inclusion lists proposers and the signed execution payload header in the beacon state: ```python def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None: # Verify the header signature ... # Cache the inclusion list proposer if the parent block was full if is_parent_block_full(state): state.latest_inclusion_list_proposer = block.proposer_index state.latest_inclusion_list_slot = block.slot # Cache the signed execution payload header state.signed_execution_payload_header = signed_header ``` These steps are detailed below in the [Inclusion lists](#inclusion-lists) section. #### Payload attestations The remaining change in block processing pertains the new `PayloadAttestation`s that are included in the beacon block body. These include two changes, we change the function `process_attestations` so as to ignore any attestations from the PTC. This is not strictly needed, the PTC is built by taking a few validators from the end of each attestation committee. This is performed in the new function `get_ptc`: ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ Get the ptc 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_commitee] return validator_indices ``` We could modify the function `get_beacon_committee` to not return the PTC members in turn, we decided with this simplified version to minimize the spec diff. Payload attestations in the beacon block are processed as follows. We enforce that the attestation is from the previous slot and for the parent beacon block root. :::info The inclusion of the payload attestations in the beacon block are mostly to reward/penalize the PTC attesters and thus incentivize them to act in their turn. They also serve for a proposer to assert their view of the parent head in case they include enough PTC votes obtaining a quorum. The latter is only useful in the case of the parent block. As for the former, this 1-slot imposition penalizes PTC members every time that there is a missed slot, but we feel that the extra simplicity and reduction in the block size that is gained, outweighs the single attestation penalty that 512 validators would receive in these cases. ::: The core of the processing function consists of two branches, if the payload attestation is consistent with the beacon state status or not. We say that the payload attestation is consistent if for example it voted for `PAYLOAD_PRESENT` and the last slot was full, that is `data.slot == state.latest_full_slot`. If the vote was for `PAYLOAD_ABSENT` or `PAYLOAD_WITHHELD` and the last full slot was empty, then we consider them consistent. In this case the proposer is rewarded for including the attestation and the attester is rewarded as a full attestation: ```python= # 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 += base_reward * weight # Reward proposer proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) increase_balance(state, proposer_index, proposer_reward) ``` In case the attestation is inconsistent we penalize both the proposer and the attester for a full attestation: ```python= # Unset the flags in case they were set by an equivocating ptc attestation proposer_penalty_numerator = 0 for index in indexed_payload_atterstation.attesting_indices: for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): if has_flag(epoch_participation[index], flag_index): epoch_participation[index] = remove_flag(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 ``` The justification for this approach is that there are no slashing conditions for equivocations on the PTC attestations. Attesters would be incentivized to submit both attestations `PAYLOAD_PRESENT` and `PAYLOAD_ABSENT` and proposer incentivized to include both as well, to be rewarded in either case. :::warning The reason for doubling the penalty for the proposer is that otherwise proposer and attester could still collude in sending an equivocation, and ordering first the inconsistent attestation and then the consistent one. In this case the proposer will be both penalized and rewarded for a net result of zero. While the attester will be rewarded as the first pass would clear the flags and the second pass would set them. By penalizing the proposer twice the amount we align incentives into not including bad attestations that would anyway count for a full penalty for the attester. ::: ## Inclusion list's timeline Inclusion lists are at the core of the censorship resistance needed in any enshrined PBS system. The flavour this specification uses is that of *forward inclusion*, with a design paralleling that of EIP 7547. The system is relatively simple: proposers broadcast separate `InclusionList` objects as described in section [inclusion lists](#Inclusion-Lists) and the builder of the next block has to satisfy them. There are however several edge cases that need to be considered. ### Gossip Inclusion lists first enter our validation pipeline through gossip. `InclusionList` objects are propagated in the `inclusion_list` global topic. The gossip validation performed are all straightforward - We check that the IL is for the current or the next slot (we allow explicitly a proposer to broadcast its IL before the start of the slot for better CPU pipelining) - We only broadcast one IL for a valid pair (proposer, slot). Proposers are still allowed to submit different ILs to different peers. - We check that the list of transactions is the same length as the list in the summary and we check that both are less than `MAX_TRANSACTIONS_PER_INCLUSION_LIST`. - We check that the signature is valid for the given proposer and that the proposer is supposed to be proposing in the given slot according to our current shuffling. There are no EL validations in the gossip stage. :::warning There is a risk that a proposer broadcasts an IL for the next slot, and head changes at second 0. May be no one has the new IL due to gossip rules. The IL will still be considered available. ::: ### `on_inclusion_list` handler Once an inclusion list has passed the pubsub validation process, the remaining steps are mostly performed in the EL. The forkchoice `on_inclusion_list` handler is essentially a wrapper to an execution engine API call. Validators may (and should) validate an inclusion list that arrives before the corresponding beacon block, by checking the proposer index and the corresponding slot against the head state. The handler in the python specification assumes that the beacon block has been already processed and is found in the store. If the parent block was *empty* any inclusion list is ignored and considered automatically available. The reason for this is that every time there is an empty block, the last inclusion list was not yet fulfilled, if we allow proposers after empty blocks to submit inclusion lists, we would create a backlog of unfulfilled inclusion lists that could never be cleared. :::info Thus we require that there is a one to one correspondence between inclusion lists and **full** blocks. ::: This is the reason why we keep the entries ``` previous_inclusion_list_proposer: ValidatorIndex # [New in ePBS] previous_inclusion_list_slot: Slot # [New in ePBS] latest_inclusion_list_proposer: ValidatorIndex # [New in ePBS] latest_inclusion_list_slot: Slot # [New in ePBS] ``` in the beacon state. The `latest` entries are stored when we process a beacon block **whose parent was full** this is the latest inclusion list that we have validated. The `previous` entries are the entries of the inclusion list that still needs to be fulfilled. When we process an execution payload fulfilling them, we set the `previous = latest`. If the parent block was full, then we send the inclusion list for EL validation. ### EL validation The EL must perform several validations for an inclusion list. We let the current state be the execution state right after importing the block with `inclusion_list.parent_block_hash`. - Check that every transaction in `inclusion_list.transactions` is includable at the current state. - Check that the list `inclusion_list.signed_summary.message.summary` consists of ordered list of "from" addresses for those transactions in `inclusion_list.transactions`. - Check that the total `gas_limit` specified by these transactions is less than `MAX_GAS_PER_INCLUSION_LIST`. - Check that the accounts in the "from" list of addresses have enough funds to cover `(base_fee_per_gas + base_fee_per_gas / BASE_FEE_MAX_CHANGE_DENOMINATOR)*gas_limit` which is the maximum change that can happen from EIP 1559. ## Execution payload's timeline Processing an execution payload becomes a fully independent state transition function on both the consensus and execution layer. As such the message that is sent is a `SignedExecutionPaylaodEnvelope` that carries all the information needed for consensus validation in addition to the execution payload that is passed to the EL for validation. ### Gossip Execution payloads are gossiped in the global `execution_payload` pubub topic which is new in ePBS. The validations performed are as follows - The corresponding beacon block for this payload was seen and it is valid. - The builder index corresponds to the committed header in the beacon block. - The payload Hash corresponds to the committed payload header block hash. - The builder's signature is valid. :::warning There are no slashigns for payload equivocations. This is a minor issue in this version of ePBS since the full payload hash is committed in the beacon block root that is slashable for equivocations. That means that there cannot be two different valid execution payloads without a slashing. In versions of ePBS, like slot auctions, where there are no commitments, dealing with equivocations and head split views seems to be a technically more difficult problem. ::: ### Consensus state transition After passing gossip validation, the execution payload enters the consensus validation pipeline in the forkchoice handler `on_execution_payload`. This wrapper simply checks that the corresponding beacon block and the blob sidecar data is available, recovers the post-state for the corresponding beacon block and then calls `process_execution_payload`. On the consensus side there are some changes: - We need to verify the signature of the envelope (clients will not do this during regular sync since they would have already done it in gossip). - We verify that the hash tree root of the included withdrawals matches the committed withdrawals root in the beacon state. That is, we verify that the payload does fulfill the already deducted withdrawals from the CL. - Verify that the proposer index and the slot of the inclusion list correspond to the committed ones in the beacon state, that is, that this payload claims to satisfy the right inclusion list :::info We need to commit both proposer index **and** slot to the beacon state and proposer to avoid a situation in which a proposer signs an empty inclusion list for a slot, and it is later included repeatedly by builders on future slots. ::: - We reassemble the `SignedInclusionListSummary` from the payload and the envelope and check the signature :::info The reason why we need to reassemble it instead of including it in the payload is so that the EL does not need to receive useless information. We maintain the invariant that the EL receives in the payload information that it is strictly needed for validation. ::: - We verify that the payload's beacon block root commitment is the one committed in the beacon state and that the payload hash, parent block hash and kzg commitments and builder index are consistent with the previous commitments in the beacon state. - We send the payload for validation by the EL - We update the beacon state `latest_block_hash`, `latest_full_slot` for this payload - We update the beacon state `previous` inclusion list information to be the `latest` one given that this payload is required to satisfy the previous IL. - We verify the committed beacon state root :::info The fact that the execution payload performs changes to the beacon state (regarding to inclusion list, and payload hashes) requires us to check against the new state root. We opted to follow the same system as the current consensus state transition function and we commit in the envelope to the resulting hash_tree_root of the beacon state. ::: ### Execution state transition In addition to the usual execution validation of a payload, the EL needs to perform a new task in ePBS, that is the validation of the satisfaction of the included *inclusion list summary*. In order to perform this validation the EL is required to do the following: - Store and save the list of addresses that sent transactions in the current payload. - Store and save the list of addresses that decreased their balance in the current payload. - For each address in the `inclusion_list_summary` check the following: - there is a transaction in the current payload with that "from" address. - If not, there is an entry for this address in the previously stored list of addresses for the parent block with that "from" address. - If not, there is an entry for this address in the previously stored list of addresses that decreased balance in the current payload. - If not, there is an entry for this address in the previously stored list of addresses that decreased balance in the previous payload. :::warning The verification of the above guarantees that this Payload does satisfy an inclusion list broadcast by the right proposer and slot. This does not guarantee that this inclusion list is available to everyone as the proposer may have given this list particularly to this builder and no one else. The storage of balances is to deal with the special case of EIP 3074 which allows a transaction from address `0xA` to spend ETH from address `0xB`, this may invalidate some transactions in the inclusion list and thus make them impossible to be included in the current payload. ::: ## Payload attestations' timeline PTC members broadcast their attestations as `PAYLOAD_ATTESTATION_MESSAGE` objects ### Gossip On gossip the following checks are performed on the `payload_attestation_message`: - We only gossip attestations for the current slot - We only gossip attestations with a valid payload status in the vote. - We only gossip one such attestation from any PTC member - We only gossip attestations for slots in which we have seen a beacon block root. - We check that the validator is indeed in the PTC. - We check the signature ### Forkchoice handler Once the attestation passes gossip validation, we process it in forkchoice in the handler `on_payload_attestation_message` which is similar to the handler `on_attestation`. The following checks are performed, some are duplicated from gossip and this is because some of these attestations may be processed directly from blocks - We check that the beacon block is in forckhoice store - We check that the attester is in the PTC for that slot - We check that the beacon block is for that slot - If the attestation is not from a block: - We check that the attestation is for the current slot - We check the signature of the attestation - We update the ptc vote that is tracked on forkchoice for the given blockroot. ## Honest validator behavior Validators have several duties, they can be *proposers*, *PTC members*, *attesters*, *aggregators* and *sync committee members*. Duties related to the last three are not changed in ePBS. However since one of the most technically complicated changes in ePBS is with regards to forkchoice considerations and head determination, we include in this section some basic examples as preparation for the more comprehensive section [Forkchoice considerations](#forkchoice-considerations) below. ### Proposers As described above in [Anatomy of a slot](#Anatomy-of-a-slot), proposers, at the time of their required slot, in addition to the usual duties to prepare a `SignedBeaconBlock` they need to - Choose a `SignedExecutionPayloadHeader` from a builder - Request and build an `InclusionList`. Both can be done in advance to the start of the slot. Rational validators are incentivized to broadcast early their inclusion lists, to maximize their chances for their blocks to be attested for. Validator can be their own builders, but doing so will require them to still sign their own `SignedExecutionPayloadHeader`. Validators are allowed to requests bids from builders by off-protocol methods directly. Economically rational validators are incentivized to do so as this mechanism allows builders to provide the best bid they have at that precise time. :::info Self building validators have higher chances of getting the payload included on-chain over externally building ones on ePBS, a self building validator can broadcast their block jointly with their execution payload at 3 seconds into the slot, giving they payload thus 6 seconds to reach the PTC members, while externally building validators will only have 3 seconds (builder can only broadcast their payload **after** they have seen and validated the consensus block) ::: :::warning Rational validators have reasons to delay requesting or choosing the builder's bid until the very last moment they can broadcast their block, to maximize MEV extraction. These timing games are attenuated by the fact that - `(block,slot)` voting is in place and honest validators **are required** to reorg late blocks. - The attestation deadline is reduced to 3" from the current 4" in the ePBS fork. ::: #### Determining head Before processing a block proposers need to determine their head of the chain. In this section we include some basic examples of common situations that proposers will be facing. In all cases, we are at the start of slot `N` and are about to propose a block, the arrow from `N` shows what head should be chosen by an honest proposer. Lightblue nodes mean they are full, white nodes are skipped slots. Orange nodes are nodes that are *empty*. ##### Happy case The are no forks, all blocks are present and full ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty]; B [label="N-1"]; D [label="..."]; C -> B; B -> D; } ``` ##### Skipped slot Last block was skipped ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1", style="empty"]; C [label="N-2"]; D [label="..."]; // Define edges B -> C; A -> C; A -> B [style=invis] C -> D; } ``` ##### Missing payload The last slot's builder did not reveal the payload ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1", fillcolor=orange]; # L [label="N-1"]; C [label="N-2"]; D [label="..."]; // Define edges A -> B; B -> C; # L -> C C -> D; } ``` ##### Payload was late The last slot's builder revealed the payload and the proposer saw it late, the PTC for `N-1` voted `PAYLOAD_ABSENT` ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1", fillcolor=orange]; L [label="N-1"]; C [label="N-2"]; D [label="..."]; // Define edges A -> B; B -> C; L -> C C -> D; } ``` ##### Honestly withheld payload The last slot's proposer revealed his block, but the proposer saw it late. In addition the builder, who also saw the block late revealed a timely *payload withheld* message. the PTC for `N-1` voted `PAYLOAD_WITHHELD` ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1", fillcolor=orange]; L [label="N-1", style="empty"]; C [label="N-2"]; D [label="..."]; // Define edges A -> C; A -> B [style=invis] A -> L [style=invis] B -> C; L -> C C -> D; } ``` ##### Failed payload withholding The last slot's proposer revealed his block, the builder, who saw the block late revealed a *payload withheld* message. the PTC for `N-1` however did not see the message timely and thus voted `PAYLOAD_ABSENT` ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1", fillcolor=orange]; L [label="N-1", style="empty"]; C [label="N-2"]; D [label="..."]; // Define edges A -> B; A -> L [style=invis] A -> C [style=invis] B -> C; L -> C C -> D; } ``` ### Attesters There are no changes to the attestation duty, but since determining head changes in ePBS, we include here some common examples. The current slot is slot `N`, color coding is as before, the rounded red node is the blockroot that the attester would vote for. Notice that slot `N` is not full in these examples as we assume the builder will be revealing after the attestation deadline. Notice these examples have some times multiple nodes at the same slot height, for example the slot `N-1` can be either full or empty (late payload). We rounded in red the head view of the attester. The attestation of honest validators only points to the beacon block root. But the forkchoice weight is only applied to the branch that agrees with the PTC vote. #### Happy case All nodes are present and full, the block arrives early ```graphviz digraph G{ rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty, color=red, penwidth=3]; B [label="N-1"]; D [label="..."]; C -> B; B -> D; } ``` #### No inclusion list All nodes are present and full, the block arrives early but the inclusion list did not arrive ```graphviz digraph G{ rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty]; B [label="N-1", color=red, penwidth=3]; D [label="..."]; C -> B; B -> D; } ``` #### Late block All nodes are present and full, the block arrived late ```graphviz digraph G{ rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty]; B [label="N-1", color=red, penwidth=3]; D [label="..."]; C -> B; B -> D; } ``` #### Attempted payload reorg The PTC for `N-1` voted `PAYLOAD_PRESENT`. The current block arrives early but reorgs the payload ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1", fillcolor=orange]; L [label="N-1", color=red, penwidth=3]; C [label="N-2"]; D [label="..."]; // Define edges A -> B; A -> L [style=invis] A -> C [style=invis] B -> C; L -> C C -> D; } ``` #### Attempted block reorg The builder of the previous block revealed the payload late, the PTC for `N-1` voted `PAYLOAD_ABSENT`. The current block arrives early but reorgs the full block ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1"]; L [label="N-1", fillcolor=orange, color=red, penwidth=3]; C [label="N-2"]; D [label="..."]; // Define edges A -> B [style=invis] A -> L [style=invis] A -> C [style=invis] A -> C; B -> C; L -> C C -> D; } ``` #### Late payload The PTC for `N-1` voted `PAYLOAD_ABSENT`. The current block arrives early and is based on the previous full block ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1"]; L [label="N-1", fillcolor=orange, color=red, penwidth=3]; C [label="N-2"]; D [label="..."]; // Define edges A -> B; A -> L [style=invis] A -> C [style=invis] B -> C; L -> C C -> D; } ``` #### Late payload and block The attestation committee for `N-1` did not see the block and voted for `N-2`. The PTC for `N-1` voted `PAYLOAD_ABSENT`. The current block arrives early and is based on the previous full block (or empty, that is why the two arrows from `N`) ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; // Define nodes A [label="N", style="empty"]; B [label="N-1"]; L [label="N-1", fillcolor=orange]; C [label="N-2", color=red, penwidth=3]; D [label="..."]; // Define edges A -> B; A -> L; A -> C [style=invis]; B -> C; L -> C C -> D; } ``` ### PTC members 512 validators are chosen per slot to attest for the timeliness of the corresponding payload. A *payload attestation* consist semantically of one of the following messages - I have seen both a valid consensus block **for the current slot** according to my current committee shuffling and the corresponding execution payload. - I have seen a valid consensus block for the current slot, and I saw a valid "payload withheld" message from the corresponding builder - I have seen a valid consensus block for the current slot but I have not seen the corresponding execution payload. - I have not seen any consensus block for the current slot according to my shuffling. In case of short forks, PTC members would not see two different consensus blocks except in the case of a proposal equivocation. In this case, PTC members will only import the first consensus block and will act with respect to this block. In case of long running forks that affect the shuffling, there may actually exist different valid blocks for the same slot. However, in this scenario it is practically impossible that the same validator is a PTC member in both branches of the fork thus in this scenario there is still only one valid consensus block from the point of view of the current PTC member's shuffling. ### Constructing the payload attestation PTC members do not attest to the timeliness of the head's payload. They attest to the timeliness of **the current slot's payload**. The following diagrams may help explain the honest PTC member behavior. The current slot is `N` and the PTC member is about to cast their attestation 9 seconds into the slot. #### Happy case All blocks are full and payloads are timely ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; B [label="N"]; D [label="..."]; B -> D; } ``` In this case the PTC member would vote `PAYLOAD_PRESENT` #### Payload is late All blocks are timely, but the current payload was not received on time ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", fillcolor="orange"]; B [label="N-1"]; D [label="..."]; C -> B; B -> D; } ``` In this case the PTC member would vote `PAYLOAD_ABSENT`. :::warning If the beacon block is timely, and the builder still decides to withhold. The PTC members will vote with `PAYLOAD_WITHHELD`. This makes it easier to do post-anti reorgs (albeit losing the payload) but at the same time it gives stronger builder's withholding safety. ::: #### Successful witholding (late block) ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty]; B [label="N-1"]; D [label="..."]; C -> B; B -> D; } ``` The current consensus block was received late and the builder's payload was timely and had a `payload_withheld=True` flag. In this case the PTC member will vote `PAYLOAD_WITHHELD`. #### Successful witholding (not head) This is a strict generalization of the previous case ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty]; B [label="N-1"]; D [label="..."]; E [label="N-2"]; C -> E; C -> B [style=invis] B -> E; E -> D } ``` The current consensus block was received timely (or late) but it is not the head of the chain. In this example it is trying to reorg the previous block. and the builder's payload was timely and had a `payload_withheld=True` flag. In this case the PTC member will vote `PAYLOAD_WITHHELD`. The previous example is a particular case of this case as if `N` is simply late, the head of the chain will continue being `N-1` as seen above. Notice that similar diagrams can be made if the incoming block `N` merely tries to reorg the previous payload for example: ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; C [label="N", style=empty]; B [label="N-1"]; L [label="N-1", fillcolor=orange]; D [label="..."]; E [label="N-2"]; C -> L; C -> B [style=invis] B -> E; L -> E E -> D } ``` As long as the PTC member sees a timely `payload_withheld=True` envelope, they would vote `PAYLOAD_WITHHELD`. #### No consensus block seen If the PTC member has not seen any consensus block for the current slot, then it would not submit any payload attestation. Any such payload attestation would be ignored anyway. Even if the PTC member has seen some envelopes timely and claiming to belong to the current slot, the PTC member will not attest. :::info Alternatively we could make the PTC members to vote for `PAYLOAD_ABSENT` as there is no possible execution payload that can be validated against a non-existent execution payload header. Or we could even create a specific vote for this situation to allow such members to be rewarded. However the penalties to PTC members in the event of a missing block are minimal (just a single attestation missed) and it only affects 512 validators per missed block. ::: ## Honest builder behavior Builders can and should prepare different payloads for different possible parent heads. They can submit multiple bids ahead of the intended slot. However, validators will only gossip the first valid seen message for the combination (builder, slot). Builders are allowed to open off-protocol services to provide bids upon requests. This allows builders to update their own best bid without broadcasting them to the network and risking inclusion of a less-than-optimal payload. ### Direct builder's bid requests An optional non-consensus part of the specification is to specify consensus client support for direct requests of bids from the builders. This would be a minor modification to the current builder API spec and can reuse existing code in clients. Validators would request an execution header by sending a `SignedBidRequest` object ```python! class BidRequest(container): slot: Slot proposer_index: Validator_index parent_hash: Hash32 parent_block_root: Root ``` ```python class SignedBidRequest(container): message: BidRequest signature: BLSSignature ``` :::info We could cryptographically bind the message for the builder So as to prevent builders from knowing what other builders bids have been at the time, dissalowing for cartelization of the builder landscape. Notice that even if builders do submit bids to a trusted relay, builders have no guarantee that another, better bid, may have been sent in private directly to the proposer by the above mechanism. ::: #### Bid gossip as fallback Validators can always self build, and can request directly bids by off-protocol mechanisms that are all but certain to guarantee better bids than those propagated in the P2P network. So one may argue why to keep a global topic. One the one hand this gives a fallback in case of failures of centralized builders to allow validators running on low end hardware to have access to larger community ran builders. On the other hand it sets a low bar for censorship and cartelization as some community members may want to run vanilla software and produce public bids for every slot, forcing centralized builders to outbid them to censor transactions that would otherwise be included in those blocks. The global topic can be hardened easily against spam since we may only gossip the highest value bid we have received for a given parent block hash and also restrict to one message per builder. ## Engine API ePBS requires some changes to the engine API: - The `ExecutionPayloadV4` is needed to add a new field `inclusionListSummary`. This is the only change pertaining EL validation. - `PayloadAttrivutesV3` is needed to include the inclusion list parent block hash and the proposer index. EL clients should keep a map of valid full inclusion list transactions keyed by the pair of a parent block hash and proposer index. The key is sent by the CL client in the payload attributes during the call to `engine_forkchoiceUpdated` to trigger block production. These fields allow the EL to choose the right inclusion list transactions that it's forced to include. - `engine_newInclusionListV1` is a new method that is used to notify the EL of a new full inclusion list. This method is called by CLs when receiving a full inclusion list to pass it to the EL for validation. The method takes as parameter a new structure `InclusionListV1` that consists of the set of transactions, the summary and the pair mentioned above. ## Forkchoice considerations Perhaps one of the technically most complicated changes in the ePBS fork is with regards to forkchoice and head determination. In this section we consider the main high level changes. ### (Block, slot) voting At the heart of ePBS there is the notion of builder's safety. Recall the definition from [the design constraints](https://ethresear.ch/t/epbs-design-constraints/18728#h-31-builder-safety-9) > (Builder reveal Safety) An honest builder that reveals his payload timely during his turn will have his block included on chain. And its [withholding version](https://ethresear.ch/t/epbs-design-constraints/18728#h-33-builder-safety-revisited-12) > (Builder withholding safety) If the CL block for the current slot has not been seen or has been seen late by the majority of the network and the builder decides not to reveal his payload, then he cannot be forced to pay. We need thus to implement a way to allow the builder to withhold their payload honestly, in the case the block arrives late (or as we will get for free in our design: when the block that arrives is not the head of the chain). For this we implement *(block,slot)* voting. At a high level, the idea is that if a validator in the committee for slot `N` did not see the block for slot `N` arrive, and voted for it's parent `N-1`, then its weight supports `N-1` and any chain that descends from it **that does not contain `N`**. Here are some examples: #### Happy case In this case all blocks are timely, there is one attester in each committee and each attester has weight 10. Here `weight` denotes the total supporting weight for that node, while "vote" denotes the total direct weight that voted for that node. ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<weight:30<BR/>vote:10>]; B [label=<weight:20<br/>vote:10>]; C [label=<weight:10<br/>vote:10>]; C -> B; B -> A; } ``` #### Late block The last block in the chain arrived late, thus the attesters for that slot voted for its parent. ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<weight:30<BR/>vote:10>]; B [label=<weight:20<br/>vote:20>]; C [label=<weight:0<br/>vote:0>]; C -> B; B -> A; } ``` #### Consecutive late blocks The effect is compounding, lets analyze the simple case of consecutive late blocks. When the first block appears it is attested for ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<weight:10<BR/>vote:10>]; } ``` During the next slot, the child appears late, thus validators would vote for the parent block and we would have the following situation: ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<weight:20<BR/>vote:20>]; B [label=<weight:0<br/>vote:0>]; B -> A; } ``` Before casting their vote, these validators had no choice, they hadn't seen the last block arrived. During the next slot the committee has now seen the child, and they are presented with a choice for head: ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-1<br/>weight:20<BR/>vote:20>]; B [label=<Slot N<br/>weight:0<br/>vote:0>]; C [label=<Slot N-1<br/>weight:20<BR/>vote:20>, style=dashed]; C -> A [style=dashed]; B -> A } ``` So if they will cast their vote for the blockroot `N-1` instead of the blockroot of `N`. If during slot `N+1` a child of `N` arrives and is early, validators will be presented with this situation ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-1<br/>weight:20+PB<BR/>vote:20>]; B [label=<Slot N<br/>weight:PB<br/>vote:0>]; C [label=<Slot N-1<br/>weight:20<BR/>vote:20>, style=dashed]; D [label=<Slot N+1<br/>weight:PB<br/>vote:0>]; E [label=<Slot N-1<br/>weight:20<BR/>vote:20>, style=dashed]; C -> A [style=dashed]; E -> C [style=dashed]; B -> A D -> B } ``` Where `PB` is the proposer boost assigned to early blocks. The weight for the chain supporting the blockroot `N+1` is `PB` and that supporting `N-1` as head root is `20`, thus if `PB < 20` then honest validators will continue voting on `N-1` as head. ### Payload status In addition to (Block, slot) voting, we have to consider the status of the payload when counting votes for LMD weight. Nodes are not only labeled by the slot/blockroot, but also can be either *missing*, *empty* or *full*. We follow the simple rule :::info A vote for the blockroot at `N` only supports chains that contain `N` and the payload status that is consistent with the PTC vote of `N`. By *consistent* we mean here that if the PTC has achieved quorum on `PAYLOAD_AVAILABLE` then a vote for `N` only counts chains that contain the payload of `N`. If the PTC has achieved a quorum of `PAYLOAD_ABSENT` then a vote for `N` will only support chains that do not contain any payload on `N`. There is a special situation in the case of the PTC achieving quorum on `PAYLOAD_WITHHELD`, in which case we treat the support only for chains that do not contain a payload for `N` as if the quorum was for `PAYLOAD_ABSENT`. ::: This rule is a little bit more difficult to explain so lets delve in some examples #### Happy case We use the same coloring as in the previous sections, with orange indicating empty blocks. All blocks are full, the PTC votes for `PAYLOAD_PRESENT` every block ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:30<BR/>vote:10>]; B [label=<Slot N-1<br/>weight:20<br/>vote:10>]; C [label=<Slot N<br/>weight:10<br/>vote:10>]; C -> B; B -> A; } ``` #### Attempted payload reorg The PTC for `N-1` voted `PAYLOAD_PRESENT` but the block for slot `N` arrives early and builds on the empty block at `N-1`. At the attestation deadline for `N` the committee sees this ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:20+PB<BR/>vote:10>]; B [label=<Slot N-1<br/>weight:10<br/>vote:10>]; C [label=<Slot N<br/>weight:PB<br/>vote:0>]; D [label=<Slot N-1<br/>weight:PB<br/>vote:0>, fillcolor=orange]; E [label=<Slot N-1<br/>weight:10<br/>vote:10>, style=dashed]; C -> D; B -> A; D -> A; E -> B [style=dashed]; } ``` Hence if `PB < 10` validators during `N` will vote for `N-1` as head, at the start of `N+1` the situation would be ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:30<BR/>vote:10>]; B [label=<Slot N-1<br/>weight:20<br/>vote:20>]; C [label=<Slot N<br/>weight:0<br/>vote:0>]; D [label=<Slot N-1<br/>weight:0<br/>vote:0>, fillcolor=orange]; C -> D; B -> A; D -> A; } ``` The exact same scenario would happen with `N` built on top of `N-2` as if `N-1` were missing instead of *empty*. #### Payload withheld In this situation the block for `N-1` was seen early by some validators (with a fraction `x` of the total committee stake) and late by the rest. The builder sent a **payload withheld message** timely. The PTC achieved quorum for `PAYLOAD_WITHHELD` at the end of slot `N-1` the situation is as follows (in this chart we use units of total committee for voting) ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:2<BR/>vote:1>]; B [label=<Slot N-2<br/>weight:1-x<br/>vote:1-x>, style=dashed]; D [label=<Slot N-1<br/>weight:x<br/>vote:x>, fillcolor=orange]; B -> A [style=dash]; D -> A; } ``` If the PTC had achieved quorum in that `PAYLOAD_ABSENT` then the situation at this stage would be exactly the same. :::warning There is an edge case in which the PTC can achieve quorum on `PAYLOAD_PRESENT` when in fact the builder sent a payload withheld message. This can only happen either by an equivocation of the proposer (because of the different payload hashes) or an equivocation of the builder. An equivocation of the builder can only happen if the builder sends different envelopes with the same payload. This can only happen if it includes a different `payload_withheld` field. This equivocation does not cause any serious problem. ::: ### Inclusion list availability Another component that complicates forkchoice is the availability of the inclusion list. We have seen above that when importing a block whose parent is empty, we mark the inclusion list for that block as available. However it its parent is full, we need to wait and validate an inclusion list for it. We follow the following simple rule when computing the head of the chain: - Compute the head by the usual rules of LMD max weight - The canonical head is the last block in that chain that has a fully validated IL. Here are some examples: #### Collusion with the next builder No honest validator has seen an inclusion list during `N-1` for its consensus block. At the attestation deadline of `N-1` the situation is this ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:10+PB<BR/>vote:10>]; B [label=<Slot N-1<br/>weight:PB<br/>vote:0>]; B -> A; } ``` Honest validators will compute their canonical head by the usual rule and find `N-1`, however since they haven't seen a valid IL they will vote for `N-2` as head. At the start of the next slot `N` the situation is thus ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:20<BR/>vote:20>]; B [label=<Slot N-1<br/>weight:0<br/>vote:0>]; C [label=<Slot N-2<br/>weight:20<BR/>vote:20>, style=dashed]; B -> A; C -> A [style=dashed]; } ``` The block for `N` arrives early and it has a fully valid IL and moreover it builds on `N-1` and is a valid payload. This in particular **implies that a valid IL was available for N-1** at least to the builder. At the attestation deadline of `N` the situation is like this ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:20+PB<BR/>vote:20>]; B [label=<Slot N-1<br/>weight:PB<br/>vote:0>]; C [label=<Slot N-2<br/>weight:20<BR/>vote:20>, style=dashed]; D [label=<Slot N<br/>weight:PB<br/>vote:0>]; E [label=<Slot N-2<br/>weight:20<BR/>vote:20>, style=dashed]; B -> A; D -> B C -> A [style=dashed]; E -> C } ``` Hence if `PB>20` honest validators will in fact vote for `N` and reorg the chain. Suppose we chose `PB = 25` in this case, honest validators will vote for `N` and the situation at the start of `N+1` will be as follows ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:30<BR/>vote:20>]; B [label=<Slot N-1<br/>weight:10<br/>vote:0>]; C [label=<Slot N-2<br/>weight:20<BR/>vote:20>, style=dashed]; D [label=<Slot N<br/>weight:10<br/>vote:10>]; E [label=<Slot N-2<br/>weight:20<BR/>vote:20>, style=dashed]; B -> A; D -> B C -> A [style=dashed]; E -> C } ``` So the next proposer will immediately reorg back and still propose on top of `N-2`. Notice that `N-1` was never an option for head. #### Late block after collusion In a situation similar to the above suppose slot `N` did not come timely. In this situation `N-1` will not become the head even though we know someone (eg the builder of `N`) had a full IL. The canonical head will continue being `N-2`. #### Isolated node It may happen that a node which will be the proposer for `N` simply does not see the IL for `N-1` but it does see the block timely and the rest of the chain has seen the IL so that the incoming block is heavily voted. In this case the proposer will compute head and receive `N-2` as head instead of `N-1`. As such it will try to reorg the block and validators will not (under normal circumstances) vote for `N` and vote instead for `N-1`. :::info In this scenario, for isolated proposers/attesters, the situation is exactly the same as if the proposers would have considered IL availability as a validity condition for the block. Validators can still requests ILs by RPC from peers, specially if they see the blocks voted. ::: ### Payload boost Payload boosts will be analyzed in the next section [Security analysis](#Security-analysis), but we include here the some examples. If the payload is revealed timely and the PTC achieves quorum on a vote of `PAYLOAD_PRESENT` then the forkchoice node for the *full* block receives a *builder's reveal boost* (RB) that is valid since the slot of the reveal until the next slot's attestation deadline. Conversely, if the builder reveals an *payload honestly withheld* message and the PTC achieves quorum on `PAYLOAD_WITHHELD`, the forkchoice node for the *missing* block gets a *builder' withholding boost* (WB), this means that this boost is applied to the parent node and only supports chains that do not contain the current block. :::info The withholding boost is applied to the parent node instead of removing it from the node and their descendants to avoid overflows. The analysis with subtraction is similar. ::: Here are some basic examples #### Happy case All blocks are timely and full. The PTC achieved quorum on `PAYLOAD_AVAILABLE`. At the attestation deadline of slot `N` we have ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:20+PB+RB<BR/>vote:10>]; B [label=<Slot N-1<br/>weight:10+PB+RB<br/>vote:10>]; D [label=<Slot N<br/>weight:PB+RB<br/>vote:0>, style=empty]; B -> A; D -> B } ``` #### Attempted payload reorgs The PTC has achieved quorum on `PAYLOAD_AVAILABLE` for `N-1` but the proposer of `N` builds on top of the empty block. At the attestation deadline of `N` the situation is ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:20+PB+RB<BR/>vote:10>]; B [label=<Slot N-1<br/>weight:10+RB<br/>vote:10>]; C [label=<Slot N-1<br/>weight:PB<br/>vote:0>, fillcolor=orange]; D [label=<Slot N<br/>weight:PB<br/>vote:0>, style=empty]; B -> A; C -> A D -> C } ``` #### Attempted builder's grief The PTC of `N-1` has achieved quorum on `PAYLOAD_WITHHELD`, the builder has revealed timely a *payload withheld message*, but the proposer of `N` builds on top of `N-1` none-the-less. At the attestation deadline of `N` we have ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:20+PB+WB<BR/>vote:20>]; B [label=<Slot N-2<br/>weight:10+WB<br/>vote:10>, style=dashed]; C [label=<Slot N-1<br/>weight:PB<br/>vote:0>, fillcolor=orange]; D [label=<Slot N<br/>weight:PB<br/>vote:0>, style=empty]; B -> A [style=dashed] C -> A D -> C B -> C [style=invis] } ``` The timeline for this is as follows. At `N-2` honest validators voted for it. During `N-1` all honest validators also voted for it. However those votes only support chains without a payload. Thus, `10` direct votes for `N-2` would support the chain containing `N` but the `10` votes from the committee at `N-1` would not, those are highlighted in the dashed node. ## Security analysis In this section we analyze to what extent the proposed design satisfies all the constrains listed in [epbs design constraints](https://ethresear.ch/t/epbs-design-constraints). The relevant conditions for these sections are - Builder reveal safety - Builder withholding safety - Proposer safety In this sections we use the same values deduced in [payload boosts in ePBS](https://ethresear.ch/t/payload-boosts-in-epbs/18769), that is $$ WB = RB = 40\%, \qquad PB = 20\%.$$ In all the below examples, the attacker is assumed to control a fraction $\beta$ of the committee stake. We assume that the PTC is honest (that is $\beta < 50\%$). ### Builder reveal safety > (Builder reveal Safety) An honest builder that reveals his payload timely during his turn will have his block included on chain. #### Reorging the payload We prove in this section that a collusion of two consecutive proposers cannot reorg the payload of a builder that has revealed. In this attack, the proposer of `N-1` and that of `N` are colluding and want to reorg the payload of the builder of `N-1`. The proposer of `N-1` is further assumed to be able to control the split of the attestation vote however he likes. Lets assume they aim for a fraction $x$ of the committee seeing the block early, while a fraction $1-x-\beta$ will see the block late (they may also withhold attestations $\beta$ worth of attestations) At the time of builder reveal the forkchoice status in the view of the builder is (we are not counting proposer boost because all attestation already happened at this stage). In the worst case if the proposer wants to trick the builder into revealing and try to reorg it, therefore they will release their attestations for `N-1` but will withhold them from $N-2$. Notice that the full committee had voted during $N-2$, those votes are irrelevant and we are not counting them here in this analysis. ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1-β<br/>vote:0>] C [label=<Slot N-1<br/>weight:x<br/>vote:x>, style=empty]; B [label=<Slot N-2<br/>weight:1-x-β<br/>vote:1-x-β>, style=dashed] C -> A B -> A [style=dashed] } ``` The builder will reveal only if $1-x-β < x$, equivalently if $$1 - 2x < \beta. \qquad \qquad \qquad \qquad (1)$$ The builder reveals his payload and the PTC, being honest, achieves quorum on `PAYLOAD_PRESENT`. The next proposer of `N` attempts to reorg the payload with an early block. The status at the attestation deadline is ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1-β+RB+PB<br/>vote:0>] D [label=<Slot N-1<br/>weight:PB<br/>vote:0>, fillcolor=orange] C [label=<Slot N-1<br/>weight:x+RB<br/>vote:x>]; B [label=<Slot N-2<br/>weight:1-x-β<br/>vote:1-x-β>, style=dashed] E [label=<Slot N<br/>weight:PB<br/>vote:0>]; C -> A B -> A [style=dashed] D -> A E -> D } ``` At this point there is no attestation from N-1 that can support the chain with `N`, therefore the only thing that the attacker can do is revealing attestations for `N-2`. Changing the lower node's weight from $1-x-\beta$ to $1-x$. The chain with `N` can never win with from the builders chain since $$ RB > PB \Rightarrow x + RB > PB.$$ The chain from `N-2`, reorging the entire block would win only if $$ RB < 1 - 2x < \beta, $$ where we have used (1). Thus, for a value of $RB = 40\%$, builders are protected from this attack from stakers controlling up to $40\%$ of the stake. #### Reorging the block We saw in the previous section that the attacker has no chances of reorging only the payload, they may as well try to reorg the entire block, for this, instead of revealing the payload on top of the empty block, they would reveal `N` on top of the missed block. At the attestation deadline of `N` the situation would be ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1+RB+PB<br/>vote:0>] C [label=<Slot N-1<br/>weight:x+RB<br/>vote:x>]; B [label=<Slot N-2<br/>weight:1-x + PB<br/>vote:1-x>, style=dashed] E [label=<Slot N<br/>weight:PB<br/>vote:0>]; C -> A B -> A [style=dashed] E -> B } ``` Now honest validators will vote on the attackers branch if $$ PB + 1 - x > x + RB,$$ or equivalently if $$ RB - PB < 1 - 2x < \beta$$ Which can never happen as long as $\beta < 20\%$. We see that the builder cannot be reorged by colluding actors that control the split as long as they also do not control more than $20\%$ of the stake. :::info Notice that if the proposers do not control the network splitting so exactly, these numbers go up noticeably. ::: ### Builder withhold safety From the [design constraints](https://ethresear.ch/t/epbs-design-constraints): > (Builder withholding safety) If the CL block for the current slot has not been seen or has been seen late by the majority of the network and the builder decides not to reveal his payload, then he cannot be forced to pay. Given that we have (block, slot) voting we actually get a stronger safety under the same parameters for the attacker's stake :::info (Builder withholding safety) If the `SignedBeaconBlock` block for the current slot is not the head of the chain and the builder decides not to reveal his payload, then he cannot be forced to pay. ::: #### Attempted builder's grief In this attach both the proposer for `N-1` and `N` are colluding to grief the builder of `N-1`. Their intention is to make the builder withhold the payload but at the same time make him pay the bid. For this, the proposer for `N` targets a split as before, where a fraction $x-\beta$ sees the block early, and a fraction $1-x$ sees it late (the attacker will withold their votes for $N-1$ to maximize the chances the builder withholds) Just as before, the situation before the builder for `N-1` needs to reveal the payload, they see the following forkchoice status: ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1-β<br/>vote:0>] C [label=<Slot N-1<br/>weight:x-β<br/>vote:x-β>, style=empty]; B [label=<Slot N-2<br/>weight:1-x<br/>vote:1-x>, style=dashed] C -> A B -> A [style=dashed] } ``` They will withold their payload if $$ x - \beta < 1- x \Leftrightarrow 2x - 1 < \beta. $$ The PTC is honest and achieves quorum on `PAYLOAD_WITHHELD` but the proposer of `N` bases his block on top of the missing payload. The attacker will reveal their attestations for the empty `N-1` block as well. At the attestation deadline of `N` the situation is as follows ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1+WB+PB<br/>vote:0>] C [label=<Slot N-1<br/>weight:x+PB<br/>vote:x>,fillcolor=orange]; B [label=<Slot N-2<br/>weight:1-x + WB<br/>vote:1-x>, style=dashed] E [label=<Slot N<br/>weight:PB<br/>vote:0>]; C -> A B -> A [style=dashed] E -> C } ``` Thus the attack is succesful if $$ PB + x > 1 - x + WB \Leftrightarrow WB - PB < 2 x - 1$$ Which using the above implies $$ WB - PB < \beta. $$ which is impossible with the chosen parameters. We see that the builder is protected against this attack of colluding proposers with network control and up to a stake of $20\%$. #### Bad parent block Another attempt to try to grief the builder would be by splitting the view with `N-2` itself, in this attack the proposer of `N-1` builds a weak block by proposing on top of a non-canonical block (not `N-2` as in the previous example). However, this requires the builder itself to be dishonest since the builder specifies both the parent block hash **and** the parent block root in the `ExecutionPayloadHeader`. ### Proposer safety From the [design constraints](https://ethresear.ch/t/epbs-design-constraints/18728#h-33-proposer-safety-11): > (Proposer safety) If the proposer acts honestly and reveals his block timely, it will be included on chain. #### Ex anti reorgs In this attack the proposer of `N-1` and the builder of `N-1` are colluding to reveal late their blocks and try to reorg the block from the proposer of `N` The timeline of this attack is as follows. The proposer of `N-1` reveals his block targetting a split as above. A fraction $x - \beta$ of the validators will see the block `N-1` timely (the attacker will withold their attestations for `N-1`). Their intent is to make the proposer of `N` to build their block on top of `N-2` to later reorg it back. At the start of `N` the view of the proposer is ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1-β<br/>vote:0>] C [label=<Slot N-1<br/>weight:x-β<br/>vote:x-β>, style=empty]; B [label=<Slot N-2<br/>weight:1-x<br/>vote:1-x>, style=dashed] C -> A B -> A [style=dashed] } ``` The PTC would achieve quorum on `PAYLOAD_ABSENT` as long as $x < 1/2$ and `PAYLOAD_PRESENT` otherwise. In the case $x < 1/2$ and the PTC voting for `PAYLOAD_ABSENT`, the proposer of `N` would base his block on top of `N-2` as long as $$ 1 - x > x - \beta \Leftrightarrow \beta > 2x - 1. $$ Notice that if $x < 1/2$ this is always satisfied, so as long as the PTC votes for `PAYLOAD_ABSENT` then the proposer of `N` will propose on top of `N-2`. On the other hand if $x > 1/2$, and the PTC has achieved quorum in `PAYLOAD_PRESENT`, then the proposer of `N-1` will see a different forkchoice diagram at the beginning of `N`: ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1-β+RB<br/>vote:0>] C [label=<Slot N-1<br/>weight:x-β+RB<br/>vote:x-β>]; B [label=<Slot N-2<br/>weight:1-x<br/>vote:1-x>, style=dashed] C -> A B -> A [style=dashed] } ``` In this case they will propose on top of `N-2` only if $$ 1-x > x - \beta + RB \Leftrightarrow RB + 2x - 1 < \beta.$$ ##### First case, missing payload. We are in the situation of $x < 1/2$ and the PTC agreeing on `PAYLOAD_ABSENT`. In this case at the attestation deadline for `N` the forkchoice status is ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1+PB<br/>vote:0>] C [label=<Slot N-1<br/>weight:x<br/>vote:x>, style=empty]; B [label=<Slot N-2<br/>weight:1-x<br/>vote:1-x>, style=dashed] D [label=<SlotN<br/>weight:PB<br/>vote:0>,style=empty] C -> A B -> A [style=dashed] D -> B; } ``` And the reorg is succesful if $$ x > PB + 1 - x $$ which can never happen. ##### Second case, payload present We are in the situation that $x > 1/2$ and $RB + 2x - 1 < \beta$. In this case the PTC achieved quorum for `PAYLOAD_PRESENT` but the proposer would still base his block on top of `N-2`. This is the range that the attacker wants to trigger since they want to get the `N-1` payload included. After the proposer of `N` reveals their block, the attacker reveals their attestations for `N-1` and at the attestation deadline of `N` the forkchoice situation is: ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1+RB+PB<br/>vote:0>] C [label=<Slot N-1<br/>weight:x+RB<br/>vote:x>]; B [label=<Slot N-2<br/>weight:1 - x<br/>vote:1-x>, style=dashed] E [label=<Slot N<br/>weight:PB<br/>vote:0>]; C -> A B -> A [style=dashed] E -> B } ``` The reorg would be effective if $$ x + RB > PB + 1 - x \Leftrightarrow PB < RB + 2x - 1$$ By the above this implies $PB < \beta$ and this is absurd given our constraints. Thus we see that the proposer is safe under ex-anti reorgs of colluding attackers with up to $20\%$ of the stake. #### Post-anti reorgs In this attack the builder of `N-1` and the proposer of `N` are colluding to try to reorg the consensus block of the proposer of `N-1`. The builder of `N-1` does not want the PTC to achieve quorum on `PAYLOAD_PRESENT` since this gives weight to `N-1`. The builder withholds and the PTC will achieve consensus on `PAYLOAD_WITHHELD` then. At the attestation deadline of `N` the forkchoice status is ```graphviz digraph G { rankdir=RL; node [style=filled, fillcolor=lightblue]; A [label=<Slot N-2<br/>weight:1+PB+WB<br/>vote:0>] B [label=<Slot N-2<br/>weight:PB + WB + β<br/>vote:0>, style=dashed] C [label=<Slot N-1<br/>weight:1-β<br/>vote:1-β>, fillcolor=orange]; E [label=<Slot N<br/>weight:PB<br/>vote:0>]; C -> A B -> A [style=dashed] E -> B } ``` The reorg is effective only if $PB + WB + \beta > 1 - \beta$ which is not the case with our choice of parameters for up to stakers with $20 \%$ of the stake. ### Unbundling One of the attacks that MEV-Boost is subject to is that of *unbundling*. Essentially this corresponds to tricking the builder into revealing a payload but the CL block committing to a different payload on the same block. We have seen above in the analysis for *builder reveal safety* that this cannot happen at least in some situations of colluding proposers. We include however here the analysis under equivocation of the proposer, which is more in line to the original attack of the [low carb crusader](https://collective.flashbots.net/t/post-mortem-april-3rd-2023-mev-boost-relay-incident-and-related-timing-issue/1540). The idea is that the proposer would equivocate and submit two different blocks `N` and `N'` and make the builder reveal the payload for `N` and somehow reuse transactions and revealing a different payload for `N'` that exploits the builder's original payload. The timeline for this is impossible as the proposer for `N'` cannot even construct the block until it has not already seen the payload for `N`. This means that `N'` cannot ever have quorum for the PTC. If `N` got enough votes, the builder's reveal safety would guarantee that the original payload is canonical. If it didn't get enough votes, then the builder withholding safety would guarantee that the builder may not reveal at all (thus making the attempt useless) and the builder would not need to pay the bid for it. ## No trusted advantage The proposed specification solves the problem of enabling both proposers and builders to fairly exchange the rights to propose an execution payload without any trust assumptions among them. However, for the system to be effective, we require that there must not be any trusted advantage. From the [design constraints](https://ethresear.ch/t/epbs-design-constraints/18728#h-35-no-trusted-advantage-14): > There should be no inherent advantage for proposers, to sell their blocks to builders off-protocol. The specification allows for builders to behave like current vertically integrated relays do. They can do bid cancellations, update their payload up to the very last moment, etc, without any extra latency of the communication with a separate relay. They do not need to worry about unbundling. Thus for builders, this mechanism enables them to enter the market trustlessly and without extra latencies or possible governance/financial aspects of dealing with an external organization running the relay. For proposers, this increases the spectrum of bids they can get. We could obfuscate the bids by making the bid request cryptographically binding to the builder, this way builders would not be able to even see what bids are already in place. This takes away any advantage a centralized relay may have as long as the entire set of builders is not a cartel, namely as long as the set of builders are indeed competing. ## Unconditional payment We have seen that when processing the consensus block (specifically when processing the signed execution payload header within the block body), the payment of the bid is guaranteed on the beacon chain. This guarantees that the proposer is paid as long as his consensus block is not reorged. There are two limitations in the current proposal that are due to simplifications: #### Builders require locked capital Builders need to have their capital staked in advance prior to submitting a bid. This imposes even further inefficiency in case they want to submit large bids over the maximum effective balance of 32 ETH. Since the corresponding validator would not even be making staking profits from the excess balance. One mitigation for this is EIP 7251 which increases the max effective balance. A byproduct of this change is the following limitation of the current proposal: #### Builders can make arbitrarily large transfers Validators (and hence builders) can have arbitrarily large balances and the builders' bids are not bound by max effective balance. In the current mechanism a validator could make arbitrarily large transfers to other validators simply by signing a block. To avoid these large changes that become specially dangerous in presence of EIP 7251, we can also use EIP 7002 deposit churn mechanism. This requires a simple change that when processing the execution payload header, instead of transferring the amount immediately, the builder is deducted immediately, but the total amount is churned on the deposit queue for the proposer. ## Optional changes ### No need for inclusion lists In an earlier design with heavily staked builders, forward forced inclusion lists were a mandatory ingredient. In the current design, there are better censor resistance guarantees than the status quo and we may delay inclusion list additions until a future fork. ### PTC does not attest validity One feature that seems promising and is a simple change from the current specification is to allow the PTC to attest just to the timeliness and essential checks of the `SignedExecutionPayloadEnvelope` (like those the CL does currently), but not require the full execution by an EL. Honest validators would not consider the payload valid anyway if they find this when importing the execution payload. This change entails also optimistically assigning the PTC status in forkchoice when applying payload boosts, or simply delaying processing of payload attestations until the full payload has been validated. ### Builder's minimal stake We could simply add a new validator withdrawal credential prefix for builders that has zero ejection balance. Thus allowing builders to have as low a stake as possible, enabling builders that want to participate only on minor block auctions. ### Payload attestations rewards The set of changes regarding payload attestations are minimal. In particular, any missed slot penalizes the PTC members by having a missed attestation. At the cost of slightly more technical overhead we could allow older payload attestations to be included in blocks and reward the attesters accordingly. ## Comparison with other approaches ### Payload equivocation Execution tickets or slot auctions have many interesting properties from both incentives perspectives as well as market efficiency and user experience overall. There is one complication that seems fundamental however and at least the author does not know how to solve. There are two problems to block equivocations. One is the fact that one may have multiple valid entries in forkchoice for the same slot. Another is the fact that they cause a head split view and if we have imported the wrong head, we need to request and import the right one. On ePBS, the structure of the forkchoice node becomes considerably more complicated by the addition of the payload status. A node with a consensus block can be either *empty* or *full*. That is, forkchoice nodes are parametrized by a pair `(Root, bool)` of the beacon block root and the presence of the payload. When arbitrary execution payloads can be seen associated to a given block, we move to a fully two dimensional problem `(Root, Root)`. This opens a myriad of edge cases on forkchoice attacks and head split views that at least this author does not know how to solve. ### Automatic blacklisting Systems that have off-protocol payload proposers make it very difficult to blacklist specific builders. A single entity that is willing to spend enough funds can either affect liveness of the chain or force local block production with the bound being on the funds and not on arbitrary heuristics of clients. This particular version of ePBS makes it trivial to automatically blacklist any builder that fails to broadcast a block. Without affecting the whole protocol. ### JIT builders Off-protocol builders allow them to be JIT opportunists, they see a trade that may be worth it and can submit bids for them. At least this author does not know how to enable such builders without having strong disadvantages to centralized trusted relays. ### Two rounds of voting / SSF Having two rounds of fully aggregated voting and specially under single slot finality would have much stronger forkchoice guarantees than the current proposal. Even without SSF, we do not know how to have two aggregation rounds in a slot without increasing considerably the slot time. ## FAQ #### Is self building still possible? Yes it is. #### Can builders do bid cancellation? Yes they can. #### Why would anyone use this? Proposers that currently can't choose non-censoring builders will be able to directly pick them. Builders that currently can't even try to enter the market due to trust issues with the relays will be able to enter. Builders that want to avoid competing with vertically integrated ones, can themselves be vertically integrated. Community members that monitor the network can guarantee a lowest bid by submitting vanilla EL bids to the P2P network. #### Will relays still play a role? They may, validators can still connect to them and request bids from them. There is no inherent advantage to them and in fact builders directly connected will have faster connections so validators are more likely to obtain better bids from the same builder by connecting to their external endpoint than through the relay. Very small builders or builders that don't want to stake may still chose to use relays. #### How about off-protocol solutions? They don't solve the problem in the [Introduction](#Introduction) #### Will clients still support MEV-Boost? There is no need to and most probably not. Clients will still implement a version of the Builder API to connect directly to builders. Relays can themselves be reached by this API. #### Will this centralize the builder system because of the staking required? 1. Currently 1 builder has over 80% of the market share, this system is strictly an improvement of the status quo. 2. We can minimize the stake to 0 if wanted allowing smaller builders to participate up to the stake they are willing to place. 3. JIT builders are not covered but can still participate through a trusted relay. 4. Client teams, community members, home stakers, etc. Will be able to open up builders easily and are already staked. They can't do so today. #### Isn't execution tickets a better solution? It may actually be, but a) I do not know how to implement ETs at the moment and b) this implementation can be ready and tested in a short time and is a step towards ETs. If the equivocation problem of slot auctions is solved, then it's a minimal change from ePBS to slot auctions and from there to ETs seems like a closer gap.