Spec v1.7.0-alpha.6 and Gloas genesis (#9190)

Co-Authored-By: Josh King <josh@sigmaprime.io>

Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
jking-aus
2026-04-29 10:23:24 +02:00
committed by GitHub
parent e8c865dcc6
commit 16132a3694
35 changed files with 349 additions and 117 deletions

View File

@@ -5023,11 +5023,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
.ok_or(Error::MissingExecutionPayloadEnvelope(parent_block_root))?;
let parent_bid = advanced_state.latest_execution_payload_bid()?.clone();
apply_parent_execution_payload(
&mut advanced_state,
&parent_bid,
&envelope.message.execution_requests,
&self.spec,
)

View File

@@ -623,11 +623,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
// For trustless building, the builder will provide the envelope separately.
if let Some(payload_data) = payload_data {
let beacon_block_root = block.tree_hash_root();
let parent_beacon_block_root = block.parent_root();
let execution_payload_envelope = ExecutionPayloadEnvelope {
payload: payload_data.payload,
execution_requests: payload_data.execution_requests,
builder_index: payload_data.builder_index,
beacon_block_root,
parent_beacon_block_root,
};
let signed_envelope = SignedExecutionPayloadEnvelope {
@@ -854,7 +856,6 @@ fn get_execution_payload_gloas<T: BeaconChainTypes>(
let mut withdrawals_state = state.clone();
apply_parent_execution_payload(
&mut withdrawals_state,
parent_bid,
&envelope.message.execution_requests,
spec,
)?;

View File

@@ -256,6 +256,7 @@ fn make_signed_preferences(
validator_index,
fee_recipient,
gas_limit,
..ProposerPreferences::default()
},
signature: Signature::empty(),
})

View File

@@ -72,6 +72,7 @@ fn build_chain(
execution_requests: Default::default(),
builder_index: 0,
beacon_block_root: block_root,
parent_beacon_block_root: Hash256::ZERO,
},
signature: Signature::empty(),
})

View File

@@ -339,6 +339,7 @@ mod tests {
execution_requests: ExecutionRequests::default(),
builder_index,
beacon_block_root: Hash256::ZERO,
parent_beacon_block_root: Hash256::ZERO,
}
}

View File

@@ -87,7 +87,7 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas {
execution_payload: &envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: block.message().parent_root(),
parent_beacon_block_root: envelope.message.parent_beacon_block_root,
execution_requests: &envelope.message.execution_requests,
}))
}

View File

@@ -105,6 +105,7 @@ mod tests {
execution_requests: ExecutionRequests::default(),
builder_index: 0,
beacon_block_root: Hash256::ZERO,
parent_beacon_block_root: Hash256::ZERO,
},
blobs: None,
}

View File

@@ -64,6 +64,7 @@ impl GossipVerifiedProposerPreferences {
ctx: &GossipVerificationContext<'_, T>,
) -> Result<Self, ProposerPreferencesError> {
let proposal_slot = signed_preferences.message.proposal_slot;
let checkpoint_root = signed_preferences.message.checkpoint_root;
let validator_index = signed_preferences.message.validator_index;
let cached_head = ctx.canonical_head.cached_head();
let current_slot = ctx
@@ -74,7 +75,7 @@ impl GossipVerifiedProposerPreferences {
if ctx
.gossip_verified_proposer_preferences_cache
.get_seen_validator(&proposal_slot, validator_index)
.get_seen_validator(&proposal_slot, checkpoint_root, validator_index)
{
return Err(ProposerPreferencesError::AlreadySeen {
validator_index,
@@ -162,6 +163,7 @@ mod tests {
fn make_preferences(proposal_slot: Slot, validator_index: u64) -> ProposerPreferences {
ProposerPreferences {
checkpoint_root: types::Hash256::ZERO,
proposal_slot,
validator_index,
fee_recipient: Address::ZERO,

View File

@@ -5,11 +5,11 @@ use std::{
use crate::proposer_preferences_verification::gossip_verified_proposer_preferences::GossipVerifiedProposerPreferences;
use parking_lot::RwLock;
use types::{SignedProposerPreferences, Slot};
use types::{Hash256, SignedProposerPreferences, Slot};
pub struct GossipVerifiedProposerPreferenceCache {
preferences: RwLock<BTreeMap<Slot, GossipVerifiedProposerPreferences>>,
seen: RwLock<BTreeMap<Slot, HashSet<u64>>>,
seen: RwLock<BTreeMap<Slot, HashSet<(Hash256, u64)>>>,
}
impl Default for GossipVerifiedProposerPreferenceCache {
@@ -34,21 +34,27 @@ impl GossipVerifiedProposerPreferenceCache {
self.preferences.write().insert(slot, preferences);
}
pub fn get_seen_validator(&self, slot: &Slot, validator_index: u64) -> bool {
pub fn get_seen_validator(
&self,
slot: &Slot,
checkpoint_root: Hash256,
validator_index: u64,
) -> bool {
self.seen
.read()
.get(slot)
.is_some_and(|seen| seen.contains(&validator_index))
.is_some_and(|seen| seen.contains(&(checkpoint_root, validator_index)))
}
pub fn insert_seen_validator(&self, preferences: &GossipVerifiedProposerPreferences) {
let slot = preferences.signed_preferences.message.proposal_slot;
let checkpoint_root = preferences.signed_preferences.message.checkpoint_root;
let validator_index = preferences.signed_preferences.message.validator_index;
self.seen
.write()
.entry(slot)
.or_default()
.insert(validator_index);
.insert((checkpoint_root, validator_index));
}
pub fn prune(&self, current_slot: Slot) {
@@ -77,6 +83,7 @@ mod tests {
validator_index,
fee_recipient: Address::ZERO,
gas_limit: 30_000_000,
..ProposerPreferences::default()
},
signature: Signature::empty(),
}),
@@ -97,11 +104,11 @@ mod tests {
for slot in [1, 2, 3, 7] {
assert!(cache.get_preferences(&Slot::new(slot)).is_none());
assert!(!cache.get_seen_validator(&Slot::new(slot), slot));
assert!(!cache.get_seen_validator(&Slot::new(slot), types::Hash256::ZERO, slot));
}
for slot in [8, 9, 10] {
assert!(cache.get_preferences(&Slot::new(slot)).is_some());
assert!(cache.get_seen_validator(&Slot::new(slot), slot));
assert!(cache.get_seen_validator(&Slot::new(slot), types::Hash256::ZERO, slot));
}
}
}

View File

@@ -131,6 +131,7 @@ fn make_signed_preferences(
validator_index,
fee_recipient: Address::ZERO,
gas_limit: 30_000_000,
..ProposerPreferences::default()
},
signature: Signature::empty(),
})
@@ -230,10 +231,11 @@ fn correct_proposer_bad_signature() {
result,
Err(ProposerPreferencesError::BadSignature)
));
assert!(
!ctx.preferences_cache
.get_seen_validator(&slot, actual_proposer)
);
assert!(!ctx.preferences_cache.get_seen_validator(
&slot,
types::Hash256::ZERO,
actual_proposer
));
assert!(ctx.preferences_cache.get_preferences(&slot).is_none());
}

View File

@@ -229,9 +229,6 @@ async fn prepare_payload_generic(
// `apply_parent_execution_payload`.
let cached_head = harness.chain.canonical_head.cached_head();
let unadvanced_empty_state = &cached_head.snapshot.beacon_state;
let parent_bid = unadvanced_empty_state
.latest_execution_payload_bid()
.unwrap();
let mut advanced_empty_state = unadvanced_empty_state.clone();
complete_state_advance(&mut advanced_empty_state, None, prepare_slot, &spec).unwrap();
@@ -239,7 +236,6 @@ async fn prepare_payload_generic(
let mut unadvanced_full_state = unadvanced_empty_state.clone();
apply_parent_execution_payload(
&mut unadvanced_full_state,
parent_bid,
&envelope.message.execution_requests,
&spec,
)
@@ -248,7 +244,6 @@ async fn prepare_payload_generic(
let mut advanced_full_state = advanced_empty_state.clone();
apply_parent_execution_payload(
&mut advanced_full_state,
parent_bid,
&envelope.message.execution_requests,
&spec,
)

View File

@@ -2131,6 +2131,7 @@ fn make_test_payload_envelope(
execution_requests: ExecutionRequests::default(),
builder_index: 0,
beacon_block_root,
parent_beacon_block_root: Hash256::ZERO,
},
signature: Signature::empty(),
}

View File

@@ -23,14 +23,6 @@ use types::{
four_byte_option_impl!(four_byte_option_usize, usize);
four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint);
fn all_true_bitvector<N: typenum::Unsigned + Clone>() -> BitVector<N> {
let mut bv = BitVector::new();
for i in 0..bv.len() {
let _ = bv.set(i, true);
}
bv
}
/// Defines an operation which may invalidate the `execution_status` of some nodes.
#[derive(Clone, Debug)]
pub enum InvalidationOperation {
@@ -568,10 +560,8 @@ impl ProtoArray {
ProtoNode::V29(v29) => {
// Both parent and child are Gloas blocks. The parent is full if the
// block hash in the parent node matches the parent block hash in the
// child bid and the parent block isn't the genesis block.
if v29.execution_payload_block_hash != ExecutionBlockHash::zero()
&& execution_payload_parent_hash == v29.execution_payload_block_hash
{
// child bid.
if execution_payload_parent_hash == v29.execution_payload_block_hash {
PayloadStatus::Full
} else {
PayloadStatus::Empty
@@ -613,18 +603,8 @@ impl ProtoArray {
full_payload_weight: 0,
execution_payload_block_hash,
execution_payload_parent_hash,
// Per spec `get_forkchoice_store`: the anchor block's PTC votes are
// initialized to all-True.
payload_timeliness_votes: if is_anchor {
all_true_bitvector()
} else {
BitVector::default()
},
payload_data_availability_votes: if is_anchor {
all_true_bitvector()
} else {
BitVector::default()
},
payload_timeliness_votes: BitVector::default(),
payload_data_availability_votes: BitVector::default(),
payload_received: false,
proposer_index,
// Spec: `record_block_timeliness` + `get_forkchoice_store`.

View File

@@ -26,6 +26,12 @@ pub enum EnvelopeProcessingError {
envelope_root: Hash256,
block_header_root: Hash256,
},
/// Envelope's `parent_beacon_block_root` doesn't match the parent root of the latest
/// block header.
ParentBeaconBlockRootMismatch {
envelope: Hash256,
state: Hash256,
},
/// Envelope doesn't match latest beacon block slot
SlotMismatch {
envelope_slot: Slot,
@@ -126,6 +132,13 @@ pub fn verify_execution_payload_envelope<E: EthSpec>(
block_header_root: latest_block_header_root,
}
);
envelope_verify!(
envelope.parent_beacon_block_root == state.latest_block_header().parent_root,
EnvelopeProcessingError::ParentBeaconBlockRootMismatch {
envelope: envelope.parent_beacon_block_root,
state: state.latest_block_header().parent_root,
}
);
envelope_verify!(
envelope.slot() == state.slot(),
EnvelopeProcessingError::SlotMismatch {

View File

@@ -175,13 +175,11 @@ pub fn initialize_beacon_state_from_eth1<E: EthSpec>(
bid.parent_block_hash = el_genesis_hash;
bid.block_hash = ExecutionBlockHash::default();
// Update latest_block_header to reflect the Gloas genesis block body which contains
// the EL genesis hash in the signed_execution_payload_bid. This is needed because
// BeaconState::new() created the header from BeaconBlock::empty() which has zero bid
// fields, but the spec requires the genesis block's bid to contain the EL block hash
// and the tree hash root of empty ExecutionRequests.
let block = genesis_block(&state, spec)?;
state.latest_block_header_mut().body_root = block.body_root();
// Update the `latest_block_header.body_root` so that it matches the body of the
// Gloas genesis block, which embeds `state.latest_execution_payload_bid` in its
// `signed_execution_payload_bid` field (see `genesis_block`).
let genesis_body_root = genesis_block(&state, spec)?.body_root();
state.latest_block_header_mut().body_root = genesis_body_root;
}
// Now that we have our validators, initialize the caches (including the committees)
@@ -193,24 +191,23 @@ pub fn initialize_beacon_state_from_eth1<E: EthSpec>(
Ok(state)
}
/// Create an unsigned genesis `BeaconBlock` whose body matches the genesis state.
/// Create an unsigned genesis `BeaconBlock`.
///
/// For Gloas, the block's `signed_execution_payload_bid` is populated from the state's
/// `latest_execution_payload_bid` so that the body root is consistent with
/// `state.latest_block_header.body_root`.
/// Per spec, the genesis block body is empty (all default fields) except for Gloas,
/// where `body.signed_execution_payload_bid.message` is initialised from
/// `state.latest_execution_payload_bid` so that the first post-genesis proposer can
/// build on the correct execution layer head.
///
/// The returned block has `state_root == Hash256::ZERO`; callers that need the real
/// state root should set it themselves.
/// `state.latest_block_header.body_root` is set from this same block's body, so the
/// two must stay in sync.
pub fn genesis_block<E: EthSpec>(
genesis_state: &BeaconState<E>,
state: &BeaconState<E>,
spec: &ChainSpec,
) -> Result<BeaconBlock<E>, BeaconStateError> {
let mut block = BeaconBlock::empty(spec);
if let Ok(block) = block.as_gloas_mut() {
let state_bid = genesis_state.latest_execution_payload_bid()?;
let bid = &mut block.body.signed_execution_payload_bid.message;
bid.block_hash = state_bid.block_hash;
bid.execution_requests_root = state_bid.execution_requests_root;
if let BeaconBlock::Gloas(ref mut gloas_block) = block {
let bid = state.latest_execution_payload_bid()?.clone();
gloas_block.body.signed_execution_payload_bid.message = bid;
}
Ok(block)
}

View File

@@ -555,13 +555,10 @@ pub fn process_parent_execution_payload<E: EthSpec, Payload: AbstractExecPayload
.signed_execution_payload_bid()?
.message
.parent_block_hash;
let parent_bid = state.latest_execution_payload_bid()?.clone();
let parent_bid = state.latest_execution_payload_bid()?;
let requests = block.body().parent_execution_requests()?;
let is_genesis_block = parent_bid.block_hash == ExecutionBlockHash::zero();
let is_parent_block_empty = bid_parent_block_hash != parent_bid.block_hash;
if is_genesis_block || is_parent_block_empty {
if bid_parent_block_hash != parent_bid.block_hash {
// Parent was EMPTY -- no execution requests expected
block_verify!(
*requests == ExecutionRequests::default(),
@@ -580,7 +577,7 @@ pub fn process_parent_execution_payload<E: EthSpec, Payload: AbstractExecPayload
}
);
apply_parent_execution_payload(state, &parent_bid, requests, spec)
apply_parent_execution_payload(state, requests, spec)
}
/// Apply the parent execution payload's deferred effects to the state.
@@ -591,10 +588,10 @@ pub fn process_parent_execution_payload<E: EthSpec, Payload: AbstractExecPayload
/// 3. Updates `execution_payload_availability` and `latest_block_hash`
pub fn apply_parent_execution_payload<E: EthSpec>(
state: &mut BeaconState<E>,
parent_bid: &ExecutionPayloadBid<E>,
requests: &ExecutionRequests<E>,
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
let parent_bid = state.latest_execution_payload_bid()?.clone();
let parent_slot = parent_bid.slot;
let parent_epoch = parent_slot.epoch(E::slots_per_epoch());

View File

@@ -9,8 +9,8 @@ use safe_arith::{SafeArith, SafeArithIter};
use tree_hash::TreeHash;
use types::{
AbstractExecPayload, BeaconState, BeaconStateError, ChainSpec, EthSpec, ExecPayload,
ExecutionBlockHash, ExpectedWithdrawals, ExpectedWithdrawalsCapella,
ExpectedWithdrawalsElectra, ExpectedWithdrawalsGloas, Validator, Withdrawal, Withdrawals,
ExpectedWithdrawals, ExpectedWithdrawalsCapella, ExpectedWithdrawalsElectra,
ExpectedWithdrawalsGloas, Validator, Withdrawal, Withdrawals,
};
/// Compute the next batch of withdrawals which should be included in a block.
@@ -495,10 +495,7 @@ pub mod gloas {
spec: &ChainSpec,
) -> Result<(), BlockProcessingError> {
// Return early if the parent block is empty.
let is_genesis_block = *state.latest_block_hash()? == ExecutionBlockHash::default();
let is_parent_block_empty =
*state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash;
if is_genesis_block || is_parent_block_empty {
if *state.latest_block_hash()? != state.latest_execution_payload_bid()?.block_hash {
return Ok(());
}

View File

@@ -962,7 +962,11 @@ fn compute_exit_epoch_and_update_churn(
spec.compute_activation_exit_epoch(state_ctxt.current_epoch)?,
);
let per_epoch_churn = get_activation_exit_churn_limit(state_ctxt, spec)?;
let per_epoch_churn = if state_ctxt.fork_name.gloas_enabled() {
get_balance_churn_limit(state_ctxt, spec)?
} else {
get_activation_exit_churn_limit(state_ctxt, spec)?
};
// New epoch for exits
let mut exit_balance_to_consume = if *earliest_exit_epoch_state < earliest_exit_epoch {
per_epoch_churn
@@ -991,17 +995,27 @@ fn get_activation_exit_churn_limit(
state_ctxt: &StateContext,
spec: &ChainSpec,
) -> Result<u64, Error> {
let max_limit = if state_ctxt.fork_name.gloas_enabled() {
spec.max_per_epoch_activation_churn_limit_gloas
} else {
spec.max_per_epoch_activation_exit_churn_limit
};
Ok(std::cmp::min(
spec.max_per_epoch_activation_exit_churn_limit,
max_limit,
get_balance_churn_limit(state_ctxt, spec)?,
))
}
fn get_balance_churn_limit(state_ctxt: &StateContext, spec: &ChainSpec) -> Result<u64, Error> {
let total_active_balance = state_ctxt.total_active_balance;
let quotient = if state_ctxt.fork_name.gloas_enabled() {
spec.churn_limit_quotient_gloas
} else {
spec.churn_limit_quotient
};
let churn = std::cmp::max(
spec.min_per_epoch_churn_limit_electra,
total_active_balance.safe_div(spec.churn_limit_quotient)?,
total_active_balance.safe_div(quotient)?,
);
Ok(churn.safe_sub(churn.safe_rem(spec.effective_balance_increment)?)?)

View File

@@ -105,12 +105,8 @@ CONTRIBUTION_DUE_BPS_GLOAS: 5000
PAYLOAD_ATTESTATION_DUE_BPS: 7500
# Heze
# 7500 basis points, 75% of SLOT_DURATION_MS
VIEW_FREEZE_CUTOFF_BPS: 7500
# 6667 basis points, ~67% of SLOT_DURATION_MS
INCLUSION_LIST_SUBMISSION_DUE_BPS: 6667
# 9167 basis points, ~92% of SLOT_DURATION_MS
PROPOSER_INCLUSION_LIST_CUTOFF_BPS: 9167
INCLUSION_LIST_DUE_BPS: 6667
# Validator cycle
# ---------------------------------------------------------------
@@ -135,6 +131,14 @@ MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000
# 2**8 * 10**9 (= 256,000,000,000) Gwei
MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000
# Gloas
# 2**15 (= 32,768)
CHURN_LIMIT_QUOTIENT_GLOAS: 32768
# 2**16 (= 65,536)
CONSOLIDATION_CHURN_LIMIT_QUOTIENT: 65536
# 2**8 * 10**9 (= 256,000,000,000) Gwei
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: 256000000000
# Fork choice
# ---------------------------------------------------------------
# 40%

View File

@@ -101,12 +101,8 @@ CONTRIBUTION_DUE_BPS_GLOAS: 5000
PAYLOAD_ATTESTATION_DUE_BPS: 7500
# Heze
# 7500 basis points, 75% of SLOT_DURATION_MS
VIEW_FREEZE_CUTOFF_BPS: 7500
# 6667 basis points, ~67% of SLOT_DURATION_MS
INCLUSION_LIST_SUBMISSION_DUE_BPS: 6667
# 9167 basis points, ~92% of SLOT_DURATION_MS
PROPOSER_INCLUSION_LIST_CUTOFF_BPS: 9167
INCLUSION_LIST_DUE_BPS: 6667
# Validator cycle
# ---------------------------------------------------------------
@@ -131,6 +127,14 @@ MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 64000000000
# [customized] 2**7 * 10**9 (= 128,000,000,000) Gwei
MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 128000000000
# Gloas
# [customized] 2**4 (= 16)
CHURN_LIMIT_QUOTIENT_GLOAS: 16
# [customized] 2**5 (= 32)
CONSOLIDATION_CHURN_LIMIT_QUOTIENT: 32
# [customized] 2**7 * 10**9 (= 128,000,000,000) Gwei
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: 128000000000
# Fork choice
# ---------------------------------------------------------------
# 40%

View File

@@ -2,8 +2,8 @@
# Misc
# ---------------------------------------------------------------
# [customized] 2**1 (= 2) validators
PTC_SIZE: 2
# [customized] 2**4 (= 16) validators
PTC_SIZE: 16
# Max operations per block
# ---------------------------------------------------------------

View File

@@ -394,15 +394,13 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> SignedBeaconBlock<E, Payload>
/// `block_hash` from the parent beacon block's bid. If the parent beacon state is available
/// this can alternatively be fetched from `state.latest_payload_bid`.
///
/// This function returns `false` for all blocks prior to Gloas and for the zero
/// `parent_block_hash`.
/// This function returns `false` for all blocks prior to Gloas.
pub fn is_parent_block_full(&self, parent_block_hash: ExecutionBlockHash) -> bool {
let Ok(signed_payload_bid) = self.message().body().signed_execution_payload_bid() else {
// Prior to Gloas.
return false;
};
parent_block_hash != ExecutionBlockHash::zero()
&& signed_payload_bid.message.parent_block_hash == parent_block_hash
signed_payload_bid.message.parent_block_hash == parent_block_hash
}
}

View File

@@ -1,5 +1,5 @@
use crate::test_utils::TestRandom;
use crate::{Address, ForkName, SignedRoot, Slot};
use crate::{Address, ForkName, Hash256, SignedRoot, Slot};
use bls::Signature;
use context_deserialize::context_deserialize;
use educe::Educe;
@@ -16,6 +16,7 @@ use tree_hash_derive::TreeHash;
#[context_deserialize(ForkName)]
// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#new-proposerpreferences
pub struct ProposerPreferences {
pub checkpoint_root: Hash256,
pub proposal_slot: Slot,
pub validator_index: u64,
pub fee_recipient: Address,

View File

@@ -251,6 +251,9 @@ pub struct ChainSpec {
pub builder_payment_threshold_numerator: u64,
pub builder_payment_threshold_denominator: u64,
pub min_builder_withdrawability_delay: Epoch,
pub churn_limit_quotient_gloas: u64,
pub consolidation_churn_limit_quotient: u64,
pub max_per_epoch_activation_churn_limit_gloas: u64,
/*
* Networking
@@ -1268,6 +1271,14 @@ impl ChainSpec {
builder_payment_threshold_numerator: 6,
builder_payment_threshold_denominator: 10,
min_builder_withdrawability_delay: Epoch::new(64),
churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15))
.expect("calculation does not overflow"),
consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16))
.expect("calculation does not overflow"),
max_per_epoch_activation_churn_limit_gloas: option_wrapper(|| {
u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
max_request_payloads: 128,
/*
@@ -1414,6 +1425,14 @@ impl ChainSpec {
gloas_fork_version: [0x07, 0x00, 0x00, 0x01],
gloas_fork_epoch: None,
min_builder_withdrawability_delay: Epoch::new(2),
churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 4))
.expect("calculation does not overflow"),
consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 5))
.expect("calculation does not overflow"),
max_per_epoch_activation_churn_limit_gloas: option_wrapper(|| {
u64::checked_pow(2, 7)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
/*
* Derived time values (set by `compute_derived_values()`)
@@ -1675,6 +1694,14 @@ impl ChainSpec {
builder_payment_threshold_numerator: 6,
builder_payment_threshold_denominator: 10,
min_builder_withdrawability_delay: Epoch::new(64),
churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15))
.expect("calculation does not overflow"),
consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16))
.expect("calculation does not overflow"),
max_per_epoch_activation_churn_limit_gloas: option_wrapper(|| {
u64::checked_pow(2, 8)?.checked_mul(u64::checked_pow(10, 9)?)
})
.expect("calculation does not overflow"),
max_request_payloads: 128,
/*
@@ -2125,6 +2152,16 @@ pub struct Config {
#[serde(default = "default_min_builder_withdrawability_delay")]
#[serde(with = "serde_utils::quoted_u64")]
min_builder_withdrawability_delay: u64,
#[serde(default = "default_churn_limit_quotient_gloas")]
#[serde(with = "serde_utils::quoted_u64")]
churn_limit_quotient_gloas: u64,
#[serde(default = "default_consolidation_churn_limit_quotient")]
#[serde(with = "serde_utils::quoted_u64")]
consolidation_churn_limit_quotient: u64,
#[serde(default = "default_max_per_epoch_activation_churn_limit_gloas")]
#[serde(with = "serde_utils::quoted_u64")]
max_per_epoch_activation_churn_limit_gloas: u64,
}
fn default_bellatrix_fork_version() -> [u8; 4] {
@@ -2362,6 +2399,18 @@ const fn default_min_builder_withdrawability_delay() -> u64 {
64
}
const fn default_churn_limit_quotient_gloas() -> u64 {
32_768
}
const fn default_consolidation_churn_limit_quotient() -> u64 {
65_536
}
const fn default_max_per_epoch_activation_churn_limit_gloas() -> u64 {
256_000_000_000
}
fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize {
let max_request_blocks = max_request_blocks as usize;
RuntimeVariableList::<Hash256>::new(
@@ -2613,6 +2662,11 @@ impl Config {
contribution_due_bps: spec.contribution_due_bps,
min_builder_withdrawability_delay: spec.min_builder_withdrawability_delay.as_u64(),
churn_limit_quotient_gloas: spec.churn_limit_quotient_gloas,
consolidation_churn_limit_quotient: spec.consolidation_churn_limit_quotient,
max_per_epoch_activation_churn_limit_gloas: spec
.max_per_epoch_activation_churn_limit_gloas,
}
}
@@ -2710,6 +2764,9 @@ impl Config {
sync_message_due_bps,
contribution_due_bps,
min_builder_withdrawability_delay,
churn_limit_quotient_gloas,
consolidation_churn_limit_quotient,
max_per_epoch_activation_churn_limit_gloas,
} = self;
if preset_base != E::spec_name().to_string().as_str() {
@@ -2817,6 +2874,10 @@ impl Config {
min_builder_withdrawability_delay: Epoch::new(min_builder_withdrawability_delay),
churn_limit_quotient_gloas,
consolidation_churn_limit_quotient,
max_per_epoch_activation_churn_limit_gloas,
..chain_spec.clone()
};
Some(spec.compute_derived_values::<E>())
@@ -3719,9 +3780,7 @@ mod yaml_tests {
"CONTRIBUTION_DUE_BPS_GLOAS",
"MAX_REQUEST_PAYLOADS",
// Heze networking
"VIEW_FREEZE_CUTOFF_BPS",
"INCLUSION_LIST_SUBMISSION_DUE_BPS",
"PROPOSER_INCLUSION_LIST_CUTOFF_BPS",
"INCLUSION_LIST_DUE_BPS",
"MAX_REQUEST_INCLUSION_LIST",
"MAX_BYTES_PER_INCLUSION_LIST",
];

View File

@@ -572,7 +572,7 @@ impl EthSpec for MinimalEthSpec {
type NumberOfColumns = U128;
type ProposerLookaheadSlots = U16; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH
type BuilderPendingPaymentsLimit = U16; // 2 * SLOTS_PER_EPOCH = 2 * 8 = 16
type PTCSize = U2;
type PTCSize = U16;
type PtcWindowLength = U24; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH
type MaxBuildersPerWithdrawalsSweep = U16;

View File

@@ -20,6 +20,7 @@ pub struct ExecutionPayloadEnvelope<E: EthSpec> {
#[serde(with = "serde_utils::quoted_u64")]
pub builder_index: u64,
pub beacon_block_root: Hash256,
pub parent_beacon_block_root: Hash256,
}
impl<E: EthSpec> ExecutionPayloadEnvelope<E> {
@@ -30,6 +31,7 @@ impl<E: EthSpec> ExecutionPayloadEnvelope<E> {
execution_requests: ExecutionRequests::default(),
builder_index: 0,
beacon_block_root: Hash256::zero(),
parent_beacon_block_root: Hash256::zero(),
}
}

View File

@@ -2762,29 +2762,55 @@ impl<E: EthSpec> BeaconState<E> {
/// Return the churn limit for the current epoch.
pub fn get_balance_churn_limit(&self, spec: &ChainSpec) -> Result<u64, BeaconStateError> {
let total_active_balance = self.get_total_active_balance()?;
let quotient = if self.fork_name_unchecked().gloas_enabled() {
spec.churn_limit_quotient_gloas
} else {
spec.churn_limit_quotient
};
let churn = std::cmp::max(
spec.min_per_epoch_churn_limit_electra,
total_active_balance.safe_div(spec.churn_limit_quotient)?,
total_active_balance.safe_div(quotient)?,
);
Ok(churn.safe_sub(churn.safe_rem(spec.effective_balance_increment)?)?)
}
/// Return the churn limit for the current epoch dedicated to activations and exits.
///
/// From Gloas onwards this is the activation-only churn limit (EIP-8061); exits use
/// [`Self::get_exit_churn_limit`].
pub fn get_activation_exit_churn_limit(
&self,
spec: &ChainSpec,
) -> Result<u64, BeaconStateError> {
let max_limit = if self.fork_name_unchecked().gloas_enabled() {
spec.max_per_epoch_activation_churn_limit_gloas
} else {
spec.max_per_epoch_activation_exit_churn_limit
};
Ok(std::cmp::min(
spec.max_per_epoch_activation_exit_churn_limit,
max_limit,
self.get_balance_churn_limit(spec)?,
))
}
/// Return the Gloas (EIP-8061) exit churn limit for the current epoch.
///
/// Unlike [`Self::get_activation_exit_churn_limit`], this is uncapped.
pub fn get_exit_churn_limit(&self, spec: &ChainSpec) -> Result<u64, BeaconStateError> {
self.get_balance_churn_limit(spec)
}
pub fn get_consolidation_churn_limit(&self, spec: &ChainSpec) -> Result<u64, BeaconStateError> {
self.get_balance_churn_limit(spec)?
.safe_sub(self.get_activation_exit_churn_limit(spec)?)
.map_err(Into::into)
if self.fork_name_unchecked().gloas_enabled() {
let total_active_balance = self.get_total_active_balance()?;
let churn = total_active_balance.safe_div(spec.consolidation_churn_limit_quotient)?;
Ok(churn.safe_sub(churn.safe_rem(spec.effective_balance_increment)?)?)
} else {
self.get_balance_churn_limit(spec)?
.safe_sub(self.get_activation_exit_churn_limit(spec)?)
.map_err(Into::into)
}
}
pub fn get_pending_balance_to_withdraw(
@@ -2879,7 +2905,11 @@ impl<E: EthSpec> BeaconState<E> {
self.compute_activation_exit_epoch(self.current_epoch(), spec)?,
);
let per_epoch_churn = self.get_activation_exit_churn_limit(spec)?;
let per_epoch_churn = if self.fork_name_unchecked().gloas_enabled() {
self.get_exit_churn_limit(spec)?
} else {
self.get_activation_exit_churn_limit(spec)?
};
// New epoch for exits
let mut exit_balance_to_consume = if self.earliest_exit_epoch()? < earliest_exit_epoch {
per_epoch_churn
@@ -3103,7 +3133,19 @@ impl<E: EthSpec> BeaconState<E> {
let total_active_balance = self.get_total_active_balance()?;
let fork_name = self.fork_name_unchecked();
if fork_name.electra_enabled() {
if fork_name.gloas_enabled() {
// [Modified in Gloas:EIP8061]
let exit_churn = self.get_exit_churn_limit(spec)?;
let activation_churn = self.get_activation_exit_churn_limit(spec)?;
let consolidation_churn = self.get_consolidation_churn_limit(spec)?;
compute_weak_subjectivity_period_gloas(
total_active_balance,
exit_churn,
activation_churn,
consolidation_churn,
spec,
)
} else if fork_name.electra_enabled() {
let balance_churn_limit = self.get_balance_churn_limit(spec)?;
compute_weak_subjectivity_period_electra(
total_active_balance,
@@ -3601,6 +3643,30 @@ pub fn compute_weak_subjectivity_period_electra(
Ok(ws_period)
}
/// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/weak-subjectivity.md
pub fn compute_weak_subjectivity_period_gloas(
total_active_balance: u64,
exit_churn_limit: u64,
activation_churn_limit: u64,
consolidation_churn_limit: u64,
spec: &ChainSpec,
) -> Result<Epoch, BeaconStateError> {
// delta = 2 * exit_churn // 3 + activation_churn // 3 + consolidation_churn
let delta = exit_churn_limit
.safe_mul(2)?
.safe_div(3)?
.safe_add(activation_churn_limit.safe_div(3)?)?
.safe_add(consolidation_churn_limit)?;
let epochs_for_validator_set_churn = SAFETY_DECAY
.safe_mul(total_active_balance)?
.safe_div(delta.safe_mul(200)?)?;
let ws_period = spec
.min_validator_withdrawability_delay
.safe_add(epochs_for_validator_set_churn)?;
Ok(ws_period)
}
#[cfg(test)]
mod weak_subjectivity_tests {
use crate::state::beacon_state::compute_weak_subjectivity_period_electra;

View File

@@ -1,6 +1,6 @@
# To download/extract nightly tests, run:
# CONSENSUS_SPECS_TEST_VERSION=nightly make
CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.5
CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.6
REPO_NAME := consensus-spec-tests
OUTPUT_DIR := ./$(REPO_NAME)

View File

@@ -55,6 +55,7 @@ excluded_paths = [
"tests/.*/.*/ssz_static/PartialDataColumn.*/.*",
# TODO(gloas): Ignore Gloas light client stuff for now
"tests/.*/gloas/ssz_static/LightClient.*/.*",
"tests/.*/gloas/light_client",
# Execution payload header is irrelevant after Gloas, this type will probably be deleted.
"tests/.*/gloas/ssz_static/ExecutionPayloadHeader/.*",
# ForkChoiceNode is internal to fork choice and probably doesn't need SSZ tests.

View File

@@ -23,7 +23,7 @@ if [[ "$version" == "nightly" || "$version" =~ ^nightly-[0-9]+$ ]]; then
if [[ "$version" == "nightly" ]]; then
run_id=$(curl --fail -s -H "${auth_header}" \
"${api}/repos/${repo}/actions/workflows/nightly-reftests.yml/runs?branch=master&status=success&per_page=1" |
"${api}/repos/${repo}/actions/workflows/tests.yml/runs?branch=master&status=success&per_page=1" |
jq -r '.workflow_runs[0].id')
else
run_id="${version#nightly-}"

View File

@@ -58,6 +58,8 @@ pub struct Eth1DataReset;
#[derive(Debug)]
pub struct PendingBalanceDeposits;
#[derive(Debug)]
pub struct PendingDepositsChurn;
#[derive(Debug)]
pub struct PendingConsolidations;
#[derive(Debug)]
pub struct EffectiveBalanceUpdates;
@@ -93,6 +95,7 @@ type_name!(RegistryUpdates, "registry_updates");
type_name!(Slashings, "slashings");
type_name!(Eth1DataReset, "eth1_data_reset");
type_name!(PendingBalanceDeposits, "pending_deposits");
type_name!(PendingDepositsChurn, "pending_deposits_churn");
type_name!(PendingConsolidations, "pending_consolidations");
type_name!(EffectiveBalanceUpdates, "effective_balance_updates");
type_name!(SlashingsReset, "slashings_reset");
@@ -191,6 +194,20 @@ impl<E: EthSpec> EpochTransition<E> for PendingBalanceDeposits {
}
}
impl<E: EthSpec> EpochTransition<E> for PendingDepositsChurn {
fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> {
process_epoch_single_pass(
state,
spec,
SinglePassConfig {
pending_deposits: true,
..SinglePassConfig::disable_all()
},
)
.map(|_| ())
}
}
impl<E: EthSpec> EpochTransition<E> for PendingConsolidations {
fn run(state: &mut BeaconState<E>, spec: &ChainSpec) -> Result<(), EpochProcessingError> {
initialize_epoch_cache(state, spec)?;
@@ -387,7 +404,9 @@ impl<E: EthSpec, T: EpochTransition<E>> Case for EpochProcessing<E, T> {
}
if !fork_name.gloas_enabled()
&& (T::name() == "builder_pending_payments" || T::name() == "ptc_window")
&& (T::name() == "builder_pending_payments"
|| T::name() == "ptc_window"
|| T::name() == "pending_deposits_churn")
{
return false;
}

View File

@@ -53,6 +53,15 @@ pub struct WithdrawalsPayload<E: EthSpec> {
payload: Option<ExecutionPayload<E>>,
}
/// Newtype for testing voluntary exit churn (Gloas+).
///
/// The test case applies the same `process_voluntary_exit` operation as the regular
/// `voluntary_exit` test, but under the `voluntary_exit_churn` handler directory.
#[derive(Debug, Clone)]
pub struct VoluntaryExitChurn {
exit: SignedVoluntaryExit,
}
/// Newtype for testing execution payload bids.
#[derive(Debug, Clone, Deserialize)]
pub struct ExecutionPayloadBidBlock<E: EthSpec> {
@@ -265,6 +274,40 @@ impl<E: EthSpec> Operation<E> for SignedVoluntaryExit {
}
}
impl<E: EthSpec> Operation<E> for VoluntaryExitChurn {
type Error = BlockProcessingError;
fn handler_name() -> String {
"voluntary_exit_churn".into()
}
fn filename() -> String {
"voluntary_exit.ssz_snappy".into()
}
fn is_enabled_for_fork(fork_name: ForkName) -> bool {
fork_name.gloas_enabled()
}
fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result<Self, Error> {
ssz_decode_file(path).map(|exit| VoluntaryExitChurn { exit })
}
fn apply_to(
&self,
state: &mut BeaconState<E>,
spec: &ChainSpec,
_: &Operations<E, Self>,
) -> Result<(), BlockProcessingError> {
process_exits(
state,
std::slice::from_ref(&self.exit),
VerifySignatures::True,
spec,
)
}
}
impl<E: EthSpec> Operation<E> for BeaconBlock<E> {
type Error = BlockProcessingError;

View File

@@ -340,6 +340,10 @@ impl<T, E> SszStaticHandler<T, E> {
pub fn pre_electra() -> Self {
Self::for_forks(ForkName::list_all()[0..5].to_vec())
}
pub fn pre_capella() -> Self {
Self::for_forks(ForkName::list_all()[0..3].to_vec())
}
}
/// Handler for SSZ types that implement `CachedTreeHash`.

View File

@@ -3,9 +3,10 @@ pub use cases::{
BuilderPendingPayments, Case, EffectiveBalanceUpdates, Eth1DataReset, ExecutionPayloadBidBlock,
FeatureName, HistoricalRootsUpdate, HistoricalSummariesUpdate, InactivityUpdates,
JustificationAndFinalization, ParentExecutionPayloadBlock, ParticipationFlagUpdates,
ParticipationRecordUpdates, PendingBalanceDeposits, PendingConsolidations, ProposerLookahead,
PtcWindow, RandaoMixesReset, RegistryUpdates, RewardsAndPenalties, Slashings, SlashingsReset,
SyncCommitteeUpdates, WithdrawalsPayload,
ParticipationRecordUpdates, PendingBalanceDeposits, PendingConsolidations,
PendingDepositsChurn, ProposerLookahead, PtcWindow, RandaoMixesReset, RegistryUpdates,
RewardsAndPenalties, Slashings, SlashingsReset, SyncCommitteeUpdates, VoluntaryExitChurn,
WithdrawalsPayload,
};
pub use decode::log_file_access;
pub use error::Error;

View File

@@ -142,6 +142,12 @@ fn operations_bls_to_execution_change() {
OperationsHandler::<MainnetEthSpec, SignedBlsToExecutionChange>::default().run();
}
#[test]
fn operations_voluntary_exit_churn() {
OperationsHandler::<MinimalEthSpec, VoluntaryExitChurn>::default().run();
OperationsHandler::<MainnetEthSpec, VoluntaryExitChurn>::default().run();
}
#[test]
fn sanity_blocks() {
SanityBlocksHandler::<MinimalEthSpec>::default().run();
@@ -285,8 +291,19 @@ mod ssz_static {
ssz_static_test!(eth1_data, Eth1Data);
ssz_static_test!(fork, Fork);
ssz_static_test!(fork_data, ForkData);
ssz_static_test!(historical_batch, HistoricalBatch<_>);
ssz_static_test!(pending_attestation, PendingAttestation<_>);
// `HistoricalBatch` was removed in Capella, so test vectors only exist for Base,
// Altair and Bellatrix.
#[test]
fn historical_batch() {
SszStaticHandler::<HistoricalBatch<MinimalEthSpec>, MinimalEthSpec>::pre_capella().run();
SszStaticHandler::<HistoricalBatch<MainnetEthSpec>, MainnetEthSpec>::pre_capella().run();
}
// `PendingAttestation` was removed in Altair, so test vectors only exist for Base.
#[test]
fn pending_attestation() {
SszStaticHandler::<PendingAttestation<MinimalEthSpec>, MinimalEthSpec>::base_only().run();
SszStaticHandler::<PendingAttestation<MainnetEthSpec>, MainnetEthSpec>::base_only().run();
}
ssz_static_test!(proposer_slashing, ProposerSlashing);
ssz_static_test!(
signed_beacon_block,
@@ -899,6 +916,12 @@ fn epoch_processing_pending_balance_deposits() {
EpochProcessingHandler::<MainnetEthSpec, PendingBalanceDeposits>::default().run();
}
#[test]
fn epoch_processing_pending_deposits_churn() {
EpochProcessingHandler::<MinimalEthSpec, PendingDepositsChurn>::default().run();
EpochProcessingHandler::<MainnetEthSpec, PendingDepositsChurn>::default().run();
}
#[test]
fn epoch_processing_pending_consolidations() {
EpochProcessingHandler::<MinimalEthSpec, PendingConsolidations>::default().run();