Gloas alpha spec 8 (#9315)

https://github.com/ethereum/consensus-specs/releases/tag/v1.7.0-alpha.8


  


Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Eitan Seri-Levi
2026-05-21 23:21:20 -07:00
committed by GitHub
parent b5d5644eeb
commit 60abd4b5b9
36 changed files with 863 additions and 243 deletions

View File

@@ -96,8 +96,8 @@ use eth2::types::{
SseExtendedPayloadAttributes, SseHead,
};
use execution_layer::{
BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer,
FailedCondition, PayloadAttributes, PayloadStatus,
BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth,
DEFAULT_GAS_LIMIT, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus,
};
use fixed_bytes::FixedBytesExtended;
use fork_choice::{
@@ -2185,12 +2185,20 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// TODO(gloas) do we want to use a dedicated envelope cache instead?
// Maybe the new gloas DA cache? (Or should the gloas DA cache use
// the envelopes_times_cache internally?)
// the envelopes_times_cache internally?
// The payload is considered present only if it was observed before
// the payload due deadline (PAYLOAD_DUE_BPS into the slot).
let payload_due = self.spec.get_payload_due();
let payload_present = self
.envelope_times_cache
.read()
.cache
.contains_key(&beacon_block_root);
.get(&beacon_block_root)
.and_then(|entry| entry.timestamps.observed)
.is_some_and(|observed| {
let slot_start = self.slot_clock.start_of(request_slot);
slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due)
});
// TODO(EIP-7732): Check blob data availability. For now, default to true.
let blob_data_available = true;
@@ -6476,6 +6484,19 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
None
};
let target_gas_limit = if prepare_slot_fork.gloas_enabled() {
let proposer_gas_limit = execution_layer.get_proposer_gas_limit(proposer).await;
if proposer_gas_limit.is_none() {
warn!(
%proposer,
"No proposer gas limit configured, falling back to parent gas limit"
);
}
proposer_gas_limit.or(Some(DEFAULT_GAS_LIMIT))
} else {
None
};
let payload_attributes = PayloadAttributes::new(
self.slot_clock
.start_of(prepare_slot)
@@ -6486,6 +6507,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
withdrawals.map(Into::into),
parent_beacon_block_root,
slot_number,
target_gas_limit,
);
execution_layer

View File

@@ -2,11 +2,13 @@ use std::collections::{HashMap, HashSet};
use std::marker::PhantomData;
use std::sync::Arc;
use proto_array::PayloadStatus;
use bls::{PublicKeyBytes, Signature};
use execution_layer::{
BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters,
BlockProposalContentsGloas, BuilderParams, DEFAULT_GAS_LIMIT, PayloadAttributes,
PayloadParameters,
};
use fork_choice::PayloadStatus;
use operation_pool::CompactAttestationRef;
use ssz::Encode;
use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation};
@@ -150,8 +152,24 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
verification: ProduceBlockVerification,
builder_boost_factor: Option<u64>,
) -> Result<BlockProductionResult<T::EthSpec>, BlockProductionError> {
// Extract the parent's execution requests from the envelope (if parent was full).
let parent_execution_requests = if parent_payload_status == PayloadStatus::Full {
let parent_root = if state.slot() > 0 {
*state
.get_block_root(state.slot() - 1)
.map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)?
} else {
state.latest_block_header().canonical_root()
};
let should_build_on_full = self
.canonical_head
.fork_choice_read_lock()
.should_build_on_full(&parent_root, parent_payload_status)
.map_err(|e| {
BlockProductionError::BeaconChain(Box::new(BeaconChainError::ForkChoiceError(e)))
})?;
// Extract the parent's execution requests from the envelope (if building on full).
let parent_execution_requests = if should_build_on_full {
parent_envelope
.as_ref()
.map(|env| env.message.execution_requests.clone())
@@ -197,7 +215,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.clone()
.produce_execution_payload_bid(
state,
parent_payload_status,
should_build_on_full,
parent_envelope,
produce_at_slot,
BID_VALUE_SELF_BUILD,
@@ -700,12 +718,12 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
/// data needed to construct the `ExecutionPayloadEnvelope` after the beacon block is
/// created, plus the EL block value and `should_override_builder` flag used by the
/// caller to compare against any cached p2p builder bid.
#[allow(clippy::type_complexity)]
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
#[instrument(level = "debug", skip_all)]
pub async fn produce_execution_payload_bid(
self: Arc<Self>,
state: BeaconState<T::EthSpec>,
parent_payload_status: PayloadStatus,
should_build_on_full: bool,
parent_envelope: Option<Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>>,
produce_at_slot: Slot,
bid_value: u64,
@@ -751,20 +769,18 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let parent_bid = state.latest_execution_payload_bid()?;
// TODO(gloas): need should_extend_payload check here as well
let parent_block_slot = state.latest_block_header().slot;
let parent_is_pre_gloas = !self
.spec
.fork_name_at_slot::<T::EthSpec>(parent_block_slot)
.gloas_enabled();
let parent_block_hash =
if parent_payload_status == PayloadStatus::Full || parent_is_pre_gloas {
// Build on parent bid's payload.
parent_bid.block_hash
} else {
// Skip parent bid's payload. For genesis this is the EL genesis hash.
parent_bid.parent_block_hash
};
let parent_block_hash = if should_build_on_full || parent_is_pre_gloas {
// Build on parent bid's payload.
parent_bid.block_hash
} else {
// Skip parent bid's payload. For genesis this is the EL genesis hash.
parent_bid.parent_block_hash
};
// TODO(gloas) this should be BlockProductionVersion::V4
// V3 is okay for now as long as we're not connected to a builder
@@ -953,10 +969,7 @@ fn get_execution_payload_gloas<T: BeaconChainTypes>(
compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?;
let random = *state.get_randao_mix(current_epoch)?;
// TODO(gloas): this gas limit calc is not necessarily right
let parent_bid = state.latest_execution_payload_bid()?;
let latest_gas_limit = parent_bid.gas_limit;
let is_parent_block_full = parent_block_hash == parent_bid.block_hash;
let withdrawals = if is_parent_block_full {
@@ -992,7 +1005,6 @@ fn get_execution_payload_gloas<T: BeaconChainTypes>(
random,
proposer_index,
parent_block_hash,
latest_gas_limit,
builder_params,
withdrawals,
parent_beacon_block_root,
@@ -1020,7 +1032,6 @@ async fn prepare_execution_payload<T>(
random: Hash256,
proposer_index: u64,
parent_block_hash: ExecutionBlockHash,
parent_gas_limit: u64,
builder_params: BuilderParams,
withdrawals: Vec<Withdrawal>,
parent_beacon_block_root: Hash256,
@@ -1058,6 +1069,10 @@ where
.get_suggested_fee_recipient(proposer_index)
.await;
let slot_number = Some(builder_params.slot.as_u64());
let target_gas_limit = execution_layer
.get_proposer_gas_limit(proposer_index)
.await
.unwrap_or(DEFAULT_GAS_LIMIT);
let payload_attributes = PayloadAttributes::new(
timestamp,
@@ -1066,13 +1081,12 @@ where
Some(withdrawals),
Some(parent_beacon_block_root),
slot_number,
Some(target_gas_limit),
);
let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await;
let payload_parameters = PayloadParameters {
parent_hash: parent_block_hash,
parent_gas_limit,
proposer_gas_limit: target_gas_limit,
parent_gas_limit: None,
proposer_gas_limit: Some(target_gas_limit),
payload_attributes: &payload_attributes,
forkchoice_update_params: &forkchoice_update_params,
current_fork: fork,

View File

@@ -342,7 +342,7 @@ pub fn get_execution_payload<T: BeaconChainTypes>(
Ok(join_handle)
}
/// Prepares an execution payload for inclusion in a block.
/// Prepares an execution payload (pre-gloas) for inclusion in a block.
///
/// ## Errors
///
@@ -373,6 +373,13 @@ where
{
let spec = &chain.spec;
let fork = spec.fork_name_at_slot::<T::EthSpec>(builder_params.slot);
if fork.gloas_enabled() {
return Err(BlockProductionError::InvalidBlockVariant(
"Called pre-gloas prepare_execution_payload on a gloas block".to_string(),
));
}
let execution_layer = chain
.execution_layer
.as_ref()
@@ -403,25 +410,20 @@ where
.get_suggested_fee_recipient(proposer_index)
.await;
let slot_number = if fork.gloas_enabled() {
Some(builder_params.slot.as_u64())
} else {
None
};
let payload_attributes = PayloadAttributes::new(
timestamp,
random,
suggested_fee_recipient,
withdrawals,
parent_beacon_block_root,
slot_number,
None,
None,
);
let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await;
let payload_parameters = PayloadParameters {
parent_hash,
parent_gas_limit: latest_execution_payload_header_gas_limit,
parent_gas_limit: Some(latest_execution_payload_header_gas_limit),
proposer_gas_limit: target_gas_limit,
payload_attributes: &payload_attributes,
forkchoice_update_params: &forkchoice_update_params,

View File

@@ -43,9 +43,6 @@ pub(crate) fn verify_bid_consistency<E: EthSpec>(
if bid.fee_recipient != proposer_preferences.message.fee_recipient {
return Err(PayloadBidError::InvalidFeeRecipient);
}
if bid.gas_limit != proposer_preferences.message.gas_limit {
return Err(PayloadBidError::InvalidGasLimit);
}
let max_blobs_per_block =
spec.max_blobs_per_block(bid_slot.epoch(E::slots_per_epoch())) as usize;
@@ -161,7 +158,23 @@ impl<T: BeaconChainTypes> GossipVerifiedPayloadBid<T> {
});
}
// TODO(gloas) [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice.
// 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);
@@ -263,8 +276,36 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
}
/// Check if `gas_limit` is compatible with `target_gas_limit` under the
/// EIP-1559 transition rule from `parent_gas_limit`.
pub fn is_gas_limit_target_compatible(
parent_gas_limit: u64,
gas_limit: u64,
target_gas_limit: u64,
) -> Result<bool, PayloadBidError> {
let max_gas_limit_difference = (parent_gas_limit / 1024)
.max(1)
.checked_sub(1)
.ok_or(PayloadBidError::InvalidGasLimit)?;
let min_gas_limit = parent_gas_limit
.checked_sub(max_gas_limit_difference)
.ok_or(PayloadBidError::InvalidGasLimit)?;
let max_gas_limit = parent_gas_limit
.checked_add(max_gas_limit_difference)
.ok_or(PayloadBidError::InvalidGasLimit)?;
if target_gas_limit >= min_gas_limit && target_gas_limit <= max_gas_limit {
Ok(gas_limit == target_gas_limit)
} else if target_gas_limit > max_gas_limit {
Ok(gas_limit == max_gas_limit)
} else {
Ok(gas_limit == min_gas_limit)
}
}
#[cfg(test)]
mod tests {
use super::is_gas_limit_target_compatible;
use bls::Signature;
use kzg::KzgCommitment;
use ssz_types::VariableList;
@@ -288,11 +329,14 @@ mod tests {
}
}
fn make_preferences(fee_recipient: Address, gas_limit: u64) -> SignedProposerPreferences {
fn make_preferences(
fee_recipient: Address,
target_gas_limit: u64,
) -> SignedProposerPreferences {
SignedProposerPreferences {
message: ProposerPreferences {
fee_recipient,
gas_limit,
target_gas_limit,
..ProposerPreferences::default()
},
signature: Signature::empty(),
@@ -382,13 +426,41 @@ mod tests {
}
#[test]
fn test_gas_limit_mismatch() {
let (state, spec) = state_and_spec();
let current_slot = Slot::new(10);
let bid = make_bid(current_slot, Address::ZERO, 30_000_000);
let prefs = make_preferences(Address::ZERO, 50_000_000);
fn test_is_gas_limit_target_compatible_increase_within_limit() {
assert!(is_gas_limit_target_compatible(60_000_000, 60_000_100, 60_000_100).unwrap());
}
let result = verify_bid_consistency::<E>(&bid, current_slot, &prefs, &state, &spec);
assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit)));
#[test]
fn test_is_gas_limit_target_compatible_increase_exceeding_limit() {
// max_diff = 60_000_000 / 1024 - 1 = 58_592
// max_gas_limit = 60_000_000 + 58_592 = 60_058_592
assert!(is_gas_limit_target_compatible(60_000_000, 60_058_592, 100_000_000).unwrap());
}
#[test]
fn test_is_gas_limit_target_compatible_increase_exceeding_off_by_one() {
assert!(!is_gas_limit_target_compatible(60_000_000, 60_058_593, 100_000_000).unwrap());
}
#[test]
fn test_is_gas_limit_target_compatible_decrease_within_limit() {
assert!(is_gas_limit_target_compatible(60_000_000, 59_999_990, 59_999_990).unwrap());
}
#[test]
fn test_is_gas_limit_target_compatible_decrease_exceeding_limit() {
// min_gas_limit = 60_000_000 - 58_592 = 59_941_408
assert!(is_gas_limit_target_compatible(60_000_000, 59_941_408, 30_000_000).unwrap());
}
#[test]
fn test_is_gas_limit_target_compatible_target_equals_parent() {
assert!(is_gas_limit_target_compatible(60_000_000, 60_000_000, 60_000_000).unwrap());
}
#[test]
fn test_is_gas_limit_target_compatible_parent_underflows() {
// parent=1023: max(1023/1024, 1) - 1 = max(0, 1) - 1 = 0, no change allowed
assert!(is_gas_limit_target_compatible(1023, 1023, 60_000_000).unwrap());
}
}

View File

@@ -48,7 +48,7 @@ pub enum PayloadBidError {
},
/// The bids fee recipient doesn't match the proposer preferences fee recipient.
InvalidFeeRecipient,
/// The bids gas limit doesn't match the proposer preferences gas limit.
/// The bid's gas limit is not compatible with the proposer's target gas limit.
InvalidGasLimit,
/// The bids execution payment is non-zero
ExecutionPaymentNonZero { execution_payment: u64 },

View File

@@ -101,6 +101,17 @@ impl TestContext {
root: Hash256::ZERO,
};
// Set a non-zero gas_limit on latest_execution_payload_bid so the gas limit
// compatibility check doesn't reject all bids at genesis.
if let Ok(bid) = state.latest_execution_payload_bid_mut() {
bid.gas_limit = 30_000_000;
}
// Update body_root to reflect the modified bid (genesis block embeds it).
let genesis_body_root = genesis_block(&state, &spec)
.expect("should build genesis block")
.body_root();
state.latest_block_header_mut().body_root = genesis_body_root;
let inactive_keypair = &keypairs[NUM_BUILDERS];
let inactive_creds = builder_withdrawal_credentials(&inactive_keypair.pk, &spec);
let inactive_builder_index = state
@@ -248,7 +259,7 @@ fn make_signed_preferences(
proposal_slot: Slot,
validator_index: u64,
fee_recipient: Address,
gas_limit: u64,
target_gas_limit: u64,
) -> Arc<SignedProposerPreferences> {
Arc::new(SignedProposerPreferences {
message: ProposerPreferences {
@@ -256,7 +267,7 @@ fn make_signed_preferences(
proposal_slot,
validator_index,
fee_recipient,
gas_limit,
target_gas_limit,
},
signature: Signature::empty(),
})

View File

@@ -18,13 +18,16 @@ pub(crate) fn verify_preferences_consistency<E: EthSpec>(
preferences: &ProposerPreferences,
current_slot: Slot,
head_state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<(), ProposerPreferencesError> {
let proposal_slot = preferences.proposal_slot;
let validator_index = preferences.validator_index;
let current_epoch = current_slot.epoch(E::slots_per_epoch());
let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch());
if proposal_epoch < current_epoch || proposal_epoch > current_epoch.saturating_add(1u64) {
if proposal_epoch < current_epoch
|| proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead)
{
return Err(ProposerPreferencesError::InvalidProposalEpoch { proposal_epoch });
}
@@ -35,7 +38,7 @@ pub(crate) fn verify_preferences_consistency<E: EthSpec>(
});
}
if !head_state.is_valid_proposal_slot(preferences)? {
if !head_state.is_valid_proposal_slot(preferences, spec)? {
return Err(ProposerPreferencesError::InvalidProposalSlot {
validator_index,
proposal_slot,
@@ -83,7 +86,12 @@ impl GossipVerifiedProposerPreferences {
});
}
verify_preferences_consistency(&signed_preferences.message, current_slot, head_state)?;
verify_preferences_consistency(
&signed_preferences.message,
current_slot,
head_state,
ctx.spec,
)?;
// Verify signature
proposer_preferences_signature_set(
@@ -155,11 +163,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
#[cfg(test)]
mod tests {
use types::{
Address, BeaconState, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, Slot,
Address, BeaconState, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences,
Slot,
};
use super::verify_preferences_consistency;
use crate::proposer_preferences_verification::ProposerPreferencesError;
use crate::test_utils::{fork_name_from_env, test_spec};
type E = MinimalEthSpec;
@@ -169,20 +179,28 @@ mod tests {
proposal_slot,
validator_index,
fee_recipient: Address::ZERO,
gas_limit: 30_000_000,
target_gas_limit: 30_000_000,
}
}
fn state() -> BeaconState<E> {
BeaconState::new(0, <_>::default(), &E::default_spec())
let spec = spec();
BeaconState::new(0, <_>::default(), &spec)
}
fn spec() -> ChainSpec {
test_spec::<E>()
}
#[test]
fn test_invalid_epoch_too_old() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let current_slot = Slot::new(2 * E::slots_per_epoch());
let prefs = make_preferences(Slot::new(3), 0);
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state());
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state(), &spec());
assert!(matches!(
result,
Err(ProposerPreferencesError::InvalidProposalEpoch { .. })
@@ -191,10 +209,13 @@ mod tests {
#[test]
fn test_invalid_epoch_too_far_ahead() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let current_slot = Slot::new(E::slots_per_epoch());
let prefs = make_preferences(Slot::new(3 * E::slots_per_epoch() + 1), 0);
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state());
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state(), &spec());
assert!(matches!(
result,
Err(ProposerPreferencesError::InvalidProposalEpoch { .. })
@@ -203,10 +224,13 @@ mod tests {
#[test]
fn test_proposal_slot_already_passed() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let current_slot = Slot::new(10);
let prefs = make_preferences(Slot::new(9), 0);
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state());
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state(), &spec());
assert!(matches!(
result,
Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. })
@@ -215,10 +239,13 @@ mod tests {
#[test]
fn test_proposal_slot_equal_to_current() {
if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) {
return;
}
let current_slot = Slot::new(10);
let prefs = make_preferences(Slot::new(10), 0);
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state());
let result = verify_preferences_consistency::<E>(&prefs, current_slot, &state(), &spec());
assert!(matches!(
result,
Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. })

View File

@@ -24,7 +24,7 @@ mod tests;
#[derive(Debug)]
pub enum ProposerPreferencesError {
/// The proposal slot is not in the current or next epoch.
/// The proposal slot is not within the proposer lookahead.
InvalidProposalEpoch { proposal_epoch: Epoch },
/// The proposal slot has already passed.
ProposalSlotAlreadyPassed {

View File

@@ -87,7 +87,7 @@ mod tests {
proposal_slot: slot,
validator_index,
fee_recipient: Address::ZERO,
gas_limit: 30_000_000,
target_gas_limit: 30_000_000,
},
signature: Signature::empty(),
}),

View File

@@ -112,7 +112,7 @@ impl TestContext {
let slot_in_epoch = slot.as_usize() % E::slots_per_epoch() as usize;
let epoch = slot.epoch(E::slots_per_epoch());
let current_epoch = state.slot().epoch(E::slots_per_epoch());
let index = if epoch == current_epoch.saturating_add(1u64) {
let index = if epoch == current_epoch.saturating_add(self.spec.min_seed_lookahead) {
E::slots_per_epoch() as usize + slot_in_epoch
} else {
slot_in_epoch
@@ -131,7 +131,7 @@ fn make_signed_preferences(
proposal_slot,
validator_index,
fee_recipient: Address::ZERO,
gas_limit: 30_000_000,
target_gas_limit: 30_000_000,
},
signature: Signature::empty(),
})
@@ -271,7 +271,7 @@ fn same_validator_different_dependent_root_not_deduplicated() {
validator_index: 42,
dependent_root: Hash256::repeat_byte(0xaa),
fee_recipient: Address::ZERO,
gas_limit: 30_000_000,
target_gas_limit: 30_000_000,
},
signature: Signature::empty(),
}),