mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-22 15:28:28 +00:00
Gloas spec v1.7.0-alpha.5 and beacon_chain tests (#8998)
Fix database pruning post-Gloas - Fix DB pruning logic (and state summaries DAG) - Get the `beacon_chain` tests running with `FORK_NAME=gloas` 🎉 Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com> Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>
This commit is contained in:
@@ -109,6 +109,8 @@ pub fn get_gloas_chain_following_test_definition() -> ForkChoiceTestDefinition {
|
||||
pub fn get_gloas_payload_probe_test_definition() -> ForkChoiceTestDefinition {
|
||||
let mut ops = vec![];
|
||||
|
||||
// Block 1 at slot 1: child of genesis. Genesis has execution_payload_block_hash=zero
|
||||
// (no execution payload at genesis), so all children have parent_payload_status=Empty.
|
||||
ops.push(Operation::ProcessBlock {
|
||||
slot: Slot::new(1),
|
||||
root: get_root(1),
|
||||
@@ -212,8 +214,10 @@ pub fn get_gloas_payload_probe_test_definition() -> ForkChoiceTestDefinition {
|
||||
justified_checkpoint: get_checkpoint(0),
|
||||
finalized_checkpoint: get_checkpoint(0),
|
||||
operations: ops,
|
||||
execution_payload_parent_hash: Some(get_hash(42)),
|
||||
execution_payload_block_hash: Some(get_hash(0)),
|
||||
// Genesis has zero execution block hash (no payload at genesis), which
|
||||
// ensures all children get parent_payload_status=Empty.
|
||||
execution_payload_parent_hash: Some(ExecutionBlockHash::zero()),
|
||||
execution_payload_block_hash: Some(ExecutionBlockHash::zero()),
|
||||
spec: Some(gloas_spec()),
|
||||
}
|
||||
}
|
||||
@@ -600,18 +604,20 @@ pub fn get_gloas_interleaved_attestations_test_definition() -> ForkChoiceTestDef
|
||||
|
||||
/// Test interleaving of blocks, payload validation, and attestations.
|
||||
///
|
||||
/// Scenario:
|
||||
/// - Genesis block (slot 0)
|
||||
/// - Block 1 (slot 1) extends genesis, Full chain
|
||||
/// - Block 2 (slot 1) extends genesis, Empty chain
|
||||
/// - Before payload arrives: payload_received is false for block 1
|
||||
/// Scenario (branching at block 1 since genesis has no payload):
|
||||
/// - Genesis block (slot 0) with zero execution block hash
|
||||
/// - Block 1 (slot 1) child of genesis (Empty parent status since genesis hash=zero)
|
||||
/// - Block 2 (slot 2) extends block 1 Full chain (parent_hash matches block 1's block_hash)
|
||||
/// - Block 3 (slot 2) extends block 1 Empty chain (parent_hash doesn't match)
|
||||
/// - Before payload arrives: payload_received is false for block 1, only Empty reachable
|
||||
/// - Process execution payload for block 1 → payload_received becomes true
|
||||
/// - Payload attestations arrive voting block 1's payload as timely + available
|
||||
/// - Head should follow block 1 because the PTC votes now count (payload_received = true)
|
||||
/// - Both Full and Empty directions from block 1 become available
|
||||
/// - With equal weight, tiebreaker prefers Full → Block 2 wins
|
||||
pub fn get_gloas_payload_received_interleaving_test_definition() -> ForkChoiceTestDefinition {
|
||||
let mut ops = vec![];
|
||||
|
||||
// Block 1 at slot 1: extends genesis Full chain.
|
||||
// Block 1 at slot 1: child of genesis. Genesis has zero block hash, so
|
||||
// parent_payload_status = Empty regardless of block 1's execution_payload_parent_hash.
|
||||
ops.push(Operation::ProcessBlock {
|
||||
slot: Slot::new(1),
|
||||
root: get_root(1),
|
||||
@@ -622,83 +628,94 @@ pub fn get_gloas_payload_received_interleaving_test_definition() -> ForkChoiceTe
|
||||
execution_payload_block_hash: Some(get_hash(1)),
|
||||
});
|
||||
|
||||
// Block 2 at slot 1: extends genesis Empty chain (parent_hash doesn't match genesis EL hash).
|
||||
// Block 2 at slot 2: Full child of block 1 (parent_hash matches block 1's block_hash).
|
||||
ops.push(Operation::ProcessBlock {
|
||||
slot: Slot::new(1),
|
||||
slot: Slot::new(2),
|
||||
root: get_root(2),
|
||||
parent_root: get_root(0),
|
||||
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)),
|
||||
});
|
||||
|
||||
// Block 3 at slot 2: Empty child of block 1 (parent_hash doesn't match block 1's block_hash).
|
||||
ops.push(Operation::ProcessBlock {
|
||||
slot: Slot::new(2),
|
||||
root: get_root(3),
|
||||
parent_root: get_root(1),
|
||||
justified_checkpoint: get_checkpoint(0),
|
||||
finalized_checkpoint: get_checkpoint(0),
|
||||
execution_payload_parent_hash: Some(get_hash(99)),
|
||||
execution_payload_block_hash: Some(get_hash(100)),
|
||||
execution_payload_block_hash: Some(get_hash(3)),
|
||||
});
|
||||
|
||||
// Both children have parent_payload_status set correctly.
|
||||
// Verify parent_payload_status is set correctly.
|
||||
ops.push(Operation::AssertParentPayloadStatus {
|
||||
block_root: get_root(1),
|
||||
expected_status: PayloadStatus::Empty,
|
||||
});
|
||||
ops.push(Operation::AssertParentPayloadStatus {
|
||||
block_root: get_root(2),
|
||||
expected_status: PayloadStatus::Full,
|
||||
});
|
||||
ops.push(Operation::AssertParentPayloadStatus {
|
||||
block_root: get_root(2),
|
||||
block_root: get_root(3),
|
||||
expected_status: PayloadStatus::Empty,
|
||||
});
|
||||
|
||||
// Per spec `get_forkchoice_store`: genesis starts with payload_received=true
|
||||
// (anchor block is in `payload_states`).
|
||||
// Genesis does NOT have payload_received (no payload at genesis).
|
||||
ops.push(Operation::AssertPayloadReceived {
|
||||
block_root: get_root(0),
|
||||
expected: true,
|
||||
expected: false,
|
||||
});
|
||||
|
||||
// Give one vote to each child so they have equal weight.
|
||||
// Block 1 does not have payload_received yet.
|
||||
ops.push(Operation::AssertPayloadReceived {
|
||||
block_root: get_root(1),
|
||||
expected: false,
|
||||
});
|
||||
|
||||
// Give one vote to each competing child so they have equal weight.
|
||||
ops.push(Operation::ProcessAttestation {
|
||||
validator_index: 0,
|
||||
block_root: get_root(1),
|
||||
attestation_slot: Slot::new(1),
|
||||
block_root: get_root(2),
|
||||
attestation_slot: Slot::new(2),
|
||||
});
|
||||
ops.push(Operation::ProcessAttestation {
|
||||
validator_index: 1,
|
||||
block_root: get_root(2),
|
||||
attestation_slot: Slot::new(1),
|
||||
block_root: get_root(3),
|
||||
attestation_slot: Slot::new(2),
|
||||
});
|
||||
|
||||
// Equal weight, payload_received=true on genesis → tiebreaker uses
|
||||
// payload_received (not previous slot, equal payload weights) → prefers Full.
|
||||
// Block 1 (Full) wins because it matches the Full preference.
|
||||
// Before payload_received on block 1: only Empty direction available.
|
||||
// Block 3 (Empty child) is reachable, Block 2 (Full child) is not.
|
||||
ops.push(Operation::FindHead {
|
||||
justified_checkpoint: get_checkpoint(0),
|
||||
finalized_checkpoint: get_checkpoint(0),
|
||||
justified_state_balances: vec![1, 1],
|
||||
expected_head: get_root(1),
|
||||
expected_head: get_root(3),
|
||||
current_slot: Slot::new(100),
|
||||
expected_payload_status: None,
|
||||
});
|
||||
|
||||
// ProcessExecutionPayloadEnvelope on genesis is a no-op (already received at init).
|
||||
// Process execution payload envelope for block 1 → payload_received becomes true.
|
||||
ops.push(Operation::ProcessExecutionPayloadEnvelope {
|
||||
block_root: get_root(0),
|
||||
block_root: get_root(1),
|
||||
});
|
||||
|
||||
ops.push(Operation::AssertPayloadReceived {
|
||||
block_root: get_root(0),
|
||||
block_root: get_root(1),
|
||||
expected: true,
|
||||
});
|
||||
|
||||
// Set PTC votes on genesis as timely + data available (simulates PTC voting).
|
||||
// This doesn't change the preference since genesis is not the previous slot
|
||||
// (slot 0 + 1 != current_slot 100).
|
||||
ops.push(Operation::SetPayloadTiebreak {
|
||||
block_root: get_root(0),
|
||||
is_timely: true,
|
||||
is_data_available: true,
|
||||
});
|
||||
|
||||
// Still prefers Full via payload_received tiebreaker → Block 1 (Full) wins.
|
||||
// After payload_received on block 1: both Full and Empty directions available.
|
||||
// Equal weight, tiebreaker prefers Full → Block 2 (Full child) wins.
|
||||
ops.push(Operation::FindHead {
|
||||
justified_checkpoint: get_checkpoint(0),
|
||||
finalized_checkpoint: get_checkpoint(0),
|
||||
justified_state_balances: vec![1, 1],
|
||||
expected_head: get_root(1),
|
||||
expected_head: get_root(2),
|
||||
current_slot: Slot::new(100),
|
||||
expected_payload_status: None,
|
||||
});
|
||||
@@ -708,8 +725,9 @@ pub fn get_gloas_payload_received_interleaving_test_definition() -> ForkChoiceTe
|
||||
justified_checkpoint: get_checkpoint(0),
|
||||
finalized_checkpoint: get_checkpoint(0),
|
||||
operations: ops,
|
||||
execution_payload_parent_hash: Some(get_hash(42)),
|
||||
execution_payload_block_hash: Some(get_hash(0)),
|
||||
// Genesis has zero execution block hash (no payload at genesis).
|
||||
execution_payload_parent_hash: Some(ExecutionBlockHash::zero()),
|
||||
execution_payload_block_hash: Some(ExecutionBlockHash::zero()),
|
||||
spec: Some(gloas_spec()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,8 +568,10 @@ impl ProtoArray {
|
||||
ProtoNode::V29(v29) => {
|
||||
// Both parent and child are Gloas blocks. The parent is full if the
|
||||
// block hash in the parent node matches the parent block hash in the
|
||||
// child bid.
|
||||
if execution_payload_parent_hash == v29.execution_payload_block_hash {
|
||||
// child bid and the parent block isn't the genesis block.
|
||||
if v29.execution_payload_block_hash != ExecutionBlockHash::zero()
|
||||
&& execution_payload_parent_hash == v29.execution_payload_block_hash
|
||||
{
|
||||
PayloadStatus::Full
|
||||
} else {
|
||||
PayloadStatus::Empty
|
||||
@@ -582,18 +584,16 @@ impl ProtoArray {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
// Parent is missing (genesis or pruned due to finalization). This code path
|
||||
// should only be hit at Gloas genesis. Default to empty, the genesis block
|
||||
// has no payload enevelope.
|
||||
PayloadStatus::Empty
|
||||
};
|
||||
|
||||
// Per spec `get_forkchoice_store`: the anchor (genesis) block has
|
||||
// its payload state initialized (`payload_states = {anchor_root: ...}`).
|
||||
// Without `payload_received = true` on genesis, the FULL virtual
|
||||
// child doesn't exist in the spec's `get_node_children`, making all
|
||||
// Full concrete children of genesis unreachable in `get_head`.
|
||||
let is_genesis = parent_index.is_none();
|
||||
// The spec does something slightly strange where it initialises the payload timeliness
|
||||
// votes and payload data availability votes for the anchor block to all true, but never
|
||||
// adds the anchor to `store.payloads`, so it is never considered full.
|
||||
let is_anchor = parent_index.is_none();
|
||||
|
||||
ProtoNode::V29(ProtoNodeV29 {
|
||||
slot: block.slot,
|
||||
@@ -614,26 +614,25 @@ impl ProtoArray {
|
||||
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.
|
||||
payload_timeliness_votes: if is_genesis {
|
||||
// initialized to all-True.
|
||||
payload_timeliness_votes: if is_anchor {
|
||||
all_true_bitvector()
|
||||
} else {
|
||||
BitVector::default()
|
||||
},
|
||||
payload_data_availability_votes: if is_genesis {
|
||||
payload_data_availability_votes: if is_anchor {
|
||||
all_true_bitvector()
|
||||
} else {
|
||||
BitVector::default()
|
||||
},
|
||||
payload_received: is_genesis,
|
||||
payload_received: false,
|
||||
proposer_index,
|
||||
// Spec: `record_block_timeliness` + `get_forkchoice_store`.
|
||||
// Anchor gets [True, True]. Others computed from time_into_slot.
|
||||
block_timeliness_attestation_threshold: is_genesis
|
||||
block_timeliness_attestation_threshold: is_anchor
|
||||
|| (is_current_slot
|
||||
&& time_into_slot < spec.get_attestation_due::<E>(current_slot)),
|
||||
block_timeliness_ptc_threshold: is_genesis
|
||||
block_timeliness_ptc_threshold: is_anchor
|
||||
|| (is_current_slot && time_into_slot < spec.get_payload_attestation_due()),
|
||||
equivocating_attestation_score: 0,
|
||||
})
|
||||
@@ -1438,7 +1437,7 @@ impl ProtoArray {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_extend_payload<E: EthSpec>(
|
||||
pub fn should_extend_payload<E: EthSpec>(
|
||||
&self,
|
||||
fc_node: &IndexedForkChoiceNode,
|
||||
proto_node: &ProtoNode,
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{
|
||||
};
|
||||
use types::{
|
||||
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
|
||||
Slot, StatePayloadStatus,
|
||||
Slot,
|
||||
};
|
||||
|
||||
pub const DEFAULT_PRUNE_THRESHOLD: usize = 256;
|
||||
@@ -110,19 +110,6 @@ pub enum PayloadStatus {
|
||||
Pending = 2,
|
||||
}
|
||||
|
||||
impl PayloadStatus {
|
||||
/// Convert a `PayloadStatus` into the equivalent `StatePayloadStatus`.
|
||||
///
|
||||
/// This maps `Empty` onto `StatePayloadStatus::Pending` because empty and pending fork choice
|
||||
/// nodes correspond to the exact same state.
|
||||
pub fn as_state_payload_status(self) -> StatePayloadStatus {
|
||||
match self {
|
||||
Self::Empty | Self::Pending => StatePayloadStatus::Pending,
|
||||
Self::Full => StatePayloadStatus::Full,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec's `ForkChoiceNode` augmented with ProtoNode index.
|
||||
pub struct IndexedForkChoiceNode {
|
||||
pub root: Hash256,
|
||||
@@ -1019,6 +1006,34 @@ impl ProtoArrayForkChoice {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether the proposer should extend the parent's execution payload chain.
|
||||
///
|
||||
/// This checks timeliness, data availability, and proposer boost conditions per the spec.
|
||||
pub fn should_extend_payload<E: EthSpec>(
|
||||
&self,
|
||||
block_root: &Hash256,
|
||||
proposer_boost_root: Hash256,
|
||||
) -> Result<bool, String> {
|
||||
let block_index = self
|
||||
.proto_array
|
||||
.indices
|
||||
.get(block_root)
|
||||
.ok_or_else(|| format!("Unknown block root: {block_root:?}"))?;
|
||||
let proto_node = self
|
||||
.proto_array
|
||||
.nodes
|
||||
.get(*block_index)
|
||||
.ok_or_else(|| format!("Missing node at index: {block_index}"))?;
|
||||
let fc_node = IndexedForkChoiceNode {
|
||||
root: proto_node.root(),
|
||||
proto_node_index: *block_index,
|
||||
payload_status: proto_node.get_parent_payload_status(),
|
||||
};
|
||||
self.proto_array
|
||||
.should_extend_payload::<E>(&fc_node, proto_node, proposer_boost_root)
|
||||
.map_err(|e| format!("{e:?}"))
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
|
||||
Reference in New Issue
Block a user