Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-7732: Enshrined Proposer-Builder Separation #3828

Merged
merged 56 commits into from
Aug 6, 2024

Conversation

potuz
Copy link
Contributor

@potuz potuz commented Jul 2, 2024

This PR implements the necessary changes to separate the processing of an Ethereum block into a consensus and an execution part. It also implements an auction mechanism for a consensus proposer to choose the proposer of the execution part of the block (called a builder in the documentation).

The full design notes are included in https://hackmd.io/@potuz/rJ9GCnT1C
Forkchoice annotated spec can be found in https://hackmd.io/@potuz/SJdXM43x0
Validator guide annotated spec can be found in https://hackmd.io/@ttsao/epbs-annotated-validator

@potuz potuz changed the title Ensrhined Proposer-Builder Separation EIP-7732: Ensrhined Proposer-Builder Separation Jul 3, 2024
@potuz potuz changed the title EIP-7732: Ensrhined Proposer-Builder Separation EIP-7732: Enshrined Proposer-Builder Separation Jul 3, 2024
potuz added 2 commits July 24, 2024 16:02
The damned linter complains and there's no problem having it

This reverts commit b7461ac.
for idx in range(committees_per_slot):
beacon_committee = get_beacon_committee(state, slot, CommitteeIndex(idx))
validator_indices += beacon_committee[:members_per_committee]
return validator_indices
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type doesn't match.

You can cast in the end:

Suggested change
return validator_indices
return Vector[ValidatorIndex, PTC_SIZE](validator_indices)

or just change the function notation to return Sequence[ValidatorIndex].

@hwwhww
Copy link
Contributor

hwwhww commented Aug 6, 2024

I'm so sorry for keeping you pending 😭 It was mainly because the release cycle got delayed, and I didn't want this huge PR to expand the scale of the v1.5.0-alpha.4.

I wanted to give it a proper review, but now I think it best to have a post-review rather than blocking it.

:shipit:

@hwwhww hwwhww merged commit 8f8ab03 into ethereum:dev Aug 6, 2024
18 checks passed
@hwwhww hwwhww added the general:enhancement New feature or request label Aug 6, 2024
state.latest_block_header.state_root = previous_state_root

# Verify consistency with the beacon block
assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces an extra round trip because it means that SignedExecutionPayloadEnvelope can only be published after SignedBeaconBlock is available. For a pure consistency check, the block_hash check may possibly be sufficient.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the P2P gossip's perspective, I won't be able to import or forward the SignedExecutionPayloadEnvelope until I have seen the beacon block that includes the header referencing the SignedExecutionPayloadEnvelope.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An envelope may also refer to same block hash with different block roots

Copy link
Contributor

@etan-status etan-status Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that a problem? If multiple BeaconBlock refer to the same ExecutionPayload, why do we have to propagate the ExecutionPayload multiple times?

Note it's only possible with equivocation (or heavy branching), because the ExecutionPayload hashes over the timestamp (so is based on slot) and parent_block_root (so is based on the branch)

assert envelope.builder_index == committed_header.builder_index
assert committed_header.blob_kzg_commitments_root == hash_tree_root(envelope.blob_kzg_commitments)

if not envelope.payload_withheld:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a cooperative mechanism, wondering how the performance of the system is impacted if the entire message is withheld (as in, no SignedExecutionPayloadEnvelope). If the performance is still okay, not sure if the explicit path to withhold the payload is necessary.


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this check required? Also, state is already updated, and is not reverted if verification fails.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without checking, how do we ensure the processing of execution payload is correct? ie latest_block_hash and latest_full_slot

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The envelope.payload.state_root is the EL post-state root and used to check that the execution payload processed correctly.

envelope.state_root refers to some CL post-state. It is not linked to the execution payload.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Here envelope.state_root is the EL post state root used to check that the execution payload processed correctly
envelope is signed_envelope.message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no. the EL post state root is envelope.payload.state_root.

envelope.state_root is hash_tree_root(state) (of the beacon state -- EL does not use htr)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misspoke. I meant the EL post-state root as the state root after processing the execution payload envelope, but that's not accurate. What I intended to say is that we need to check signed_envelope.message.state_root == hash_tree_root(state) to ensure that the post-state aligns. This is similar to checking block.state_root == hash_tree_root(state) after running the state transition for the beacon state.

# ePBS
latest_block_hash=pre.latest_execution_payload_header.block_hash, # [New in EIP-7732]
latest_full_slot=pre.slot, # [New in EIP-7732]
latest_withdrawals_root=Root(), # [New in EIP-7732]
Copy link
Contributor

@etan-status etan-status Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this trigger bugs lateron in process_execution_payload's withdrawals root check?

# Verify consistency of the parent hash with respect to the previous execution payload
assert payload.parent_hash == state.latest_block_hash
# Verify prev_randao
assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work?

At slot N, builders build payloads based on prev_randao up through the latest BeaconBlock (so, up through slot N-1).

Then, the proposer picks one of the SignedExecutionPayloadHeader (which has a block_hash based on get_randao_mix up through N-1), and puts it into a BeaconBlock.

Then, validators apply that BeaconBlock, advancing get_randao_mix in the state.

So, later, when the payload is actually revealed, get_randao_mix now includes info up through slot N, but at the time that block_hash was computed it was only up through slot N-1.

I doubt the correctness of the logic here, or maybe need additional clarification on how this is intended.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no different than builders built block for mev-boost today no? It's the same check we have process_execution_payload and the builder just takes the head state and uses get_randao_mix(state, get_current_epoch(state))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before ePBS, process_randao is done after process_execution_payload.

With ePBS, RANDAO mix is already updated at time that execution payload is processed, so validation will fail.

def process_block(state: BeaconState, block: BeaconBlock) -> None:
    process_block_header(state, block)
    process_withdrawals(state, block.body.execution_payload)
    process_execution_payload(state, block.body, EXECUTION_ENGINE)
    process_randao(state, block.body)
    process_eth1_data(state, block.body)
    process_operations(state, block.body)
    process_sync_aggregate(state, block.body.sync_aggregate)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I'm convinced you are right. Thanks! I also let potuz know, he is looking at it now


#### Modified `get_attesting_indices`

`get_attesting_indices` is modified to ignore PTC votes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this prevent finality in 2 epochs on the minimal preset in tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like, the PTC is still counted in the denominator, just not in the numerator

# Operations
proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]
attestations: List[Attestation, MAX_ATTESTATIONS]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not MAX_ATTESTATIONS_ELECTRA? Do we need 128 once more?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think this is a bug

# Execution
# ---------------------------------------------------------------
# 2**9 (= 512)
PTC_SIZE: 512
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the tests, the number of initial validators is SLOTS_PER_EPOCH * 8. so, 32 * 8 = 256 in mainnet. The PTC_SIZE here is double than the amount of validators. This, combined with attestations by the PTC not counting, smells like potential finality issues in tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya, tests will fail for sure, we either decrease the ptc size for minimal or have to sample ptc like we do in sync committee, it's still an open question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the PTC even influence justification / finality? it seems orthogonal to me, execution has nothing to do with consensus.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as in, why is the PTC even having an effect on finality weighing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the writing spec's perspective, it's easier to borrow PTC from the beacon attestation committee. 512 is a non-negotiable number, but as you said, this causes issues for minimal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on mainnet preset, the tests run with 256 validators (32*8)

# Execution
# ---------------------------------------------------------------
# 2**1(= 2)
PTC_SIZE: 2
Copy link
Contributor

@etan-status etan-status Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we have 2 out of 64 validators. But because validators are split up into SLOTS_PER_EPOCH committees, only 8 of them vote on each slot. So, 25% is PTC which does not count in the numerator but counts in the denominator. If there is some randomness involves, may slow down finality (but not fully break it at least).


```python
def process_withdrawals(state: BeaconState) -> None:
# return early if the parent block was empty
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has an interesting scenario which may warrant a test:

  1. Balances are decreased in the CL but are only increased in the EL at a later stage once the payload is revealed.
  2. Assume that revealing fails for a while for some reason, but the CL still continues. Meaning the balances have been decreased in the CL but withdrawals are still pending.
  3. Assume this goes on for like 20 minutes and the chain finalizes.

Someone doing checkpoint sync from the finalized checkpoint will now no longer be able to checkpoint sync, because they do not know the actual withdrawals from get_expected_withdrawals as they have already been processed in the CL. Because they only have data from finalized checkpoint onward, they are now unable to send the expected withdrawals to an EL and will not be able to propose any blocks until someone else who still has the data recovers the situation and manages to properly reveal a payload.

pending_consolidations=pre.pending_consolidations,
# ePBS
latest_block_hash=pre.latest_execution_payload_header.block_hash, # [New in EIP-7732]
latest_full_slot=pre.slot, # [New in EIP-7732]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pre.slot? No BeaconBlock has been received for this slot yet, and there may have been empty blocks before that.


```python
def is_parent_block_full(state: BeaconState) -> bool:
return state.latest_execution_payload_header.block_hash == state.latest_block_hash
Copy link
Contributor

@etan-status etan-status Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tests, block_hash may be zero hash, and then this may have false positives.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point!


for_ops(payload.deposit_requests, process_deposit_request)
for_ops(payload.withdrawal_requests, process_withdrawal_request)
for_ops(payload, process_consolidation_request)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be payload.consolidation_requests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
general:enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants