mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-20 22:38:34 +00:00
Implement get_filtered_block_tree and fix remaining test failures
- Add get_filtered_block_tree/filter_block_tree matching the spec's recursive viability pre-filter for get_head - Skip invalid execution status nodes in the filter (they aren't in store.blocks in the spec) - Fix attestation_score for V17 nodes: fall back to weight() for Empty/Full since pre-Gloas has no payload separation - Include head_payload_status in ForkChoiceView so CachedHead updates when payload status changes - Update votes test: branch with incompatible finalized leaf is now correctly excluded by the recursive filter - Update execution_status test_03: stored weights no longer include proposer boost All 30 proto_array/fork_choice tests pass. All 9 EF fork_choice test suites pass.
This commit is contained in:
@@ -357,17 +357,18 @@ pub fn get_votes_test_definition() -> ForkChoiceTestDefinition {
|
||||
execution_payload_block_hash: None,
|
||||
});
|
||||
|
||||
// Ensure that 5 is filtered out and the head stays at 4.
|
||||
// Block 5 has incompatible finalized checkpoint, so `get_filtered_block_tree`
|
||||
// excludes the entire 1->3->4->5 branch (no viable leaf). Head moves to 2.
|
||||
//
|
||||
// 0
|
||||
// / \
|
||||
// 2 1
|
||||
// head-> 2 1
|
||||
// |
|
||||
// 3
|
||||
// |
|
||||
// 4 <- head
|
||||
// 4
|
||||
// /
|
||||
// 5
|
||||
// 5 <- incompatible finalized checkpoint
|
||||
ops.push(Operation::FindHead {
|
||||
justified_checkpoint: Checkpoint {
|
||||
epoch: Epoch::new(1),
|
||||
@@ -378,7 +379,7 @@ pub fn get_votes_test_definition() -> ForkChoiceTestDefinition {
|
||||
root: get_root(0),
|
||||
},
|
||||
justified_state_balances: balances.clone(),
|
||||
expected_head: get_root(4),
|
||||
expected_head: get_root(2),
|
||||
current_slot: Slot::new(0),
|
||||
expected_payload_status: None,
|
||||
});
|
||||
|
||||
@@ -193,8 +193,12 @@ impl ProtoNode {
|
||||
pub fn attestation_score(&self, payload_status: PayloadStatus) -> u64 {
|
||||
match payload_status {
|
||||
PayloadStatus::Pending => self.weight(),
|
||||
PayloadStatus::Empty => self.empty_payload_weight().unwrap_or(0),
|
||||
PayloadStatus::Full => self.full_payload_weight().unwrap_or(0),
|
||||
// Pre-Gloas (V17) nodes have no payload separation — all weight
|
||||
// is in `weight()`. Post-Gloas (V29) nodes track per-status weights.
|
||||
PayloadStatus::Empty => self
|
||||
.empty_payload_weight()
|
||||
.unwrap_or_else(|_| self.weight()),
|
||||
PayloadStatus::Full => self.full_payload_weight().unwrap_or_else(|_| self.weight()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,6 +1185,100 @@ impl ProtoArray {
|
||||
Ok((best_fc_node.root, best_fc_node.payload_status))
|
||||
}
|
||||
|
||||
/// Spec: `get_filtered_block_tree`.
|
||||
///
|
||||
/// Returns the set of node indices on viable branches — those with at least
|
||||
/// one leaf descendant with correct justified/finalized checkpoints.
|
||||
fn get_filtered_block_tree<E: EthSpec>(
|
||||
&self,
|
||||
start_index: usize,
|
||||
current_slot: Slot,
|
||||
best_justified_checkpoint: Checkpoint,
|
||||
best_finalized_checkpoint: Checkpoint,
|
||||
) -> HashSet<usize> {
|
||||
let mut viable = HashSet::new();
|
||||
self.filter_block_tree::<E>(
|
||||
start_index,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
best_finalized_checkpoint,
|
||||
&mut viable,
|
||||
);
|
||||
viable
|
||||
}
|
||||
|
||||
/// Spec: `filter_block_tree`.
|
||||
fn filter_block_tree<E: EthSpec>(
|
||||
&self,
|
||||
node_index: usize,
|
||||
current_slot: Slot,
|
||||
best_justified_checkpoint: Checkpoint,
|
||||
best_finalized_checkpoint: Checkpoint,
|
||||
viable: &mut HashSet<usize>,
|
||||
) -> bool {
|
||||
let Some(node) = self.nodes.get(node_index) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Nodes with invalid execution payloads are never viable.
|
||||
// (The spec doesn't need this check because invalid blocks aren't in store.blocks.)
|
||||
if node
|
||||
.execution_status()
|
||||
.is_ok_and(|status| status.is_invalid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip invalid children — they aren't in store.blocks in the spec.
|
||||
let children: Vec<usize> = self
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, child)| {
|
||||
child.parent() == Some(node_index)
|
||||
&& !child
|
||||
.execution_status()
|
||||
.is_ok_and(|status| status.is_invalid())
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if !children.is_empty() {
|
||||
// Evaluate ALL children (no short-circuit) to mark all viable branches.
|
||||
let any_viable = children
|
||||
.iter()
|
||||
.map(|&child_index| {
|
||||
self.filter_block_tree::<E>(
|
||||
child_index,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
best_finalized_checkpoint,
|
||||
viable,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.any(|v| v);
|
||||
if any_viable {
|
||||
viable.insert(node_index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Leaf node: check viability.
|
||||
if self.node_is_viable_for_head::<E>(
|
||||
node,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
best_finalized_checkpoint,
|
||||
) {
|
||||
viable.insert(node_index);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Spec: `get_head`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn find_head_walk<E: EthSpec>(
|
||||
@@ -1199,6 +1297,14 @@ impl ProtoArray {
|
||||
payload_status: PayloadStatus::Pending,
|
||||
};
|
||||
|
||||
// Spec: `get_filtered_block_tree`.
|
||||
let viable_nodes = self.get_filtered_block_tree::<E>(
|
||||
start_index,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
best_finalized_checkpoint,
|
||||
);
|
||||
|
||||
// Compute once rather than per-child per-level.
|
||||
let apply_proposer_boost =
|
||||
self.should_apply_proposer_boost::<E>(proposer_boost_root, justified_balances, spec)?;
|
||||
@@ -1207,17 +1313,7 @@ impl ProtoArray {
|
||||
let children: Vec<_> = self
|
||||
.get_node_children(&head)?
|
||||
.into_iter()
|
||||
.filter(|(_, proto_node)| {
|
||||
// Spec: `get_filtered_block_tree` pre-filters to only include
|
||||
// blocks on viable branches. We approximate this by checking
|
||||
// viability of each child during the walk.
|
||||
self.node_is_viable_for_head::<E>(
|
||||
proto_node,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
best_finalized_checkpoint,
|
||||
)
|
||||
})
|
||||
.filter(|(fc_node, _)| viable_nodes.contains(&fc_node.proto_node_index))
|
||||
.collect();
|
||||
|
||||
if children.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user