mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-27 17:53:42 +00:00
Merge branch 'gloas-walk-always' of https://github.com/sigp/lighthouse into gloas-fork-choice-fixes
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user