Merge branch 'gloas-walk-always' of https://github.com/sigp/lighthouse into gloas-fork-choice-fixes

This commit is contained in:
Eitan Seri- Levi
2026-03-31 23:53:09 -07:00
45 changed files with 2528 additions and 883 deletions

View File

@@ -138,10 +138,6 @@ pub enum InvalidBlock {
finalized_root: Hash256,
block_ancestor: Option<Hash256>,
},
MissingExecutionPayloadBid {
block_slot: Slot,
block_root: Hash256,
},
}
#[derive(Debug)]
@@ -174,11 +170,11 @@ pub enum InvalidAttestation {
/// The attestation is attesting to a state that is later than itself. (Viz., attesting to the
/// future).
AttestsToFutureBlock { block: Slot, attestation: Slot },
/// Post-GLOAS: attestation index must be 0 or 1.
/// Post-Gloas: attestation index must be 0 or 1.
InvalidAttestationIndex { index: u64 },
/// A same-slot attestation has a non-zero index, which is invalid post-GLOAS.
/// A same-slot attestation has a non-zero index, which is invalid post-Gloas.
InvalidSameSlotAttestationIndex { slot: Slot },
/// Post-GLOAS: attestation with index == 1 (payload_present) requires the block's
/// Post-Gloas: attestation with index == 1 (payload_present) requires the block's
/// payload to have been received (`root in store.payload_states`).
PayloadNotReceived { beacon_block_root: Hash256 },
/// A payload attestation votes payload_present for a block in the current slot, which is
@@ -260,10 +256,19 @@ pub struct QueuedAttestation {
attesting_indices: Vec<u64>,
block_root: Hash256,
target_epoch: Epoch,
/// Per GLOAS spec: `payload_present = attestation.data.index == 1`.
/// Per Gloas spec: `payload_present = attestation.data.index == 1`.
payload_present: bool,
}
/// Legacy queued attestation without payload_present (pre-Gloas, schema V28).
#[derive(Clone, PartialEq, Encode, Decode)]
pub struct QueuedAttestationV28 {
slot: Slot,
attesting_indices: Vec<u64>,
block_root: Hash256,
target_epoch: Epoch,
}
impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
fn from(a: IndexedAttestationRef<'a, E>) -> Self {
Self {
@@ -276,19 +281,6 @@ impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
}
}
/// Used for queuing payload attestations (PTC votes) from the current slot.
/// Payload attestations have different dequeue timing than regular attestations:
/// gossiped payload attestations need an extra slot of delay (slot + 1 < current_slot).
#[derive(Clone, PartialEq, Encode, Decode)]
pub struct QueuedPayloadAttestation {
slot: Slot,
/// Resolved PTC committee positions (not validator indices).
ptc_indices: Vec<usize>,
block_root: Hash256,
payload_present: bool,
blob_data_available: bool,
}
/// Returns all values in `self.queued_attestations` that have a slot that is earlier than the
/// current slot. Also removes those values from `self.queued_attestations`.
fn dequeue_attestations(
@@ -310,22 +302,6 @@ fn dequeue_attestations(
std::mem::replace(queued_attestations, remaining)
}
/// Returns all values in `queued` that have `slot + 1 < current_slot`.
/// Payload attestations need an extra slot of delay compared to regular attestations.
fn dequeue_payload_attestations(
current_slot: Slot,
queued: &mut Vec<QueuedPayloadAttestation>,
) -> Vec<QueuedPayloadAttestation> {
let remaining = queued.split_off(
queued
.iter()
.position(|a| a.slot.saturating_add(1_u64) >= current_slot)
.unwrap_or(queued.len()),
);
std::mem::replace(queued, remaining)
}
/// Denotes whether an attestation we are processing was received from a block or from gossip.
/// Equivalent to the `is_from_block` `bool` in:
///
@@ -370,9 +346,6 @@ pub struct ForkChoice<T, E> {
proto_array: ProtoArrayForkChoice,
/// Attestations that arrived at the current slot and must be queued for later processing.
queued_attestations: Vec<QueuedAttestation>,
/// Payload attestations (PTC votes) that must be queued for later processing.
/// These have different dequeue timing than regular attestations.
queued_payload_attestations: Vec<QueuedPayloadAttestation>,
/// Stores a cache of the values required to be sent to the execution layer.
forkchoice_update_parameters: ForkchoiceUpdateParameters,
_phantom: PhantomData<E>,
@@ -387,7 +360,6 @@ where
self.fc_store == other.fc_store
&& self.proto_array == other.proto_array
&& self.queued_attestations == other.queued_attestations
&& self.queued_payload_attestations == other.queued_payload_attestations
}
}
@@ -423,22 +395,7 @@ where
.map_err(Error::BeaconStateError)?;
let (execution_status, execution_payload_parent_hash, execution_payload_block_hash) =
if let Ok(execution_payload) = anchor_block.message().execution_payload() {
// Pre-Gloas forks: hashes come from the execution payload.
if execution_payload.is_default_with_empty_roots() {
(ExecutionStatus::irrelevant(), None, None)
} else {
// Assume that this payload is valid, since the anchor should be a
// trusted block and state.
(
ExecutionStatus::Valid(execution_payload.block_hash()),
Some(execution_payload.parent_hash()),
Some(execution_payload.block_hash()),
)
}
} else if let Ok(signed_bid) =
anchor_block.message().body().signed_execution_payload_bid()
{
if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() {
// Gloas: execution status is irrelevant post-Gloas; payload validation
// is decoupled from beacon blocks.
(
@@ -446,6 +403,19 @@ where
Some(signed_bid.message.parent_block_hash),
Some(signed_bid.message.block_hash),
)
} else if let Ok(execution_payload) = anchor_block.message().execution_payload() {
// Pre-Gloas forks: do not set payload hashes, they are only used post-Gloas.
if execution_payload.is_default_with_empty_roots() {
(ExecutionStatus::irrelevant(), None, None)
} else {
// Assume that this payload is valid, since the anchor should be a
// trusted block and state.
(
ExecutionStatus::Valid(execution_payload.block_hash()),
None,
None,
)
}
} else {
// Pre-merge: no execution payload at all.
(ExecutionStatus::irrelevant(), None, None)
@@ -465,6 +435,7 @@ where
execution_status,
execution_payload_parent_hash,
execution_payload_block_hash,
anchor_block.message().proposer_index(),
spec,
)?;
@@ -472,13 +443,12 @@ where
fc_store,
proto_array,
queued_attestations: vec![],
queued_payload_attestations: vec![],
// This will be updated during the next call to `Self::get_head`.
forkchoice_update_parameters: ForkchoiceUpdateParameters {
head_hash: None,
justified_hash: None,
finalized_hash: None,
// These will be updated during the next call to `Self::get_head`.
// This will be updated during the next call to `Self::get_head`.
head_root: Hash256::zero(),
},
_phantom: PhantomData,
@@ -966,14 +936,6 @@ where
Some(signed_bid.message.block_hash),
)
} else {
if spec.fork_name_at_slot::<E>(block.slot()).gloas_enabled() {
return Err(Error::InvalidBlock(
InvalidBlock::MissingExecutionPayloadBid {
block_slot: block.slot(),
block_root,
},
));
}
(None, None)
};
@@ -1163,7 +1125,7 @@ where
{
let index = indexed_attestation.data().index;
// Post-GLOAS: attestation index must be 0 or 1.
// Post-Gloas: attestation index must be 0 or 1.
if index > 1 {
return Err(InvalidAttestation::InvalidAttestationIndex { index });
}
@@ -1176,6 +1138,7 @@ where
}
// index == 1 (payload_present) requires the block's payload to have been received.
// TODO(gloas): could optimise by adding `payload_received` to `Block`
if index == 1
&& !self
.proto_array
@@ -1334,10 +1297,13 @@ where
) -> Result<(), Error<T::Error>> {
self.update_time(system_time_current_slot)?;
if attestation.data.beacon_block_root == Hash256::zero() {
if attestation.data.beacon_block_root.is_zero() {
return Ok(());
}
// TODO(gloas): Should ignore wrong-slot payload attestations at the caller, they could
// have been processed at the correct slot when received on gossip, but then have the
// wrong-slot by the time they make it to here (TOCTOU).
self.validate_on_payload_attestation(attestation, is_from_block)?;
// Resolve validator indices to PTC committee positions.
@@ -1346,34 +1312,13 @@ where
.filter_map(|vi| ptc.iter().position(|&p| p == *vi as usize))
.collect();
let processing_slot = self.fc_store.get_current_slot();
// Payload attestations from blocks can be applied in the next slot (S+1 for data.slot=S),
// while gossiped payload attestations are delayed one extra slot.
let should_process_now = match is_from_block {
AttestationFromBlock::True => attestation.data.slot < processing_slot,
AttestationFromBlock::False => {
attestation.data.slot.saturating_add(1_u64) < processing_slot
}
};
if should_process_now {
for &ptc_index in &ptc_indices {
self.proto_array.process_payload_attestation(
attestation.data.beacon_block_root,
ptc_index,
attestation.data.payload_present,
attestation.data.blob_data_available,
)?;
}
} else {
self.queued_payload_attestations
.push(QueuedPayloadAttestation {
slot: attestation.data.slot,
ptc_indices,
block_root: attestation.data.beacon_block_root,
payload_present: attestation.data.payload_present,
blob_data_available: attestation.data.blob_data_available,
});
for &ptc_index in &ptc_indices {
self.proto_array.process_payload_attestation(
attestation.data.beacon_block_root,
ptc_index,
attestation.data.payload_present,
attestation.data.blob_data_available,
)?;
}
Ok(())
@@ -1408,7 +1353,6 @@ where
// Process any attestations that might now be eligible.
self.process_attestation_queue()?;
self.process_payload_attestation_queue()?;
Ok(self.fc_store.get_current_slot())
}
@@ -1495,26 +1439,6 @@ where
Ok(())
}
/// Processes and removes from the queue any queued payload attestations which may now be
/// eligible for processing. Payload attestations use `slot + 1 < current_slot` timing.
fn process_payload_attestation_queue(&mut self) -> Result<(), Error<T::Error>> {
let current_slot = self.fc_store.get_current_slot();
for attestation in
dequeue_payload_attestations(current_slot, &mut self.queued_payload_attestations)
{
for &ptc_index in &attestation.ptc_indices {
self.proto_array.process_payload_attestation(
attestation.block_root,
ptc_index,
attestation.payload_present,
attestation.blob_data_available,
)?;
}
}
Ok(())
}
/// Returns `true` if the block is known **and** a descendant of the finalized root.
pub fn contains_block(&self, block_root: &Hash256) -> bool {
self.proto_array.contains_block(block_root)
@@ -1670,11 +1594,6 @@ where
&self.queued_attestations
}
/// Returns a reference to the currently queued payload attestations.
pub fn queued_payload_attestations(&self) -> &[QueuedPayloadAttestation] {
&self.queued_payload_attestations
}
/// Returns the store's `proposer_boost_root`.
pub fn proposer_boost_root(&self) -> Hash256 {
self.fc_store.proposer_boost_root()
@@ -1758,8 +1677,7 @@ where
let mut fork_choice = Self {
fc_store,
proto_array,
queued_attestations: persisted.queued_attestations,
queued_payload_attestations: persisted.queued_payload_attestations,
queued_attestations: vec![],
// Will be updated in the following call to `Self::get_head`.
forkchoice_update_parameters: ForkchoiceUpdateParameters {
head_hash: None,
@@ -1799,8 +1717,6 @@ where
pub fn to_persisted(&self) -> PersistedForkChoice {
PersistedForkChoice {
proto_array: self.proto_array().as_ssz_container(),
queued_attestations: self.queued_attestations().to_vec(),
queued_payload_attestations: self.queued_payload_attestations.clone(),
}
}
@@ -1823,9 +1739,8 @@ pub struct PersistedForkChoice {
pub proto_array_v28: proto_array::core::SszContainerV28,
#[superstruct(only(V29))]
pub proto_array: proto_array::core::SszContainerV29,
pub queued_attestations: Vec<QueuedAttestation>,
#[superstruct(only(V29))]
pub queued_payload_attestations: Vec<QueuedPayloadAttestation>,
#[superstruct(only(V28))]
pub queued_attestations_v28: Vec<QueuedAttestationV28>,
}
pub type PersistedForkChoice = PersistedForkChoiceV29;
@@ -1834,8 +1749,15 @@ impl From<PersistedForkChoiceV28> for PersistedForkChoiceV29 {
fn from(v28: PersistedForkChoiceV28) -> Self {
Self {
proto_array: v28.proto_array_v28.into(),
queued_attestations: v28.queued_attestations,
queued_payload_attestations: vec![],
}
}
}
impl From<PersistedForkChoiceV29> for PersistedForkChoiceV28 {
fn from(v29: PersistedForkChoiceV29) -> Self {
Self {
proto_array_v28: v29.proto_array.into(),
queued_attestations_v28: vec![],
}
}
}

View File

@@ -5,8 +5,7 @@ mod metrics;
pub use crate::fork_choice::{
AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters,
InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice,
PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, QueuedPayloadAttestation,
ResetPayloadStatuses,
PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, ResetPayloadStatuses,
};
pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::{

View File

@@ -73,9 +73,9 @@ impl ForkChoiceTest {
Self { harness }
}
/// Creates a new tester with the GLOAS fork active at epoch 1.
/// Creates a new tester with the Gloas fork active at epoch 1.
/// Genesis is a standard Fulu block (epoch 0), so block production works normally.
/// Tests that need GLOAS semantics should advance the chain into epoch 1 first.
/// Tests that need Gloas semantics should advance the chain into epoch 1 first.
/// Get a value from the `ForkChoice` instantiation.
fn get<T, U>(&self, func: T) -> U
where

View File

@@ -144,6 +144,7 @@ impl ForkChoiceTestDefinition {
ExecutionStatus::Optimistic(ExecutionBlockHash::zero()),
self.execution_payload_parent_hash,
self.execution_payload_block_hash,
0,
&spec,
)
.expect("should create fork choice struct");

View File

@@ -52,7 +52,7 @@ pub fn get_gloas_chain_following_test_definition() -> ForkChoiceTestDefinition {
});
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the GLOAS fork choice tree.
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
block_root: get_root(1),
});
@@ -262,7 +262,7 @@ pub fn get_gloas_find_head_vote_transition_test_definition() -> ForkChoiceTestDe
});
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the GLOAS fork choice tree.
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
block_root: get_root(1),
});
@@ -367,7 +367,7 @@ pub fn get_gloas_weight_priority_over_payload_preference_test_definition()
});
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the GLOAS fork choice tree.
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
block_root: get_root(1),
});
@@ -537,7 +537,7 @@ pub fn get_gloas_interleaved_attestations_test_definition() -> ForkChoiceTestDef
});
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the GLOAS fork choice tree.
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
block_root: get_root(1),
});
@@ -718,6 +718,137 @@ pub fn get_gloas_payload_received_interleaving_test_definition() -> ForkChoiceTe
mod tests {
use super::*;
fn gloas_fork_boundary_spec() -> ChainSpec {
let mut spec = MainnetEthSpec::default_spec();
spec.proposer_score_boost = Some(50);
spec.gloas_fork_epoch = Some(Epoch::new(1));
spec
}
/// Gloas fork boundary: a chain starting pre-Gloas (V17 nodes) that crosses into
/// Gloas (V29 nodes). The head should advance through the fork boundary.
///
/// Parameters:
/// - `skip_first_gloas_slot`: if true, there is no block at the first Gloas slot (slot 32);
/// the first V29 block appears at slot 33.
/// - `first_gloas_block_full`: if true, the first V29 block extends the parent V17 node's
/// EL chain (Full parent payload status). If false, it doesn't (Empty).
fn get_gloas_fork_boundary_test_definition(
skip_first_gloas_slot: bool,
first_gloas_block_full: bool,
) -> ForkChoiceTestDefinition {
let mut ops = vec![];
// Block at slot 31 — last pre-Gloas slot. Created as a V17 node because
// gloas_fork_epoch = 1 → Gloas starts at slot 32.
//
// The test harness sets execution_status = Optimistic(ExecutionBlockHash::from_root(root)),
// so this V17 node's EL block hash = ExecutionBlockHash::from_root(get_root(1)).
ops.push(Operation::ProcessBlock {
slot: Slot::new(31),
root: get_root(1),
parent_root: get_root(0),
justified_checkpoint: get_checkpoint(0),
finalized_checkpoint: get_checkpoint(0),
execution_payload_parent_hash: None,
execution_payload_block_hash: None,
});
// First Gloas block (V29 node).
let gloas_slot = if skip_first_gloas_slot { 33 } else { 32 };
// The first Gloas block should always have the pre-Gloas block as its execution parent,
// although this is currently not checked anywhere (the spec doesn't mention this).
ops.push(Operation::ProcessBlock {
slot: Slot::new(gloas_slot),
root: get_root(2),
parent_root: get_root(1),
justified_checkpoint: get_checkpoint(0),
finalized_checkpoint: get_checkpoint(0),
execution_payload_parent_hash: Some(get_hash(1)),
execution_payload_block_hash: Some(get_hash(2)),
});
// Parent payload status of fork boundary block should always be Empty.
let expected_parent_status = PayloadStatus::Empty;
ops.push(Operation::AssertParentPayloadStatus {
block_root: get_root(2),
expected_status: expected_parent_status,
});
// Mark root 2's execution payload as received so the Full virtual child exists.
if first_gloas_block_full {
ops.push(Operation::ProcessExecutionPayload {
block_root: get_root(2),
});
}
// Extend the chain with another V29 block (Full child of root 2).
ops.push(Operation::ProcessBlock {
slot: Slot::new(gloas_slot + 1),
root: get_root(3),
parent_root: get_root(2),
justified_checkpoint: get_checkpoint(0),
finalized_checkpoint: get_checkpoint(0),
execution_payload_parent_hash: if first_gloas_block_full {
Some(get_hash(2))
} else {
Some(get_hash(1))
},
execution_payload_block_hash: Some(get_hash(3)),
});
// Head should advance to the tip of the chain through the fork boundary.
ops.push(Operation::FindHead {
justified_checkpoint: get_checkpoint(0),
finalized_checkpoint: get_checkpoint(0),
justified_state_balances: vec![1],
expected_head: get_root(3),
current_slot: Slot::new(gloas_slot + 1),
expected_payload_status: None,
});
ops.push(Operation::AssertParentPayloadStatus {
block_root: get_root(3),
expected_status: if first_gloas_block_full {
PayloadStatus::Full
} else {
PayloadStatus::Empty
},
});
ForkChoiceTestDefinition {
finalized_block_slot: Slot::new(0),
justified_checkpoint: get_checkpoint(0),
finalized_checkpoint: get_checkpoint(0),
operations: ops,
// Genesis is V17 (slot 0 < Gloas fork slot 32), these are unused for V17.
execution_payload_parent_hash: None,
execution_payload_block_hash: None,
spec: Some(gloas_fork_boundary_spec()),
}
}
#[test]
fn fork_boundary_no_skip_full() {
get_gloas_fork_boundary_test_definition(false, true).run();
}
#[test]
fn fork_boundary_no_skip_empty() {
get_gloas_fork_boundary_test_definition(false, false).run();
}
#[test]
fn fork_boundary_skip_first_gloas_slot_full() {
get_gloas_fork_boundary_test_definition(true, true).run();
}
#[test]
fn fork_boundary_skip_first_gloas_slot_empty() {
get_gloas_fork_boundary_test_definition(true, false).run();
}
#[test]
fn chain_following() {
let test = get_gloas_chain_following_test_definition();

View File

@@ -117,10 +117,10 @@ pub struct ProtoNode {
pub finalized_checkpoint: Checkpoint,
#[superstruct(getter(copy))]
pub weight: u64,
#[superstruct(getter(copy))]
#[superstruct(only(V17), partial_getter(copy))]
#[ssz(with = "four_byte_option_usize")]
pub best_child: Option<usize>,
#[superstruct(getter(copy))]
#[superstruct(only(V17), partial_getter(copy))]
#[ssz(with = "four_byte_option_usize")]
pub best_descendant: Option<usize>,
/// Indicates if an execution node has marked this block as valid. Also contains the execution
@@ -143,6 +143,8 @@ pub struct ProtoNode {
pub full_payload_weight: u64,
#[superstruct(only(V29), partial_getter(copy))]
pub execution_payload_block_hash: ExecutionBlockHash,
#[superstruct(only(V29), partial_getter(copy))]
pub execution_payload_parent_hash: ExecutionBlockHash,
/// Equivalent to spec's `block_timeliness[root][ATTESTATION_TIMELINESS_INDEX]`.
#[superstruct(only(V29), partial_getter(copy))]
pub block_timeliness_attestation_threshold: bool,
@@ -181,7 +183,6 @@ pub struct ProtoNode {
impl ProtoNode {
/// Generic version of spec's `parent_payload_status` that works for pre-Gloas nodes by
/// considering their parents Empty.
/// Pre-Gloas nodes have no ePBS, default to Empty.
pub fn get_parent_payload_status(&self) -> PayloadStatus {
self.parent_payload_status().unwrap_or(PayloadStatus::Empty)
}
@@ -535,7 +536,7 @@ impl ProtoArray {
.parent_root
.and_then(|parent| self.indices.get(&parent).copied());
let node = if !spec.fork_name_at_slot::<E>(current_slot).gloas_enabled() {
let node = if !spec.fork_name_at_slot::<E>(block.slot).gloas_enabled() {
ProtoNode::V17(ProtoNodeV17 {
slot: block.slot,
root: block.root,
@@ -570,31 +571,31 @@ impl ProtoArray {
block_root: block.root,
})?;
let parent_payload_status: PayloadStatus = if let Some(parent_node) =
parent_index.and_then(|idx| self.nodes.get(idx))
{
// Get the parent's execution block hash, handling both V17 and V29 nodes.
// V17 parents occur during the Gloas fork transition.
// TODO(gloas): the spec's `get_parent_payload_status` assumes all blocks are
// post-Gloas with bids. Revisit once the spec clarifies fork-transition behavior.
let parent_el_block_hash = match parent_node {
ProtoNode::V29(v29) => Some(v29.execution_payload_block_hash),
ProtoNode::V17(v17) => v17.execution_status.block_hash(),
};
// Per spec's `is_parent_node_full`: if the child's EL parent hash
// matches the parent's EL block hash, the child extends the parent's
// payload chain, meaning the parent was Full.
if parent_el_block_hash.is_some_and(|hash| execution_payload_parent_hash == hash) {
PayloadStatus::Full
let parent_payload_status: PayloadStatus =
if let Some(parent_node) = parent_index.and_then(|idx| self.nodes.get(idx)) {
match parent_node {
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.
if execution_payload_parent_hash == v29.execution_payload_block_hash {
PayloadStatus::Full
} else {
PayloadStatus::Empty
}
}
ProtoNode::V17(_) => {
// Parent is pre-Gloas, pre-Gloas blocks are treated as having Empty
// payload status. This case is reached during the fork transition.
PayloadStatus::Empty
}
}
} else {
PayloadStatus::Empty
}
} else {
// Parent is missing (genesis or pruned due to finalization). Default to Full
// since this path should only be hit at Gloas genesis, and extending the payload
// chain is the safe default.
PayloadStatus::Full
};
// TODO(gloas): re-assess this assumption
// Parent is missing (genesis or pruned due to finalization). Default to Full
// since this path should only be hit at Gloas genesis.
PayloadStatus::Full
};
// Per spec `get_forkchoice_store`: the anchor (genesis) block has
// its payload state initialized (`payload_states = {anchor_root: ...}`).
@@ -614,14 +615,13 @@ impl ProtoArray {
justified_checkpoint: block.justified_checkpoint,
finalized_checkpoint: block.finalized_checkpoint,
weight: 0,
best_child: None,
best_descendant: None,
unrealized_justified_checkpoint: block.unrealized_justified_checkpoint,
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
parent_payload_status,
empty_payload_weight: 0,
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, ensuring `is_payload_timely` and
// `is_payload_data_available` return true for the anchor.
@@ -642,7 +642,7 @@ impl ProtoArray {
block_timeliness_attestation_threshold: is_genesis
|| (is_current_slot
&& time_into_slot < spec.get_unaggregated_attestation_due()),
// TODO(gloas): use GLOAS-specific PTC due threshold once
// TODO(gloas): use Gloas-specific PTC due threshold once
// `get_payload_attestation_due_ms` is on ChainSpec.
block_timeliness_ptc_threshold: is_genesis
|| (is_current_slot && time_into_slot < spec.get_slot_duration() / 2),

View File

@@ -33,6 +33,47 @@ pub struct VoteTracker {
next_payload_present: bool,
}
// Can be deleted once the V28 schema migration is buried.
// Matches the on-disk format from schema v28: current_root, next_root, next_epoch.
#[derive(Default, PartialEq, Clone, Encode, Decode)]
pub struct VoteTrackerV28 {
current_root: Hash256,
next_root: Hash256,
next_epoch: Epoch,
}
// This impl is only used upon upgrade from pre-Gloas to Gloas with all pre-Gloas nodes.
// The payload status is `false` for pre-Gloas nodes.
impl From<VoteTrackerV28> for VoteTracker {
fn from(v: VoteTrackerV28) -> Self {
VoteTracker {
current_root: v.current_root,
next_root: v.next_root,
// The v28 format stored next_epoch rather than slots. Default to 0 since the
// vote tracker will be updated on the next attestation.
current_slot: Slot::new(0),
next_slot: Slot::new(0),
current_payload_present: false,
next_payload_present: false,
}
}
}
// This impl is only used upon downgrade from V29 to V28, with exclusively pre-Gloas nodes.
impl From<VoteTracker> for VoteTrackerV28 {
fn from(v: VoteTracker) -> Self {
// Drop the payload_present fields. This is safe because this is only called on pre-Gloas
// nodes.
VoteTrackerV28 {
current_root: v.current_root,
next_root: v.next_root,
// The v28 format stored next_epoch. Default to 0 since the vote tracker will be
// updated on the next attestation.
next_epoch: Epoch::new(0),
}
}
}
pub struct LatestMessage {
pub slot: Slot,
pub root: Hash256,
@@ -479,6 +520,7 @@ impl ProtoArrayForkChoice {
execution_status: ExecutionStatus,
execution_payload_parent_hash: Option<ExecutionBlockHash>,
execution_payload_block_hash: Option<ExecutionBlockHash>,
proposer_index: u64,
spec: &ChainSpec,
) -> Result<Self, String> {
let mut proto_array = ProtoArray {
@@ -505,7 +547,7 @@ impl ProtoArrayForkChoice {
unrealized_finalized_checkpoint: Some(finalized_checkpoint),
execution_payload_parent_hash,
execution_payload_block_hash,
proposer_index: Some(0),
proposer_index: Some(proposer_index),
};
proto_array
@@ -988,7 +1030,7 @@ impl ProtoArrayForkChoice {
.unwrap_or_else(|_| ExecutionStatus::irrelevant()),
unrealized_justified_checkpoint: block.unrealized_justified_checkpoint(),
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint(),
execution_payload_parent_hash: None,
execution_payload_parent_hash: block.execution_payload_parent_hash().ok(),
execution_payload_block_hash: block.execution_payload_block_hash().ok(),
proposer_index: block.proposer_index().ok(),
})
@@ -997,11 +1039,16 @@ impl ProtoArrayForkChoice {
/// Returns the `block.execution_status` field, if the block is present.
pub fn get_block_execution_status(&self, block_root: &Hash256) -> Option<ExecutionStatus> {
let block = self.get_proto_node(block_root)?;
block.execution_status().ok()
Some(
block
.execution_status()
.unwrap_or_else(|_| ExecutionStatus::irrelevant()),
)
}
/// Returns whether the execution payload for a block has been received.
/// Returns `false` for pre-GLOAS (V17) nodes or unknown blocks.
///
/// Returns `false` for pre-Gloas (V17) nodes or unknown blocks.
pub fn is_payload_received(&self, block_root: &Hash256) -> bool {
self.get_proto_node(block_root)
.and_then(|node| node.payload_received().ok())
@@ -1313,6 +1360,7 @@ mod test_compute_deltas {
execution_status,
None,
None,
0,
&spec,
)
.unwrap();
@@ -1467,6 +1515,7 @@ mod test_compute_deltas {
execution_status,
None,
None,
0,
&spec,
)
.unwrap();

View File

@@ -2,7 +2,7 @@ use crate::proto_array::ProposerBoost;
use crate::{
Error, JustifiedBalances,
proto_array::{ProtoArray, ProtoNode, ProtoNodeV17},
proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker},
proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker, VoteTrackerV28},
};
use ssz::{Encode, four_byte_option_impl};
use ssz_derive::{Decode, Encode};
@@ -22,6 +22,9 @@ pub type SszContainer = SszContainerV29;
no_enum
)]
pub struct SszContainer {
#[superstruct(only(V28))]
pub votes_v28: Vec<VoteTrackerV28>,
#[superstruct(only(V29))]
pub votes: Vec<VoteTracker>,
pub prune_threshold: usize,
// Deprecated, remove in a future schema migration
@@ -75,9 +78,19 @@ impl TryFrom<(SszContainerV29, JustifiedBalances)> for ProtoArrayForkChoice {
impl From<SszContainerV28> for SszContainerV29 {
fn from(v28: SszContainerV28) -> Self {
Self {
votes: v28.votes,
votes: v28.votes_v28.into_iter().map(Into::into).collect(),
prune_threshold: v28.prune_threshold,
nodes: v28.nodes.into_iter().map(ProtoNode::V17).collect(),
nodes: v28
.nodes
.into_iter()
.map(|mut node| {
// best_child/best_descendant are no longer used (replaced by
// the virtual tree walk). Clear during conversion.
node.best_child = None;
node.best_descendant = None;
ProtoNode::V17(node)
})
.collect(),
indices: v28.indices,
previous_proposer_boost: v28.previous_proposer_boost,
}
@@ -88,7 +101,7 @@ impl From<SszContainerV28> for SszContainerV29 {
impl From<SszContainerV29> for SszContainerV28 {
fn from(v29: SszContainerV29) -> Self {
Self {
votes: v29.votes,
votes_v28: v29.votes.into_iter().map(Into::into).collect(),
prune_threshold: v29.prune_threshold,
// These checkpoints are not consumed in v28 paths since the upgrade from v17,
// we can safely default the values.

View File

@@ -51,8 +51,8 @@ pub fn process_epoch<E: EthSpec>(
// without loss of correctness.
let current_epoch_progressive_balances = state.progressive_balances_cache().clone();
let current_epoch_total_active_balance = state.get_total_active_balance()?;
let participation_summary =
process_epoch_single_pass(state, spec, SinglePassConfig::default())?;
let epoch_result = process_epoch_single_pass(state, spec, SinglePassConfig::default())?;
let participation_summary = epoch_result.summary;
// Reset eth1 data votes.
process_eth1_data_reset(state)?;
@@ -79,6 +79,13 @@ pub fn process_epoch<E: EthSpec>(
// Rotate the epoch caches to suit the epoch transition.
state.advance_caches()?;
// Install the lookahead committee cache (built during PTC window processing) as the Next
// cache. After advance_caches, the lookahead epoch becomes the Next relative epoch.
if let Some(cache) = epoch_result.lookahead_committee_cache {
state.set_committee_cache(RelativeEpoch::Next, cache)?;
}
update_progressive_balances_on_epoch_transition(state, spec)?;
Ok(EpochProcessingSummary::Altair {

View File

@@ -12,12 +12,13 @@ use milhouse::{Cow, List, Vector};
use safe_arith::{SafeArith, SafeArithIter};
use std::cmp::{max, min};
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
use tracing::instrument;
use typenum::Unsigned;
use types::{
ActivationQueue, BeaconState, BeaconStateError, BuilderPendingPayment, ChainSpec, Checkpoint,
DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags, PendingDeposit,
ProgressiveBalancesCache, RelativeEpoch, Validator,
CommitteeCache, DepositData, Epoch, EthSpec, ExitCache, ForkName, ParticipationFlags,
PendingDeposit, ProgressiveBalancesCache, RelativeEpoch, Validator,
consts::altair::{
NUM_FLAG_INDICES, PARTICIPATION_FLAG_WEIGHTS, TIMELY_HEAD_FLAG_INDEX,
TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR,
@@ -34,6 +35,7 @@ pub struct SinglePassConfig {
pub effective_balance_updates: bool,
pub proposer_lookahead: bool,
pub builder_pending_payments: bool,
pub ptc_window: bool,
}
impl Default for SinglePassConfig {
@@ -54,6 +56,7 @@ impl SinglePassConfig {
effective_balance_updates: true,
proposer_lookahead: true,
builder_pending_payments: true,
ptc_window: true,
}
}
@@ -68,6 +71,7 @@ impl SinglePassConfig {
effective_balance_updates: false,
proposer_lookahead: false,
builder_pending_payments: false,
ptc_window: false,
}
}
}
@@ -139,12 +143,20 @@ impl ValidatorInfo {
}
}
/// Result of single-pass epoch processing.
pub struct SinglePassEpochResult<E: EthSpec> {
pub summary: ParticipationEpochSummary<E>,
/// Committee cache for the lookahead epoch, built during PTC window processing.
/// Can be installed as the Next committee cache after `advance_caches`.
pub lookahead_committee_cache: Option<Arc<CommitteeCache>>,
}
#[instrument(skip_all)]
pub fn process_epoch_single_pass<E: EthSpec>(
state: &mut BeaconState<E>,
spec: &ChainSpec,
conf: SinglePassConfig,
) -> Result<ParticipationEpochSummary<E>, Error> {
) -> Result<SinglePassEpochResult<E>, Error> {
initialize_epoch_cache(state, spec)?;
initialize_progressive_balances_cache(state, spec)?;
state.build_exit_cache(spec)?;
@@ -479,7 +491,16 @@ pub fn process_epoch_single_pass<E: EthSpec>(
process_proposer_lookahead(state, spec)?;
}
Ok(summary)
let lookahead_committee_cache = if conf.ptc_window && fork_name.gloas_enabled() {
Some(process_ptc_window(state, spec)?)
} else {
None
};
Ok(SinglePassEpochResult {
summary,
lookahead_committee_cache,
})
}
// TOOO(EIP-7917): use balances cache
@@ -512,6 +533,53 @@ pub fn process_proposer_lookahead<E: EthSpec>(
Ok(())
}
/// Process the PTC window, returning the committee cache built for the lookahead epoch.
///
/// The returned cache can be injected into the state's Next committee cache slot after
/// `advance_caches` is called during the epoch transition, avoiding redundant recomputation.
pub fn process_ptc_window<E: EthSpec>(
state: &mut BeaconState<E>,
spec: &ChainSpec,
) -> Result<Arc<CommitteeCache>, Error> {
let slots_per_epoch = E::slots_per_epoch() as usize;
// Convert Vector -> List to use tree-efficient pop_front.
let ptc_window = state.ptc_window()?.clone();
let mut window: List<_, E::PtcWindowLength> = List::from(ptc_window);
// Drop the oldest epoch from the front (reuses shared tree nodes).
window
.pop_front(slots_per_epoch)
.map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?;
// Compute PTC for the new lookahead epoch
let next_epoch = state
.current_epoch()
.safe_add(spec.min_seed_lookahead.as_u64())?
.safe_add(1)?;
let start_slot = next_epoch.start_slot(E::slots_per_epoch());
// Build a committee cache for the lookahead epoch (beyond the normal Next bound)
let committee_cache = state.initialize_committee_cache_for_lookahead(next_epoch, spec)?;
for i in 0..slots_per_epoch {
let slot = start_slot.safe_add(i as u64)?;
let ptc = state.compute_ptc_with_cache(slot, &committee_cache, spec)?;
let ptc_u64: Vec<u64> = ptc.into_iter().map(|v| v as u64).collect();
let entry = ssz_types::FixedVector::new(ptc_u64)
.map_err(|e| Error::BeaconStateError(BeaconStateError::SszTypesError(e)))?;
window
.push(entry)
.map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?;
}
// Convert List back to Vector.
*state.ptc_window_mut()? = Vector::try_from(window)
.map_err(|e| Error::BeaconStateError(BeaconStateError::MilhouseError(e)))?;
Ok(committee_cache)
}
/// Calculate the quorum threshold for builder payments based on total active balance.
fn get_builder_payment_quorum_threshold<E: EthSpec>(
state_ctxt: &StateContext,

View File

@@ -2,7 +2,9 @@ use crate::per_block_processing::{
is_valid_deposit_signature, process_operations::apply_deposit_for_builder,
};
use milhouse::{List, Vector};
use safe_arith::SafeArith;
use ssz_types::BitVector;
use ssz_types::FixedVector;
use std::collections::HashSet;
use std::mem;
use typenum::Unsigned;
@@ -102,13 +104,11 @@ pub fn upgrade_state_to_gloas<E: EthSpec>(
vec![0xFFu8; E::SlotsPerHistoricalRoot::to_usize() / 8].into(),
)
.map_err(|_| Error::InvalidBitfield)?,
builder_pending_payments: Vector::new(vec![
BuilderPendingPayment::default();
E::builder_pending_payments_limit()
])?,
builder_pending_payments: Vector::from_elem(BuilderPendingPayment::default())?,
builder_pending_withdrawals: List::default(), // Empty list initially,
latest_block_hash: pre.latest_execution_payload_header.block_hash,
payload_expected_withdrawals: List::default(),
ptc_window: Vector::from_elem(FixedVector::from_elem(0))?, // placeholder, will be initialized below
// Caches
total_active_balance: pre.total_active_balance,
progressive_balances_cache: mem::take(&mut pre.progressive_balances_cache),
@@ -120,10 +120,45 @@ pub fn upgrade_state_to_gloas<E: EthSpec>(
});
// [New in Gloas:EIP7732]
onboard_builders_from_pending_deposits(&mut post, spec)?;
initialize_ptc_window(&mut post, spec)?;
Ok(post)
}
/// Initialize the `ptc_window` field in the beacon state at fork transition.
///
/// The window contains:
/// - One epoch of empty entries (previous epoch)
/// - Computed PTC for the current epoch through `1 + MIN_SEED_LOOKAHEAD` epochs
fn initialize_ptc_window<E: EthSpec>(
state: &mut BeaconState<E>,
spec: &ChainSpec,
) -> Result<(), Error> {
let slots_per_epoch = E::slots_per_epoch() as usize;
let empty_previous_epoch = vec![FixedVector::<u64, E::PTCSize>::from_elem(0); slots_per_epoch];
let mut ptcs = empty_previous_epoch;
// Compute PTC for current epoch + lookahead epochs
let current_epoch = state.current_epoch();
for e in 0..=spec.min_seed_lookahead.as_u64() {
let epoch = current_epoch.safe_add(e)?;
let committee_cache = state.initialize_committee_cache_for_lookahead(epoch, spec)?;
let start_slot = epoch.start_slot(E::slots_per_epoch());
for i in 0..slots_per_epoch {
let slot = start_slot.safe_add(i as u64)?;
let ptc = state.compute_ptc_with_cache(slot, &committee_cache, spec)?;
let ptc_u64: Vec<u64> = ptc.into_iter().map(|v| v as u64).collect();
let entry = FixedVector::new(ptc_u64)?;
ptcs.push(entry);
}
}
*state.ptc_window_mut()? = Vector::new(ptcs)?;
Ok(())
}
/// Applies any pending deposit for builders, effectively onboarding builders at the fork.
fn onboard_builders_from_pending_deposits<E: EthSpec>(
state: &mut BeaconState<E>,

View File

@@ -0,0 +1,227 @@
# Mainnet config
# Extends the mainnet preset
PRESET_BASE: 'mainnet'
# Free-form short name of the network that this configuration applies to - known
# canonical network names include:
# * 'mainnet' - there can be only one
# * 'sepolia' - testnet
# * 'holesky' - testnet
# * 'hoodi' - testnet
# Must match the regex: [a-z0-9\-]
CONFIG_NAME: 'mainnet'
# Transition
# ---------------------------------------------------------------
# Estimated on Sept 15, 2022
TERMINAL_TOTAL_DIFFICULTY: 58750000000000000000000
# By default, don't use these params
TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615
# Genesis
# ---------------------------------------------------------------
# 2**14 (= 16,384) validators
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384
# Dec 1, 2020, 12pm UTC
MIN_GENESIS_TIME: 1606824000
# Initial fork version for mainnet
GENESIS_FORK_VERSION: 0x00000000
# 7 * 24 * 3,600 (= 604,800) seconds, 7 days
GENESIS_DELAY: 604800
# Forking
# ---------------------------------------------------------------
# Some forks are disabled for now:
# - These may be re-assigned to another fork-version later
# - Temporarily set to max uint64 value: 2**64 - 1
# Altair
ALTAIR_FORK_VERSION: 0x01000000
ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC
# Bellatrix
BELLATRIX_FORK_VERSION: 0x02000000
BELLATRIX_FORK_EPOCH: 144896 # Sept 6, 2022, 11:34:47am UTC
# Capella
CAPELLA_FORK_VERSION: 0x03000000
CAPELLA_FORK_EPOCH: 194048 # April 12, 2023, 10:27:35pm UTC
# Deneb
DENEB_FORK_VERSION: 0x04000000
DENEB_FORK_EPOCH: 269568 # March 13, 2024, 01:55:35pm UTC
# Electra
ELECTRA_FORK_VERSION: 0x05000000
ELECTRA_FORK_EPOCH: 364032 # May 7, 2025, 10:05:11am UTC
# Fulu
FULU_FORK_VERSION: 0x06000000
FULU_FORK_EPOCH: 411392 # December 3, 2025, 09:49:11pm UTC
# Gloas
GLOAS_FORK_VERSION: 0x07000000
GLOAS_FORK_EPOCH: 18446744073709551615
# Heze
HEZE_FORK_VERSION: 0x08000000
HEZE_FORK_EPOCH: 18446744073709551615
# EIP7928
EIP7928_FORK_VERSION: 0xe7928000 # temporary stub
EIP7928_FORK_EPOCH: 18446744073709551615
# Time parameters
# ---------------------------------------------------------------
# 12000 milliseconds
SLOT_DURATION_MS: 12000
# 14 (estimate from Eth1 mainnet)
SECONDS_PER_ETH1_BLOCK: 14
# 2**8 (= 256) epochs
MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256
# 2**8 (= 256) epochs
SHARD_COMMITTEE_PERIOD: 256
# 2**11 (= 2,048) Eth1 blocks
ETH1_FOLLOW_DISTANCE: 2048
# 1667 basis points, ~17% of SLOT_DURATION_MS
PROPOSER_REORG_CUTOFF_BPS: 1667
# 3333 basis points, ~33% of SLOT_DURATION_MS
ATTESTATION_DUE_BPS: 3333
# 6667 basis points, ~67% of SLOT_DURATION_MS
AGGREGATE_DUE_BPS: 6667
# Altair
# 3333 basis points, ~33% of SLOT_DURATION_MS
SYNC_MESSAGE_DUE_BPS: 3333
# 6667 basis points, ~67% of SLOT_DURATION_MS
CONTRIBUTION_DUE_BPS: 6667
# Gloas
# 2**6 (= 64) epochs
MIN_BUILDER_WITHDRAWABILITY_DELAY: 64
# 2500 basis points, 25% of SLOT_DURATION_MS
ATTESTATION_DUE_BPS_GLOAS: 2500
# 5000 basis points, 50% of SLOT_DURATION_MS
AGGREGATE_DUE_BPS_GLOAS: 5000
# 2500 basis points, 25% of SLOT_DURATION_MS
SYNC_MESSAGE_DUE_BPS_GLOAS: 2500
# 5000 basis points, 50% of SLOT_DURATION_MS
CONTRIBUTION_DUE_BPS_GLOAS: 5000
# 7500 basis points, 75% of SLOT_DURATION_MS
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
# Validator cycle
# ---------------------------------------------------------------
# 2**2 (= 4)
INACTIVITY_SCORE_BIAS: 4
# 2**4 (= 16)
INACTIVITY_SCORE_RECOVERY_RATE: 16
# 2**4 * 10**9 (= 16,000,000,000) Gwei
EJECTION_BALANCE: 16000000000
# 2**2 (= 4) validators
MIN_PER_EPOCH_CHURN_LIMIT: 4
# 2**16 (= 65,536)
CHURN_LIMIT_QUOTIENT: 65536
# Deneb
# 2**3 (= 8) (*deprecated*)
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8
# Electra
# 2**7 * 10**9 (= 128,000,000,000) Gwei
MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000
# 2**8 * 10**9 (= 256,000,000,000) Gwei
MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000
# Fork choice
# ---------------------------------------------------------------
# 40%
PROPOSER_SCORE_BOOST: 40
# 20%
REORG_HEAD_WEIGHT_THRESHOLD: 20
# 160%
REORG_PARENT_WEIGHT_THRESHOLD: 160
# 2 epochs
REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2
# Deposit contract
# ---------------------------------------------------------------
# Ethereum PoW Mainnet
DEPOSIT_CHAIN_ID: 1
DEPOSIT_NETWORK_ID: 1
DEPOSIT_CONTRACT_ADDRESS: 0x00000000219ab540356cBB839Cbe05303d7705Fa
# Networking
# ---------------------------------------------------------------
# 10 * 2**20 (= 10,485,760) bytes, 10 MiB
MAX_PAYLOAD_SIZE: 10485760
# 2**10 (= 1,024) blocks
MAX_REQUEST_BLOCKS: 1024
# 2**8 (= 256) epochs
EPOCHS_PER_SUBNET_SUBSCRIPTION: 256
# 2**5 (= 32) slots
ATTESTATION_PROPAGATION_SLOT_RANGE: 32
# 500ms
MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500
MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000
MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000
# 2 subnets per node
SUBNETS_PER_NODE: 2
# 2**6 (= 64) subnets
ATTESTATION_SUBNET_COUNT: 64
# 0 bits
ATTESTATION_SUBNET_EXTRA_BITS: 0
# Deneb
# 2**7 (= 128) blocks
MAX_REQUEST_BLOCKS_DENEB: 128
# 2**12 (= 4,096) epochs
MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096
# 6 subnets
BLOB_SIDECAR_SUBNET_COUNT: 6
# 6 blobs
MAX_BLOBS_PER_BLOCK: 6
# Electra
# 9 subnets
BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9
# 9 blobs
MAX_BLOBS_PER_BLOCK_ELECTRA: 9
# Fulu
# 2**7 (= 128) groups
NUMBER_OF_CUSTODY_GROUPS: 128
# 2**7 (= 128) subnets
DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128
# 2**3 (= 8) samples
SAMPLES_PER_SLOT: 8
# 2**2 (= 4) sidecars
CUSTODY_REQUIREMENT: 4
# 2**3 (= 8) sidecars
VALIDATOR_CUSTODY_REQUIREMENT: 8
# 2**5 * 10**9 (= 32,000,000,000) Gwei
BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000
# 2**12 (= 4,096) epochs
MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096
# Gloas
# 2**7 (= 128) payloads
MAX_REQUEST_PAYLOADS: 128
# Heze
# 2**4 (= 16) inclusion lists
MAX_REQUEST_INCLUSION_LIST: 16
# 2**13 (= 8,192) bytes
MAX_BYTES_PER_INCLUSION_LIST: 8192
# Blob Scheduling
# ---------------------------------------------------------------
BLOB_SCHEDULE:
- EPOCH: 412672 # December 9, 2025, 02:21:11pm UTC
MAX_BLOBS_PER_BLOCK: 15
- EPOCH: 419072 # January 7, 2026, 01:01:11am UTC
MAX_BLOBS_PER_BLOCK: 21

View File

@@ -0,0 +1,220 @@
# Minimal config
# Extends the minimal preset
PRESET_BASE: 'minimal'
# Free-form short name of the network that this configuration applies to - known
# canonical network names include:
# * 'minimal' - spec-testing
# Must match the regex: [a-z0-9\-]
CONFIG_NAME: 'minimal'
# Transition
# ---------------------------------------------------------------
# 2**256-2**10 for testing minimal network
TERMINAL_TOTAL_DIFFICULTY: 115792089237316195423570985008687907853269984665640564039457584007913129638912
# By default, don't use these params
TERMINAL_BLOCK_HASH: 0x0000000000000000000000000000000000000000000000000000000000000000
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH: 18446744073709551615
# Genesis
# ---------------------------------------------------------------
# [customized] 2**6 (= 64) validators
MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 64
# [customized] Jan 3, 2020, 12am UTC
MIN_GENESIS_TIME: 1578009600
# [customized] Initial fork version for minimal
GENESIS_FORK_VERSION: 0x00000001
# [customized] 5 * 60 (= 300) seconds
GENESIS_DELAY: 300
# Forking
# ---------------------------------------------------------------
# Values provided for illustrative purposes.
# Individual tests/testnets may set different values.
# [customized] Altair
ALTAIR_FORK_VERSION: 0x01000001
ALTAIR_FORK_EPOCH: 18446744073709551615
# [customized] Bellatrix
BELLATRIX_FORK_VERSION: 0x02000001
BELLATRIX_FORK_EPOCH: 18446744073709551615
# [customized] Capella
CAPELLA_FORK_VERSION: 0x03000001
CAPELLA_FORK_EPOCH: 18446744073709551615
# [customized] Deneb
DENEB_FORK_VERSION: 0x04000001
DENEB_FORK_EPOCH: 18446744073709551615
# [customized] Electra
ELECTRA_FORK_VERSION: 0x05000001
ELECTRA_FORK_EPOCH: 18446744073709551615
# [customized] Fulu
FULU_FORK_VERSION: 0x06000001
FULU_FORK_EPOCH: 18446744073709551615
# [customized] Gloas
GLOAS_FORK_VERSION: 0x07000001
GLOAS_FORK_EPOCH: 18446744073709551615
# [customized] Heze
HEZE_FORK_VERSION: 0x08000001
HEZE_FORK_EPOCH: 18446744073709551615
# [customized] EIP7928
EIP7928_FORK_VERSION: 0xe7928001
EIP7928_FORK_EPOCH: 18446744073709551615
# Time parameters
# ---------------------------------------------------------------
# [customized] 6000 milliseconds
SLOT_DURATION_MS: 6000
# 14 (estimate from Eth1 mainnet)
SECONDS_PER_ETH1_BLOCK: 14
# 2**8 (= 256) epochs
MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256
# [customized] 2**6 (= 64) epochs
SHARD_COMMITTEE_PERIOD: 64
# [customized] 2**4 (= 16) Eth1 blocks
ETH1_FOLLOW_DISTANCE: 16
# 1667 basis points, ~17% of SLOT_DURATION_MS
PROPOSER_REORG_CUTOFF_BPS: 1667
# 3333 basis points, ~33% of SLOT_DURATION_MS
ATTESTATION_DUE_BPS: 3333
# 6667 basis points, ~67% of SLOT_DURATION_MS
AGGREGATE_DUE_BPS: 6667
# Altair
# 3333 basis points, ~33% of SLOT_DURATION_MS
SYNC_MESSAGE_DUE_BPS: 3333
# 6667 basis points, ~67% of SLOT_DURATION_MS
CONTRIBUTION_DUE_BPS: 6667
# Gloas
# [customized] 2**1 (= 2) epochs
MIN_BUILDER_WITHDRAWABILITY_DELAY: 2
# 2500 basis points, 25% of SLOT_DURATION_MS
ATTESTATION_DUE_BPS_GLOAS: 2500
# 5000 basis points, 50% of SLOT_DURATION_MS
AGGREGATE_DUE_BPS_GLOAS: 5000
# 2500 basis points, 25% of SLOT_DURATION_MS
SYNC_MESSAGE_DUE_BPS_GLOAS: 2500
# 5000 basis points, 50% of SLOT_DURATION_MS
CONTRIBUTION_DUE_BPS_GLOAS: 5000
# 7500 basis points, 75% of SLOT_DURATION_MS
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
# Validator cycle
# ---------------------------------------------------------------
# 2**2 (= 4)
INACTIVITY_SCORE_BIAS: 4
# 2**4 (= 16)
INACTIVITY_SCORE_RECOVERY_RATE: 16
# 2**4 * 10**9 (= 16,000,000,000) Gwei
EJECTION_BALANCE: 16000000000
# [customized] 2**1 (= 2) validators
MIN_PER_EPOCH_CHURN_LIMIT: 2
# [customized] 2**5 (= 32)
CHURN_LIMIT_QUOTIENT: 32
# Deneb
# [customized] 2**2 (= 4) (*deprecated*)
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 4
# Electra
# [customized] 2**6 * 10**9 (= 64,000,000,000) Gwei
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
# Fork choice
# ---------------------------------------------------------------
# 40%
PROPOSER_SCORE_BOOST: 40
# 20%
REORG_HEAD_WEIGHT_THRESHOLD: 20
# 160%
REORG_PARENT_WEIGHT_THRESHOLD: 160
# 2 epochs
REORG_MAX_EPOCHS_SINCE_FINALIZATION: 2
# Deposit contract
# ---------------------------------------------------------------
# Ethereum Goerli testnet
DEPOSIT_CHAIN_ID: 5
DEPOSIT_NETWORK_ID: 5
# Configured on a per testnet basis
DEPOSIT_CONTRACT_ADDRESS: 0x1234567890123456789012345678901234567890
# Networking
# ---------------------------------------------------------------
# 10 * 2**20 (= 10,485,760) bytes, 10 MiB
MAX_PAYLOAD_SIZE: 10485760
# 2**10 (= 1,024) blocks
MAX_REQUEST_BLOCKS: 1024
# 2**8 (= 256) epochs
EPOCHS_PER_SUBNET_SUBSCRIPTION: 256
# 2**5 (= 32) slots
ATTESTATION_PROPAGATION_SLOT_RANGE: 32
# 500ms
MAXIMUM_GOSSIP_CLOCK_DISPARITY: 500
MESSAGE_DOMAIN_INVALID_SNAPPY: 0x00000000
MESSAGE_DOMAIN_VALID_SNAPPY: 0x01000000
# 2 subnets per node
SUBNETS_PER_NODE: 2
# 2**6 (= 64) subnets
ATTESTATION_SUBNET_COUNT: 64
# 0 bits
ATTESTATION_SUBNET_EXTRA_BITS: 0
# Deneb
# 2**7 (= 128) blocks
MAX_REQUEST_BLOCKS_DENEB: 128
# 2**12 (= 4,096) epochs
MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096
# 6 subnets
BLOB_SIDECAR_SUBNET_COUNT: 6
# 6 blobs
MAX_BLOBS_PER_BLOCK: 6
# Electra
# 9 subnets
BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: 9
# 9 blobs
MAX_BLOBS_PER_BLOCK_ELECTRA: 9
# Fulu
# 2**7 (= 128) groups
NUMBER_OF_CUSTODY_GROUPS: 128
# 2**7 (= 128) subnets
DATA_COLUMN_SIDECAR_SUBNET_COUNT: 128
# 2**3 (= 8) samples
SAMPLES_PER_SLOT: 8
# 2**2 (= 4) sidecars
CUSTODY_REQUIREMENT: 4
# 2**3 (= 8) sidecars
VALIDATOR_CUSTODY_REQUIREMENT: 8
# 2**5 * 10**9 (= 32,000,000,000) Gwei
BALANCE_PER_ADDITIONAL_CUSTODY_GROUP: 32000000000
# 2**12 (= 4,096) epochs
MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: 4096
# Gloas
# 2**7 (= 128) payloads
MAX_REQUEST_PAYLOADS: 128
# Heze
# 2**4 (= 16) inclusion lists
MAX_REQUEST_INCLUSION_LIST: 16
# 2**13 (= 8,192) bytes
MAX_BYTES_PER_INCLUSION_LIST: 8192
# Blob Scheduling
# ---------------------------------------------------------------
BLOB_SCHEDULE: []

View File

@@ -96,8 +96,7 @@ pub struct ChainSpec {
* Time parameters
*/
pub genesis_delay: u64,
// TODO deprecate seconds_per_slot
pub seconds_per_slot: u64,
seconds_per_slot: u64,
// Private so that this value can't get changed except via the `set_slot_duration_ms` function.
slot_duration_ms: u64,
pub min_attestation_inclusion_delay: u64,
@@ -829,15 +828,17 @@ impl ChainSpec {
/// Returns the min epoch for blob / data column sidecar requests based on the current epoch.
/// Switch to use the column sidecar config once the `blob_retention_epoch` has passed Fulu fork epoch.
/// Never uses the `blob_retention_epoch` for networks that started with Fulu enabled.
pub fn min_epoch_data_availability_boundary(&self, current_epoch: Epoch) -> Option<Epoch> {
let fork_epoch = self.deneb_fork_epoch?;
let deneb_fork_epoch = self.deneb_fork_epoch?;
let blob_retention_epoch =
current_epoch.saturating_sub(self.min_epochs_for_blob_sidecars_requests);
match self.fulu_fork_epoch {
Some(fulu_fork_epoch) if blob_retention_epoch > fulu_fork_epoch => Some(
current_epoch.saturating_sub(self.min_epochs_for_data_column_sidecars_requests),
),
_ => Some(std::cmp::max(fork_epoch, blob_retention_epoch)),
if let Some(fulu_fork_epoch) = self.fulu_fork_epoch
&& blob_retention_epoch >= fulu_fork_epoch
{
Some(current_epoch.saturating_sub(self.min_epochs_for_data_column_sidecars_requests))
} else {
Some(std::cmp::max(deneb_fork_epoch, blob_retention_epoch))
}
}
@@ -914,6 +915,7 @@ impl ChainSpec {
/// Set the duration of a slot (in ms).
pub fn set_slot_duration_ms<E: EthSpec>(mut self, slot_duration_ms: u64) -> Self {
self.slot_duration_ms = slot_duration_ms;
self.seconds_per_slot = slot_duration_ms.saturating_div(1000);
self.compute_derived_values::<E>()
}
@@ -1235,7 +1237,7 @@ impl ChainSpec {
gloas_fork_epoch: None,
builder_payment_threshold_numerator: 6,
builder_payment_threshold_denominator: 10,
min_builder_withdrawability_delay: Epoch::new(4096),
min_builder_withdrawability_delay: Epoch::new(64),
max_request_payloads: 128,
/*
@@ -1381,6 +1383,7 @@ impl ChainSpec {
// Gloas
gloas_fork_version: [0x07, 0x00, 0x00, 0x01],
gloas_fork_epoch: None,
min_builder_withdrawability_delay: Epoch::new(2),
/*
* Derived time values (set by `compute_derived_values()`)
@@ -1391,6 +1394,9 @@ impl ChainSpec {
sync_message_due: Duration::from_millis(1999),
contribution_and_proof_due: Duration::from_millis(4000),
// Networking Fulu
blob_schedule: BlobSchedule::default(),
// Other
network_id: 2, // lighthouse testnet network id
deposit_chain_id: 5,
@@ -1631,7 +1637,7 @@ impl ChainSpec {
gloas_fork_epoch: None,
builder_payment_threshold_numerator: 6,
builder_payment_threshold_denominator: 10,
min_builder_withdrawability_delay: Epoch::new(4096),
min_builder_withdrawability_delay: Epoch::new(64),
max_request_payloads: 128,
/*
@@ -1908,8 +1914,9 @@ pub struct Config {
#[serde(deserialize_with = "deserialize_fork_epoch")]
pub gloas_fork_epoch: Option<MaybeQuoted<Epoch>>,
#[serde(with = "serde_utils::quoted_u64")]
seconds_per_slot: u64,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
seconds_per_slot: Option<MaybeQuoted<u64>>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
slot_duration_ms: Option<MaybeQuoted<u64>>,
@@ -2064,6 +2071,10 @@ pub struct Config {
#[serde(default = "default_contribution_due_bps")]
#[serde(with = "serde_utils::quoted_u64")]
contribution_due_bps: u64,
#[serde(default = "default_min_builder_withdrawability_delay")]
#[serde(with = "serde_utils::quoted_u64")]
min_builder_withdrawability_delay: u64,
}
fn default_bellatrix_fork_version() -> [u8; 4] {
@@ -2289,6 +2300,10 @@ const fn default_contribution_due_bps() -> u64 {
6667
}
const fn default_min_builder_withdrawability_delay() -> u64 {
64
}
fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize {
let max_request_blocks = max_request_blocks as usize;
RuntimeVariableList::<Hash256>::new(
@@ -2459,7 +2474,9 @@ impl Config {
.gloas_fork_epoch
.map(|epoch| MaybeQuoted { value: epoch }),
seconds_per_slot: spec.seconds_per_slot,
seconds_per_slot: Some(MaybeQuoted {
value: spec.seconds_per_slot,
}),
slot_duration_ms: Some(MaybeQuoted {
value: spec.slot_duration_ms,
}),
@@ -2525,6 +2542,8 @@ impl Config {
aggregate_due_bps: spec.aggregate_due_bps,
sync_message_due_bps: spec.sync_message_due_bps,
contribution_due_bps: spec.contribution_due_bps,
min_builder_withdrawability_delay: spec.min_builder_withdrawability_delay.as_u64(),
}
}
@@ -2616,12 +2635,21 @@ impl Config {
aggregate_due_bps,
sync_message_due_bps,
contribution_due_bps,
min_builder_withdrawability_delay,
} = self;
if preset_base != E::spec_name().to_string().as_str() {
return None;
}
// Fail if seconds_per_slot and slot_duration_ms are both set but are inconsistent.
if let (Some(seconds_per_slot), Some(slot_duration_ms)) =
(seconds_per_slot, slot_duration_ms)
&& seconds_per_slot.value.saturating_mul(1000) != slot_duration_ms.value
{
return None;
}
let spec = ChainSpec {
config_name: config_name.clone(),
min_genesis_active_validator_count,
@@ -2642,10 +2670,12 @@ impl Config {
fulu_fork_version,
gloas_fork_version,
gloas_fork_epoch: gloas_fork_epoch.map(|q| q.value),
seconds_per_slot,
seconds_per_slot: seconds_per_slot
.map(|q| q.value)
.or_else(|| slot_duration_ms.and_then(|q| q.value.checked_div(1000)))?,
slot_duration_ms: slot_duration_ms
.map(|q| q.value)
.unwrap_or_else(|| seconds_per_slot.saturating_mul(1000)),
.or_else(|| seconds_per_slot.map(|q| q.value.saturating_mul(1000)))?,
seconds_per_eth1_block,
min_validator_withdrawability_delay,
shard_committee_period,
@@ -2705,6 +2735,8 @@ impl Config {
sync_message_due_bps,
contribution_due_bps,
min_builder_withdrawability_delay: Epoch::new(min_builder_withdrawability_delay),
..chain_spec.clone()
};
Some(spec.compute_derived_values::<E>())
@@ -2853,6 +2885,9 @@ mod yaml_tests {
use super::*;
use crate::core::MinimalEthSpec;
use paste::paste;
use std::collections::BTreeSet;
use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::NamedTempFile;
@@ -2902,6 +2937,67 @@ mod yaml_tests {
assert_eq!(from, yamlconfig);
}
#[test]
fn slot_duration_fallback_both_fields() {
let mainnet = ChainSpec::mainnet();
let mut config = Config::from_chain_spec::<MainnetEthSpec>(&mainnet);
config.seconds_per_slot = Some(MaybeQuoted { value: 12 });
config.slot_duration_ms = Some(MaybeQuoted { value: 12000 });
let spec = config
.apply_to_chain_spec::<MainnetEthSpec>(&mainnet)
.unwrap();
assert_eq!(spec.seconds_per_slot, 12);
assert_eq!(spec.slot_duration_ms, 12000);
}
#[test]
fn slot_duration_fallback_both_fields_inconsistent() {
let mainnet = ChainSpec::mainnet();
let mut config = Config::from_chain_spec::<MainnetEthSpec>(&mainnet);
config.seconds_per_slot = Some(MaybeQuoted { value: 10 });
config.slot_duration_ms = Some(MaybeQuoted { value: 12000 });
assert_eq!(config.apply_to_chain_spec::<MainnetEthSpec>(&mainnet), None);
}
#[test]
fn slot_duration_fallback_seconds_only() {
let mainnet = ChainSpec::mainnet();
let mut config = Config::from_chain_spec::<MainnetEthSpec>(&mainnet);
config.seconds_per_slot = Some(MaybeQuoted { value: 12 });
config.slot_duration_ms = None;
let spec = config
.apply_to_chain_spec::<MainnetEthSpec>(&mainnet)
.unwrap();
assert_eq!(spec.seconds_per_slot, 12);
assert_eq!(spec.slot_duration_ms, 12000);
}
#[test]
fn slot_duration_fallback_ms_only() {
let mainnet = ChainSpec::mainnet();
let mut config = Config::from_chain_spec::<MainnetEthSpec>(&mainnet);
config.seconds_per_slot = None;
config.slot_duration_ms = Some(MaybeQuoted { value: 12000 });
let spec = config
.apply_to_chain_spec::<MainnetEthSpec>(&mainnet)
.unwrap();
assert_eq!(spec.seconds_per_slot, 12);
assert_eq!(spec.slot_duration_ms, 12000);
}
#[test]
fn slot_duration_fallback_neither() {
let mainnet = ChainSpec::mainnet();
let mut config = Config::from_chain_spec::<MainnetEthSpec>(&mainnet);
config.seconds_per_slot = None;
config.slot_duration_ms = None;
assert!(
config
.apply_to_chain_spec::<MainnetEthSpec>(&mainnet)
.is_none()
);
}
#[test]
fn blob_schedule_max_blobs_per_block() {
let spec_contents = r#"
@@ -3304,17 +3400,19 @@ mod yaml_tests {
spec.min_epoch_data_availability_boundary(fulu_fork_epoch)
);
// `min_epochs_for_data_sidecar_requests` at fulu fork epoch + min_epochs_for_blob_sidecars_request
let blob_retention_epoch_after_fulu = fulu_fork_epoch + blob_retention_epochs;
let expected_blob_retention_epoch = blob_retention_epoch_after_fulu - blob_retention_epochs;
// Now, the blob retention period starts still before the fulu fork epoch, so the boundary
// should respect the blob retention period.
let half_blob_retention_epoch_after_fulu = fulu_fork_epoch + (blob_retention_epochs / 2);
let expected_blob_retention_epoch =
half_blob_retention_epoch_after_fulu - blob_retention_epochs;
assert_eq!(
Some(expected_blob_retention_epoch),
spec.min_epoch_data_availability_boundary(blob_retention_epoch_after_fulu)
spec.min_epoch_data_availability_boundary(half_blob_retention_epoch_after_fulu)
);
// After the final blob retention epoch, `min_epochs_for_data_sidecar_requests` should be calculated
// using `min_epochs_for_data_column_sidecars_request`
let current_epoch = blob_retention_epoch_after_fulu + 1;
// If the retention period starts with the fulu fork epoch, there are no more blobs to
// retain, and the return value will be based on the data column retention period.
let current_epoch = fulu_fork_epoch + blob_retention_epochs;
let expected_data_column_retention_epoch = current_epoch - data_column_retention_epochs;
assert_eq!(
Some(expected_data_column_retention_epoch),
@@ -3322,6 +3420,39 @@ mod yaml_tests {
);
}
#[test]
fn min_epochs_for_data_sidecar_requests_fulu_genesis() {
type E = MainnetEthSpec;
let spec = {
// fulu active at genesis
let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
// set a different value for testing purpose, 4096 / 2 = 2048
spec.min_epochs_for_data_column_sidecars_requests =
spec.min_epochs_for_blob_sidecars_requests / 2;
Arc::new(spec)
};
let blob_retention_epochs = spec.min_epochs_for_blob_sidecars_requests;
let data_column_retention_epochs = spec.min_epochs_for_data_column_sidecars_requests;
// If Fulu is activated at genesis, the column retention period should always be used.
let assert_correct_boundary = |epoch| {
let epoch = Epoch::new(epoch);
assert_eq!(
Some(epoch.saturating_sub(data_column_retention_epochs)),
spec.min_epoch_data_availability_boundary(epoch)
)
};
assert_correct_boundary(0);
assert_correct_boundary(1);
assert_correct_boundary(blob_retention_epochs - 1);
assert_correct_boundary(blob_retention_epochs);
assert_correct_boundary(blob_retention_epochs + 1);
assert_correct_boundary(data_column_retention_epochs - 1);
assert_correct_boundary(data_column_retention_epochs);
assert_correct_boundary(data_column_retention_epochs + 1);
}
#[test]
fn proposer_shuffling_decision_root_around_epoch_boundary() {
type E = MainnetEthSpec;
@@ -3375,7 +3506,6 @@ mod yaml_tests {
// Test slot duration
let slot_duration = spec.get_slot_duration();
assert_eq!(slot_duration, Duration::from_millis(12000));
assert_eq!(slot_duration, Duration::from_secs(spec.seconds_per_slot));
// Test edge cases with custom spec
let mut custom_spec = spec.clone();
@@ -3485,4 +3615,133 @@ mod yaml_tests {
spec.attestation_due_bps = 15000;
spec.compute_derived_values::<MainnetEthSpec>();
}
fn configs_base_path() -> PathBuf {
env::var("CARGO_MANIFEST_DIR")
.expect("should know manifest dir")
.parse::<PathBuf>()
.expect("should parse manifest dir as path")
.join("configs")
}
/// Upstream config keys that Lighthouse intentionally does not include in its
/// `Config` struct. These are forks/features not yet implemented. Update this
/// list as new forks are added.
const UPSTREAM_KEYS_NOT_IN_LIGHTHOUSE: &[&str] = &[
// Forks not yet implemented
"HEZE_FORK_VERSION",
"HEZE_FORK_EPOCH",
"EIP7928_FORK_VERSION",
"EIP7928_FORK_EPOCH",
// Gloas params not yet in Config
"ATTESTATION_DUE_BPS_GLOAS",
"AGGREGATE_DUE_BPS_GLOAS",
"SYNC_MESSAGE_DUE_BPS_GLOAS",
"CONTRIBUTION_DUE_BPS_GLOAS",
"PAYLOAD_ATTESTATION_DUE_BPS",
"MAX_REQUEST_PAYLOADS",
// Gloas fork choice params not yet in Config
"REORG_HEAD_WEIGHT_THRESHOLD",
"REORG_PARENT_WEIGHT_THRESHOLD",
"REORG_MAX_EPOCHS_SINCE_FINALIZATION",
// Heze networking
"VIEW_FREEZE_CUTOFF_BPS",
"INCLUSION_LIST_SUBMISSION_DUE_BPS",
"PROPOSER_INCLUSION_LIST_CUTOFF_BPS",
"MAX_REQUEST_INCLUSION_LIST",
"MAX_BYTES_PER_INCLUSION_LIST",
];
/// Compare a `ChainSpec` against an upstream consensus-specs config YAML file.
///
/// 1. Extracts keys from the raw YAML text (to avoid yaml_serde's inability
/// to parse integers > u64 into `Value`/`Mapping` types) and checks that
/// every key is either known to `Config` or explicitly listed in
/// `UPSTREAM_KEYS_NOT_IN_LIGHTHOUSE`.
/// 2. Deserializes the upstream YAML as `Config` (which has custom
/// deserializers for large values like `TERMINAL_TOTAL_DIFFICULTY`) and
/// compares against `Config::from_chain_spec`.
fn config_test<E: EthSpec>(spec: &ChainSpec, config_name: &str) {
let file_path = configs_base_path().join(format!("{config_name}.yaml"));
let upstream_yaml = std::fs::read_to_string(&file_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", file_path.display()));
// Extract top-level keys from the raw YAML text. We can't parse as
// yaml_serde::Mapping because yaml_serde cannot represent integers
// exceeding u64 (e.g. TERMINAL_TOTAL_DIFFICULTY). Config YAML uses a
// simple `KEY: value` format with no indentation for top-level keys.
let upstream_keys: BTreeSet<String> = upstream_yaml
.lines()
.filter_map(|line| {
// Skip comments, blank lines, and indented lines (nested YAML).
if line.is_empty()
|| line.starts_with('#')
|| line.starts_with(' ')
|| line.starts_with('\t')
{
return None;
}
line.split(':').next().map(|k| k.to_string())
})
.collect();
// Get the set of keys that Config knows about by serializing and collecting
// keys. Also include keys for optional fields that may be skipped during
// serialization (e.g. CONFIG_NAME).
let our_config = Config::from_chain_spec::<E>(spec);
let our_yaml = yaml_serde::to_string(&our_config).expect("failed to serialize Config");
let our_mapping: yaml_serde::Mapping =
yaml_serde::from_str(&our_yaml).expect("failed to re-parse our Config");
let mut known_keys: BTreeSet<String> = our_mapping
.keys()
.filter_map(|k| k.as_str().map(String::from))
.collect();
// Fields that Config knows but may skip during serialization.
known_keys.insert("CONFIG_NAME".to_string());
// Check for upstream keys that our Config doesn't know about.
let mut missing_keys: Vec<&String> = upstream_keys
.iter()
.filter(|k| {
!known_keys.contains(k.as_str())
&& !UPSTREAM_KEYS_NOT_IN_LIGHTHOUSE.contains(&k.as_str())
})
.collect();
missing_keys.sort();
assert!(
missing_keys.is_empty(),
"Upstream {config_name} config has keys not present in Lighthouse Config \
(add to Config or to UPSTREAM_KEYS_NOT_IN_LIGHTHOUSE): {missing_keys:?}"
);
// Compare values for all fields Config knows about.
let mut upstream_config: Config = yaml_serde::from_str(&upstream_yaml)
.unwrap_or_else(|e| panic!("failed to parse {config_name} as Config: {e}"));
// CONFIG_NAME is network metadata (not a spec parameter), so align it
// before comparing.
upstream_config.config_name = our_config.config_name.clone();
// SECONDS_PER_SLOT is deprecated upstream but we still emit it, so
// fill it in if the upstream YAML omitted it.
if upstream_config.seconds_per_slot.is_none() {
upstream_config.seconds_per_slot = our_config.seconds_per_slot;
}
assert_eq!(
upstream_config, our_config,
"Config mismatch for {config_name}"
);
}
#[test]
fn mainnet_config_consistent() {
let spec = ChainSpec::mainnet();
config_test::<MainnetEthSpec>(&spec, "mainnet");
}
#[test]
fn minimal_config_consistent() {
let spec = ChainSpec::minimal();
config_test::<MinimalEthSpec>(&spec, "minimal");
}
}

View File

@@ -6,9 +6,9 @@ use std::{
use safe_arith::{ArithError, SafeArith};
use serde::{Deserialize, Serialize};
use typenum::{
U0, U1, U2, U4, U8, U16, U17, U32, U64, U128, U256, U512, U625, U1024, U2048, U4096, U8192,
U16384, U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728, U1073741824,
U1099511627776, UInt, Unsigned, bit::B0,
U0, U1, U2, U4, U8, U16, U17, U24, U32, U48, U64, U96, U128, U256, U512, U625, U1024, U2048,
U4096, U8192, U16384, U65536, U131072, U262144, U1048576, U16777216, U33554432, U134217728,
U1073741824, U1099511627776, UInt, Unsigned, bit::B0,
};
use crate::core::{ChainSpec, Epoch};
@@ -176,6 +176,7 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq +
* New in Gloas
*/
type PTCSize: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type PtcWindowLength: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type MaxPayloadAttestations: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type BuilderPendingPaymentsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq;
type BuilderPendingWithdrawalsLimit: Unsigned + Clone + Sync + Send + Debug + PartialEq;
@@ -428,6 +429,11 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq +
Self::PTCSize::to_usize()
}
/// Returns the `PtcWindowLength` constant for this specification.
fn ptc_window_length() -> usize {
Self::PtcWindowLength::to_usize()
}
/// Returns the `MaxPayloadAttestations` constant for this specification.
fn max_payload_attestations() -> usize {
Self::MaxPayloadAttestations::to_usize()
@@ -515,6 +521,7 @@ impl EthSpec for MainnetEthSpec {
type MaxWithdrawalRequestsPerPayload = U16;
type MaxPendingDepositsPerEpoch = U16;
type PTCSize = U512;
type PtcWindowLength = U96; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH
type MaxPayloadAttestations = U4;
type MaxBuildersPerWithdrawalsSweep = U16384;
@@ -561,6 +568,7 @@ impl EthSpec for MinimalEthSpec {
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 PtcWindowLength = U24; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH
type MaxBuildersPerWithdrawalsSweep = U16;
params_from_eth_spec!(MainnetEthSpec {
@@ -668,6 +676,7 @@ impl EthSpec for GnosisEthSpec {
type ProposerLookaheadSlots = U32; // Derived from (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH
type BuilderRegistryLimit = U1099511627776;
type PTCSize = U512;
type PtcWindowLength = U48; // (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH
type MaxPayloadAttestations = U2;
type MaxBuildersPerWithdrawalsSweep = U16384;
@@ -694,6 +703,11 @@ mod test {
E::proposer_lookahead_slots(),
(spec.min_seed_lookahead.as_usize() + 1) * E::slots_per_epoch() as usize
);
assert_eq!(
E::ptc_window_length(),
(spec.min_seed_lookahead.as_usize() + 2) * E::slots_per_epoch() as usize,
"PtcWindowLength must equal (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH"
);
}
#[test]

View File

@@ -667,6 +667,11 @@ where
#[superstruct(only(Gloas))]
pub payload_expected_withdrawals: List<Withdrawal, E::MaxWithdrawalsPerPayload>,
#[compare_fields(as_iter)]
#[test_random(default)]
#[superstruct(only(Gloas))]
pub ptc_window: Vector<FixedVector<u64, E::PTCSize>, E::PtcWindowLength>,
// Caching (not in the spec)
#[serde(skip_serializing, skip_deserializing)]
#[ssz(skip_serializing, skip_deserializing)]
@@ -2431,6 +2436,18 @@ impl<E: EthSpec> BeaconState<E> {
CommitteeCache::initialized(self, epoch, spec)
}
/// Like [`initialize_committee_cache`](Self::initialize_committee_cache), but allows epochs
/// beyond `current_epoch + 1`. Only checks that the required randao seed is available.
///
/// Used by PTC window computation which needs shufflings for lookahead epochs.
pub fn initialize_committee_cache_for_lookahead(
&self,
epoch: Epoch,
spec: &ChainSpec,
) -> Result<Arc<CommitteeCache>, BeaconStateError> {
CommitteeCache::initialized_for_lookahead(self, epoch, spec)
}
/// Advances the cache for this state into the next epoch.
///
/// This should be used if the `slot` of this state is advanced beyond an epoch boundary.
@@ -2501,6 +2518,17 @@ impl<E: EthSpec> BeaconState<E> {
.ok_or(BeaconStateError::CommitteeCachesOutOfBounds(index))
}
/// Set the committee cache for the given `relative_epoch` to `cache`.
pub fn set_committee_cache(
&mut self,
relative_epoch: RelativeEpoch,
cache: Arc<CommitteeCache>,
) -> Result<(), BeaconStateError> {
let i = Self::committee_cache_index(relative_epoch);
*self.committee_cache_at_index_mut(i)? = cache;
Ok(())
}
/// Returns the cache for some `RelativeEpoch`. Returns an error if the cache has not been
/// initialized.
pub fn committee_cache(
@@ -3084,12 +3112,55 @@ impl<E: EthSpec> BeaconState<E> {
}
}
/// Get the payload timeliness committee for the given `slot`.
///
/// Requires the committee cache to be initialized.
/// TODO(EIP-7732): definitely gonna have to cache this..
/// Get the payload timeliness committee for the given `slot` from the `ptc_window`.
pub fn get_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result<PTC<E>, BeaconStateError> {
let ptc_window = self.ptc_window()?;
let epoch = slot.epoch(E::slots_per_epoch());
let state_epoch = self.current_epoch();
let slots_per_epoch = E::slots_per_epoch() as usize;
let slot_in_epoch = slot.as_usize().safe_rem(slots_per_epoch)?;
let index = if epoch < state_epoch {
if epoch.safe_add(1)? != state_epoch {
return Err(BeaconStateError::SlotOutOfBounds);
}
slot_in_epoch
} else {
if epoch > state_epoch.safe_add(spec.min_seed_lookahead)? {
return Err(BeaconStateError::SlotOutOfBounds);
}
let offset = epoch
.safe_sub(state_epoch)?
.safe_add(1)?
.as_usize()
.safe_mul(slots_per_epoch)?;
offset.safe_add(slot_in_epoch)?
};
let entry = ptc_window
.get(index)
.ok_or(BeaconStateError::SlotOutOfBounds)?;
// Convert from FixedVector<u64, PTCSize> to PTC<E> (FixedVector<usize, PTCSize>)
let indices: Vec<usize> = entry.iter().map(|&v| v as usize).collect();
Ok(PTC(FixedVector::new(indices)?))
}
/// Compute the payload timeliness committee for the given `slot` from scratch.
///
/// Requires the committee cache to be initialized for the slot's epoch.
pub fn compute_ptc(&self, slot: Slot, spec: &ChainSpec) -> Result<PTC<E>, BeaconStateError> {
let committee_cache = self.committee_cache_at_slot(slot)?;
self.compute_ptc_with_cache(slot, committee_cache, spec)
}
/// Compute the PTC for a slot using a specific committee cache.
pub fn compute_ptc_with_cache(
&self,
slot: Slot,
committee_cache: &CommitteeCache,
spec: &ChainSpec,
) -> Result<PTC<E>, BeaconStateError> {
let committees = committee_cache.get_beacon_committees_at_slot(slot)?;
let seed = self.get_ptc_attester_seed(slot, spec)?;

View File

@@ -62,6 +62,9 @@ fn compare_shuffling_positions(xs: &Vec<NonZeroUsizeOption>, ys: &Vec<NonZeroUsi
impl CommitteeCache {
/// Return a new, fully initialized cache.
///
/// The epoch must be within the range that the state can service: historic epochs with
/// available randao data, up to `current_epoch + 1` (the "next" epoch).
///
/// Spec v0.12.1
pub fn initialized<E: EthSpec>(
state: &BeaconState<E>,
@@ -81,12 +84,44 @@ impl CommitteeCache {
|| epoch
> state
.current_epoch()
.safe_add(1)
.safe_add(1u64)
.map_err(BeaconStateError::ArithError)?
{
return Err(BeaconStateError::EpochOutOfBounds);
}
Self::initialized_unchecked(state, epoch, spec)
}
/// Return a new, fully initialized cache for a lookahead epoch.
///
/// Like [`initialized`](Self::initialized), but allows epochs beyond `current_epoch + 1`.
/// The only bound enforced is that the required randao seed is available in the state.
///
/// This is used by PTC window computation, which needs committee shufflings for
/// `current_epoch + 1 + MIN_SEED_LOOKAHEAD`.
pub fn initialized_for_lookahead<E: EthSpec>(
state: &BeaconState<E>,
epoch: Epoch,
spec: &ChainSpec,
) -> Result<Arc<CommitteeCache>, BeaconStateError> {
let reqd_randao_epoch = epoch
.saturating_sub(spec.min_seed_lookahead)
.saturating_sub(1u64);
if reqd_randao_epoch < state.min_randao_epoch() {
return Err(BeaconStateError::EpochOutOfBounds);
}
Self::initialized_unchecked(state, epoch, spec)
}
/// Core committee cache construction. Callers are responsible for bounds-checking `epoch`.
fn initialized_unchecked<E: EthSpec>(
state: &BeaconState<E>,
epoch: Epoch,
spec: &ChainSpec,
) -> Result<Arc<CommitteeCache>, BeaconStateError> {
// May cause divide-by-zero errors.
if E::slots_per_epoch() == 0 {
return Err(BeaconStateError::ZeroSlotsPerEpoch);