-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix valid Gloas bids rejected with InvalidGasLimit after an empty parent #9905
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
base: unstable
Are you sure you want to change the base?
Changes from 5 commits
242e463
32ac238
f795fdb
66f5f78
455c07a
28a036b
d0d560e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,12 +15,152 @@ use slot_clock::SlotClock; | |||||||||||||||||||
| use state_processing::signature_sets::{ | ||||||||||||||||||||
| execution_payload_bid_signature_set, get_builder_pubkey_from_state, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| use store::iter::ParentRootBlockIterator; | ||||||||||||||||||||
| use tracing::debug; | ||||||||||||||||||||
| use types::{ | ||||||||||||||||||||
| BeaconState, ChainSpec, EthSpec, ExecutionPayloadBid, SignedExecutionPayloadBid, | ||||||||||||||||||||
| SignedProposerPreferences, Slot, consts::gloas::PAYLOAD_BUILDER_VERSION, | ||||||||||||||||||||
| BeaconState, ChainSpec, EthSpec, ExecPayload, ExecutionBlockHash, ExecutionPayloadBid, Hash256, | ||||||||||||||||||||
| SignedBlindedBeaconBlock, SignedExecutionPayloadBid, SignedProposerPreferences, Slot, | ||||||||||||||||||||
| consts::gloas::PAYLOAD_BUILDER_VERSION, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub(super) enum ParentExecutionPayloadLocation { | ||||||||||||||||||||
| /// The beacon block identifying the parent execution payload is available in fork choice. | ||||||||||||||||||||
| BeaconBlock(Hash256), | ||||||||||||||||||||
| /// Continue the canonical ancestry search in the database from this beacon block root. | ||||||||||||||||||||
| SearchStoreFrom(Hash256), | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Locate the beacon block that identifies the bid's parent execution payload in fork choice. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe add additional info on what happens if its not found in fork choice? |
||||||||||||||||||||
| pub(super) fn locate_parent_execution_payload_in_fork_choice<T: BeaconChainTypes>( | ||||||||||||||||||||
| fork_choice_read: &ForkChoiceReadGuard<'_, T>, | ||||||||||||||||||||
| bid_parent_beacon_block_root: Hash256, | ||||||||||||||||||||
| parent_execution_block_hash: ExecutionBlockHash, | ||||||||||||||||||||
| ) -> ParentExecutionPayloadLocation { | ||||||||||||||||||||
| let mut block_root = bid_parent_beacon_block_root; | ||||||||||||||||||||
| while let Some(block) = fork_choice_read.get_block(&block_root) { | ||||||||||||||||||||
| // Gloas genesis has no payload envelope. Its bid retains the EL genesis hash in | ||||||||||||||||||||
| // `parent_block_hash`, while `block_hash` is zero to represent an empty payload. | ||||||||||||||||||||
| if block.slot == Slot::new(0) | ||||||||||||||||||||
| && block.execution_payload_parent_hash == Some(parent_execution_block_hash) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| return ParentExecutionPayloadLocation::BeaconBlock(block_root); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if let Some(block_hash) = block.execution_payload_block_hash { | ||||||||||||||||||||
| // Payload receipt is only recorded after the Gloas envelope has been validated. | ||||||||||||||||||||
| if fork_choice_read.is_payload_received(&block_root) | ||||||||||||||||||||
| && block_hash == parent_execution_block_hash | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we could also add an |
||||||||||||||||||||
| { | ||||||||||||||||||||
| return ParentExecutionPayloadLocation::BeaconBlock(block_root); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else if !block.execution_status.is_invalid() | ||||||||||||||||||||
| && block.execution_status.block_hash() == Some(parent_execution_block_hash) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| return ParentExecutionPayloadLocation::BeaconBlock(block_root); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let Some(parent_root) = block.parent_root else { | ||||||||||||||||||||
| break; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| block_root = parent_root; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ParentExecutionPayloadLocation::SearchStoreFrom(block_root) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Return the execution block hash and gas limit represented by a beacon block. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// Gloas genesis represents the EL genesis block with `parent_block_hash` because it has no payload | ||||||||||||||||||||
| /// envelope of its own. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit
Suggested change
|
||||||||||||||||||||
| fn execution_payload_hash_and_gas_limit<E: EthSpec>( | ||||||||||||||||||||
| block: &SignedBlindedBeaconBlock<E>, | ||||||||||||||||||||
| ) -> Result<(ExecutionBlockHash, u64), PayloadBidError> { | ||||||||||||||||||||
| if block.fork_name_unchecked().gloas_enabled() { | ||||||||||||||||||||
| let bid = &block | ||||||||||||||||||||
| .message() | ||||||||||||||||||||
| .body() | ||||||||||||||||||||
| .signed_execution_payload_bid()? | ||||||||||||||||||||
| .message; | ||||||||||||||||||||
| let block_hash = if block.slot() == Slot::new(0) { | ||||||||||||||||||||
| bid.parent_block_hash | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| bid.block_hash | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| Ok((block_hash, bid.gas_limit)) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| let payload = block.message().execution_payload()?; | ||||||||||||||||||||
| Ok((payload.block_hash(), payload.gas_limit())) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Continue searching canonical ancestry in the database after fork choice reaches its finalized | ||||||||||||||||||||
| /// boundary. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(this comment is only true if we add the |
||||||||||||||||||||
| fn find_parent_execution_payload_gas_limit_in_store<T: BeaconChainTypes>( | ||||||||||||||||||||
| store: &BeaconStore<T>, | ||||||||||||||||||||
| search_start_beacon_block_root: Hash256, | ||||||||||||||||||||
| parent_execution_block_hash: ExecutionBlockHash, | ||||||||||||||||||||
| ) -> Result<u64, PayloadBidError> { | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function could potentially iterate all the way back to genesis? am i wrong about that?
Maybe we should only enter this loop when |
||||||||||||||||||||
| for block_result in ParentRootBlockIterator::new(store.as_ref(), search_start_beacon_block_root) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| let (block_root, block) = block_result.map_err(|e| { | ||||||||||||||||||||
| PayloadBidError::InternalError(format!( | ||||||||||||||||||||
| "failed to load beacon block while finding execution payload: {e:?}" | ||||||||||||||||||||
| )) | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure this should be an internal error, I think it should be a |
||||||||||||||||||||
| })?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let (block_hash, gas_limit) = execution_payload_hash_and_gas_limit(&block)?; | ||||||||||||||||||||
| if block_hash == parent_execution_block_hash { | ||||||||||||||||||||
| // Non-genesis Gloas blocks only carry an execution payload after its envelope arrives. | ||||||||||||||||||||
| // Compare the hash first so unrelated ancestors do not require another database read. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit
Suggested change
|
||||||||||||||||||||
| if block.fork_name_unchecked().gloas_enabled() && block.slot() != Slot::new(0) { | ||||||||||||||||||||
| let payload_received = store.payload_envelope_exists(&block_root).map_err(|e| { | ||||||||||||||||||||
| PayloadBidError::InternalError(format!( | ||||||||||||||||||||
| "failed to check payload envelope for block {block_root:?}: {e:?}" | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| })?; | ||||||||||||||||||||
| if payload_received { | ||||||||||||||||||||
| return Ok(gas_limit); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| return Ok(gas_limit); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Err(PayloadBidError::ParentExecutionPayloadUnknown { | ||||||||||||||||||||
| parent_block_hash: parent_execution_block_hash, | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Load the gas limit for `parent_execution_block_hash` from the beacon block previously identified | ||||||||||||||||||||
| /// as representing that execution payload. | ||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
| fn get_parent_execution_payload_gas_limit_from_beacon_block<T: BeaconChainTypes>( | ||||||||||||||||||||
| store: &BeaconStore<T>, | ||||||||||||||||||||
| parent_execution_payload_beacon_block_root: Hash256, | ||||||||||||||||||||
| parent_execution_block_hash: ExecutionBlockHash, | ||||||||||||||||||||
| ) -> Result<u64, PayloadBidError> { | ||||||||||||||||||||
| let block = store | ||||||||||||||||||||
| .get_blinded_block(&parent_execution_payload_beacon_block_root) | ||||||||||||||||||||
| .map_err(|e| { | ||||||||||||||||||||
| PayloadBidError::InternalError(format!( | ||||||||||||||||||||
| "failed to load execution payload block {parent_execution_payload_beacon_block_root:?}: {e:?}" | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| })? | ||||||||||||||||||||
| .ok_or_else(|| { | ||||||||||||||||||||
| PayloadBidError::InternalError(format!( | ||||||||||||||||||||
| "execution payload block {parent_execution_payload_beacon_block_root:?} unavailable" | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| })?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let (block_hash, gas_limit) = execution_payload_hash_and_gas_limit(&block)?; | ||||||||||||||||||||
| if block_hash != parent_execution_block_hash { | ||||||||||||||||||||
| return Err(PayloadBidError::InternalError(format!( | ||||||||||||||||||||
| "execution payload hash mismatch for block {parent_execution_payload_beacon_block_root:?}" | ||||||||||||||||||||
| ))); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| Ok(gas_limit) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Verify that an execution payload bid is consistent with the current chain state | ||||||||||||||||||||
| /// and proposer preferences. | ||||||||||||||||||||
| pub(crate) fn verify_bid_consistency<E: EthSpec>( | ||||||||||||||||||||
|
|
@@ -302,24 +442,6 @@ impl<E: EthSpec> GossipVerifiedPayloadBid<E> { | |||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // TODO(gloas): [IGNORE] bid.parent_block_hash is the block hash of a known execution | ||||||||||||||||||||
| // payload in fork choice. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // TODO(gloas): This uses head state's bid gas_limit as parent_gas_limit, which is only | ||||||||||||||||||||
| // correct when the bid's parent is the head. If the parent is an ancestor further back | ||||||||||||||||||||
| // this check may be inaccurate. Fixing this requires storing | ||||||||||||||||||||
| // gas_limit in fork choice or looking it up from the store by parent_block_hash. Taking the above | ||||||||||||||||||||
| // TODO into consideration maybe should persist parent block hash and gas limit in fork choice? | ||||||||||||||||||||
| if let Ok(parent_bid) = head_state.latest_execution_payload_bid() | ||||||||||||||||||||
| && !is_gas_limit_target_compatible( | ||||||||||||||||||||
| parent_bid.gas_limit, | ||||||||||||||||||||
| signed_bid.message.gas_limit, | ||||||||||||||||||||
| proposer_preferences.message.target_gas_limit, | ||||||||||||||||||||
| )? | ||||||||||||||||||||
| { | ||||||||||||||||||||
| return Err(PayloadBidError::InvalidGasLimit); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| drop(fork_choice); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| verify_bid_consistency( | ||||||||||||||||||||
|
|
@@ -330,7 +452,26 @@ impl<E: EthSpec> GossipVerifiedPayloadBid<E> { | |||||||||||||||||||
| ctx.spec, | ||||||||||||||||||||
| )?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Verify signature | ||||||||||||||||||||
| let check_parent_gas_limit = |parent_gas_limit| { | ||||||||||||||||||||
| if is_gas_limit_target_compatible( | ||||||||||||||||||||
| parent_gas_limit, | ||||||||||||||||||||
| signed_bid.message.gas_limit, | ||||||||||||||||||||
| proposer_preferences.message.target_gas_limit, | ||||||||||||||||||||
| )? { | ||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| Err(PayloadBidError::InvalidGasLimit) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let cached_parent_gas_limit = ctx | ||||||||||||||||||||
| .gossip_verified_payload_bid_cache | ||||||||||||||||||||
| .get_parent_gas_limit(bid_slot, signed_bid.message.parent_block_hash); | ||||||||||||||||||||
| if let Some(parent_gas_limit) = cached_parent_gas_limit { | ||||||||||||||||||||
| check_parent_gas_limit(parent_gas_limit)?; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Verify the signature before falling back to a historical database scan. | ||||||||||||||||||||
| execution_payload_bid_signature_set( | ||||||||||||||||||||
| head_state, | ||||||||||||||||||||
| |i| get_builder_pubkey_from_state(head_state, i), | ||||||||||||||||||||
|
|
@@ -343,6 +484,38 @@ impl<E: EthSpec> GossipVerifiedPayloadBid<E> { | |||||||||||||||||||
| .then_some(()) | ||||||||||||||||||||
| .ok_or(PayloadBidError::BadSignature)?; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if cached_parent_gas_limit.is_none() { | ||||||||||||||||||||
| // Ancestor lookup can walk fork choice and the database, so only perform it for a bid | ||||||||||||||||||||
| // whose builder and signature have already been verified. | ||||||||||||||||||||
| let fork_choice = ctx.canonical_head.fork_choice_read_lock(); | ||||||||||||||||||||
| let location = locate_parent_execution_payload_in_fork_choice( | ||||||||||||||||||||
| &fork_choice, | ||||||||||||||||||||
| signed_bid.message.parent_block_root, | ||||||||||||||||||||
| signed_bid.message.parent_block_hash, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| drop(fork_choice); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let gas_limit = match location { | ||||||||||||||||||||
| ParentExecutionPayloadLocation::BeaconBlock(beacon_block_root) => { | ||||||||||||||||||||
| get_parent_execution_payload_gas_limit_from_beacon_block::<T>( | ||||||||||||||||||||
| ctx.store, | ||||||||||||||||||||
| beacon_block_root, | ||||||||||||||||||||
| signed_bid.message.parent_block_hash, | ||||||||||||||||||||
| )? | ||||||||||||||||||||
| } | ||||||||||||||||||||
| ParentExecutionPayloadLocation::SearchStoreFrom(beacon_block_root) => { | ||||||||||||||||||||
| find_parent_execution_payload_gas_limit_in_store::<T>( | ||||||||||||||||||||
| ctx.store, | ||||||||||||||||||||
| beacon_block_root, | ||||||||||||||||||||
| signed_bid.message.parent_block_hash, | ||||||||||||||||||||
| )? | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| ctx.gossip_verified_payload_bid_cache | ||||||||||||||||||||
| .insert_parent_gas_limit(bid_slot, signed_bid.message.parent_block_hash, gas_limit); | ||||||||||||||||||||
| check_parent_gas_limit(gas_limit)?; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let gossip_verified_bid = GossipVerifiedPayloadBid { signed_bid }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ctx.gossip_verified_payload_bid_cache | ||||||||||||||||||||
|
|
||||||||||||||||||||
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.
nit: this enum is a bit confusing to me
What if we renamed to something like
or something where fork choice found and not found is mentioned explicitly?