mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-25 16:58:28 +00:00
Merge branch 'gloas-walk-always' of https://github.com/sigp/lighthouse into gloas-fork-choice-fixes
This commit is contained in:
@@ -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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
227
consensus/types/configs/mainnet.yaml
Normal file
227
consensus/types/configs/mainnet.yaml
Normal 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
|
||||
220
consensus/types/configs/minimal.yaml
Normal file
220
consensus/types/configs/minimal.yaml
Normal 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: []
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user