-
Notifications
You must be signed in to change notification settings - Fork 997
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
Conversation
Fix epbs consensus spec to be executable
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 |
There was a problem hiding this comment.
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:
return validator_indices | |
return Vector[ValidatorIndex, PTC_SIZE](validator_indices) |
or just change the function notation to return Sequence[ValidatorIndex]
.
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 I wanted to give it a proper review, but now I think it best to have a post-review rather than blocking it. |
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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))
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:
- Balances are decreased in the CL but are only increased in the EL at a later stage once the payload is revealed.
- 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.
- 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] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
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