Align GLOAS fork choice with spec

- Move proposer boost from apply_score_changes to get_weight, matching
  the spec's structure where get_weight adds boost via is_supporting_vote
- Implement is_supporting_vote and get_ancestor_node spec functions
- Fix should_extend_payload: return true when proposer_boost_root is zero
- Compute record_block_timeliness from time_into_slot instead of hardcoding false
- Fix anchor block_timeliness to [true, true] per get_forkchoice_store spec
- Add equivocating_attestation_score for is_head_weak monotonicity
- Use payload-aware weight in is_parent_strong
- Add with_status helper on IndexedForkChoiceNode
- Simplify find_head_walk to return IndexedForkChoiceNode directly
This commit is contained in:
dapplion
2026-03-25 02:59:50 -05:00
parent cec5ce179d
commit 845831ce56
4 changed files with 241 additions and 204 deletions

View File

@@ -797,6 +797,11 @@ where
let attestation_threshold = spec.get_unaggregated_attestation_due(); let attestation_threshold = spec.get_unaggregated_attestation_due();
// Add proposer score boost if the block is timely. // Add proposer score boost if the block is timely.
// TODO(gloas): the spec's `update_proposer_boost_root` additionally checks that
// `block.proposer_index == get_beacon_proposer_index(head_state)` — i.e. that
// the block's proposer matches the expected proposer on the canonical chain.
// This requires calling `get_head` and advancing the head state to the current
// slot, which is expensive. Implement once we have a cached proposer index.
let is_before_attesting_interval = block_delay < attestation_threshold; let is_before_attesting_interval = block_delay < attestation_threshold;
let is_first_block = self.fc_store.proposer_boost_root().is_zero(); let is_first_block = self.fc_store.proposer_boost_root().is_zero();
@@ -1001,6 +1006,7 @@ where
self.justified_checkpoint(), self.justified_checkpoint(),
self.finalized_checkpoint(), self.finalized_checkpoint(),
spec, spec,
block_delay,
)?; )?;
Ok(()) Ok(())

View File

@@ -10,6 +10,7 @@ use fixed_bytes::FixedBytesExtended;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssz::BitVector; use ssz::BitVector;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::time::Duration;
use types::{ use types::{
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
MainnetEthSpec, Slot, MainnetEthSpec, Slot,
@@ -288,6 +289,7 @@ impl ForkChoiceTestDefinition {
self.justified_checkpoint, self.justified_checkpoint,
self.finalized_checkpoint, self.finalized_checkpoint,
&spec, &spec,
Duration::ZERO,
) )
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!( panic!(

View File

@@ -1,6 +1,8 @@
use crate::error::InvalidBestNodeInfo; use crate::error::InvalidBestNodeInfo;
use crate::proto_array_fork_choice::IndexedForkChoiceNode; use crate::proto_array_fork_choice::IndexedForkChoiceNode;
use crate::{Block, ExecutionStatus, JustifiedBalances, PayloadStatus, error::Error}; use crate::{
Block, ExecutionStatus, JustifiedBalances, LatestMessage, PayloadStatus, error::Error,
};
use fixed_bytes::FixedBytesExtended; use fixed_bytes::FixedBytesExtended;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ssz::BitVector; use ssz::BitVector;
@@ -8,6 +10,7 @@ use ssz::Encode;
use ssz::four_byte_option_impl; use ssz::four_byte_option_impl;
use ssz_derive::{Decode, Encode}; use ssz_derive::{Decode, Encode};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::time::Duration;
use superstruct::superstruct; use superstruct::superstruct;
use typenum::U512; use typenum::U512;
use types::{ use types::{
@@ -20,6 +23,14 @@ use types::{
four_byte_option_impl!(four_byte_option_usize, usize); four_byte_option_impl!(four_byte_option_usize, usize);
four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint); four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint);
fn all_true_bitvector<N: typenum::Unsigned + Clone>() -> BitVector<N> {
let mut bv = BitVector::new();
for i in 0..bv.len() {
let _ = bv.set(i, true);
}
bv
}
/// Defines an operation which may invalidate the `execution_status` of some nodes. /// Defines an operation which may invalidate the `execution_status` of some nodes.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum InvalidationOperation { pub enum InvalidationOperation {
@@ -160,22 +171,27 @@ pub struct ProtoNode {
/// to detect equivocations at the parent's slot. /// to detect equivocations at the parent's slot.
#[superstruct(only(V29), partial_getter(copy))] #[superstruct(only(V29), partial_getter(copy))]
pub proposer_index: u64, pub proposer_index: u64,
/// Weight from equivocating validators that voted for this block.
/// Used by `is_head_weak` to match the spec's monotonicity guarantee:
/// more attestations can only increase head weight, never decrease it.
#[superstruct(only(V29), partial_getter(copy))]
pub equivocating_attestation_score: u64,
} }
impl ProtoNode { impl ProtoNode {
/// Generic version of spec's `parent_payload_status` that works for pre-Gloas nodes by /// Generic version of spec's `parent_payload_status` that works for pre-Gloas nodes by
/// considering their parents Empty. /// considering their parents Empty.
fn get_parent_payload_status(&self) -> PayloadStatus { /// 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) self.parent_payload_status().unwrap_or(PayloadStatus::Empty)
} }
fn is_parent_node_full(&self) -> bool { pub fn is_parent_node_full(&self) -> bool {
self.get_parent_payload_status() == PayloadStatus::Full self.get_parent_payload_status() == PayloadStatus::Full
} }
fn attestation_score(&self, payload_status: PayloadStatus) -> u64 { pub fn attestation_score(&self, payload_status: PayloadStatus) -> u64 {
match payload_status { match payload_status {
// TODO(gloas): rename weight and remove proposer boost from it?
PayloadStatus::Pending => self.weight(), PayloadStatus::Pending => self.weight(),
PayloadStatus::Empty => self.empty_payload_weight().unwrap_or(0), PayloadStatus::Empty => self.empty_payload_weight().unwrap_or(0),
PayloadStatus::Full => self.full_payload_weight().unwrap_or(0), PayloadStatus::Full => self.full_payload_weight().unwrap_or(0),
@@ -187,8 +203,7 @@ impl ProtoNode {
return false; return false;
}; };
// If the payload is not locally available, the payload // Equivalent to `if root not in store.payload_states` in the spec.
// is not considered available regardless of the PTC vote
if !node.payload_received { if !node.payload_received {
return false; return false;
} }
@@ -201,8 +216,7 @@ impl ProtoNode {
return false; return false;
}; };
// If the payload is not locally available, the payload // Equivalent to `if root not in store.payload_states` in the spec.
// is not considered available regardless of the PTC vote
if !node.payload_received { if !node.payload_received {
return false; return false;
} }
@@ -252,6 +266,8 @@ pub struct NodeDelta {
pub empty_delta: i64, pub empty_delta: i64,
/// Weight change from `PayloadStatus::Full` votes. /// Weight change from `PayloadStatus::Full` votes.
pub full_delta: i64, pub full_delta: i64,
/// Weight from equivocating validators that voted for this node.
pub equivocating_attestation_delta: u64,
} }
impl NodeDelta { impl NodeDelta {
@@ -308,6 +324,7 @@ impl NodeDelta {
delta, delta,
empty_delta: 0, empty_delta: 0,
full_delta: 0, full_delta: 0,
equivocating_attestation_delta: 0,
} }
} }
@@ -370,10 +387,10 @@ impl ProtoArray {
mut deltas: Vec<NodeDelta>, mut deltas: Vec<NodeDelta>,
best_justified_checkpoint: Checkpoint, best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint,
new_justified_balances: &JustifiedBalances, _new_justified_balances: &JustifiedBalances,
proposer_boost_root: Hash256, _proposer_boost_root: Hash256,
current_slot: Slot, current_slot: Slot,
spec: &ChainSpec, _spec: &ChainSpec,
) -> Result<(), Error> { ) -> Result<(), Error> {
if deltas.len() != self.indices.len() { if deltas.len() != self.indices.len() {
return Err(Error::InvalidDeltaLen { return Err(Error::InvalidDeltaLen {
@@ -382,9 +399,6 @@ impl ProtoArray {
}); });
} }
// Default the proposer boost score to zero.
let mut proposer_score = 0;
// Iterate backwards through all indices in `self.nodes`. // Iterate backwards through all indices in `self.nodes`.
for node_index in (0..self.nodes.len()).rev() { for node_index in (0..self.nodes.len()).rev() {
let node = self let node = self
@@ -412,7 +426,7 @@ impl ProtoArray {
.copied() .copied()
.ok_or(Error::InvalidNodeDelta(node_index))?; .ok_or(Error::InvalidNodeDelta(node_index))?;
let mut delta = if execution_status_is_invalid { let delta = if execution_status_is_invalid {
// If the node has an invalid execution payload, reduce its weight to zero. // If the node has an invalid execution payload, reduce its weight to zero.
0_i64 0_i64
.checked_sub(node.weight() as i64) .checked_sub(node.weight() as i64)
@@ -427,37 +441,9 @@ impl ProtoArray {
(0, 0) (0, 0)
}; };
// If we find the node for which the proposer boost was previously applied, decrease // Proposer boost is NOT applied here. It is computed on-the-fly
// the delta by the previous score amount. // during the virtual tree walk in `get_weight`, matching the spec's
// TODO(gloas): implement `should_apply_proposer_boost` from the Gloas spec. // `get_weight` which adds boost separately from `get_attestation_score`.
// The spec conditionally applies proposer boost based on parent weakness and
// early equivocations. Currently boost is applied unconditionally.
if self.previous_proposer_boost.root != Hash256::zero()
&& self.previous_proposer_boost.root == node.root()
// Invalid nodes will always have a weight of zero so there's no need to subtract
// the proposer boost delta.
&& !execution_status_is_invalid
{
delta = delta
.checked_sub(self.previous_proposer_boost.score as i64)
.ok_or(Error::DeltaOverflow(node_index))?;
}
// If we find the node matching the current proposer boost root, increase
// the delta by the new score amount (unless the block has an invalid execution status).
// For Gloas (V29), `should_apply_proposer_boost` is checked after the loop
// with final weights, and the boost is removed if needed.
if let Some(proposer_score_boost) = spec.proposer_score_boost
&& proposer_boost_root != Hash256::zero()
&& proposer_boost_root == node.root()
&& !execution_status_is_invalid
{
proposer_score =
calculate_committee_fraction::<E>(new_justified_balances, proposer_score_boost)
.ok_or(Error::ProposerBoostOverflow(node_index))?;
delta = delta
.checked_add(proposer_score as i64)
.ok_or(Error::DeltaOverflow(node_index))?;
}
// Apply the delta to the node. // Apply the delta to the node.
if execution_status_is_invalid { if execution_status_is_invalid {
@@ -473,6 +459,9 @@ impl ProtoArray {
apply_delta(node.empty_payload_weight, node_empty_delta, node_index)?; apply_delta(node.empty_payload_weight, node_empty_delta, node_index)?;
node.full_payload_weight = node.full_payload_weight =
apply_delta(node.full_payload_weight, node_full_delta, node_index)?; apply_delta(node.full_payload_weight, node_full_delta, node_index)?;
node.equivocating_attestation_score = node
.equivocating_attestation_score
.saturating_add(node_delta.equivocating_attestation_delta);
} }
// Update the parent delta (if any). // Update the parent delta (if any).
@@ -514,67 +503,6 @@ impl ProtoArray {
} }
} }
// Gloas: now that all weights are final, check `should_apply_proposer_boost`.
// If the boost should NOT apply, walk from the boosted node to root and subtract
// `proposer_score` from weight and payload weights in a single pass.
// We detect Gloas by checking the boosted node's variant (V29) directly.
if proposer_score > 0
&& let Some(&boost_index) = self.indices.get(&proposer_boost_root)
&& self
.nodes
.get(boost_index)
.is_some_and(|n| n.as_v29().is_ok())
&& !self.should_apply_proposer_boost::<E>(
proposer_boost_root,
new_justified_balances,
spec,
)?
{
// Single walk: subtract proposer_score from weight and payload weights.
let mut walk_index = Some(boost_index);
let mut child_payload_status: Option<PayloadStatus> = None;
while let Some(idx) = walk_index {
let node = self
.nodes
.get_mut(idx)
.ok_or(Error::InvalidNodeIndex(idx))?;
*node.weight_mut() = node
.weight()
.checked_sub(proposer_score)
.ok_or(Error::DeltaOverflow(idx))?;
// Subtract from the payload bucket that the child-on-path
// contributed to (based on the child's parent_payload_status).
if let Some(child_ps) = child_payload_status
&& let Ok(v29) = node.as_v29_mut()
{
if child_ps == PayloadStatus::Full {
v29.full_payload_weight = v29
.full_payload_weight
.checked_sub(proposer_score)
.ok_or(Error::DeltaOverflow(idx))?;
} else {
v29.empty_payload_weight = v29
.empty_payload_weight
.checked_sub(proposer_score)
.ok_or(Error::DeltaOverflow(idx))?;
}
}
child_payload_status = node.parent_payload_status().ok();
walk_index = node.parent();
}
proposer_score = 0;
}
// After applying all deltas, update the `previous_proposer_boost`.
self.previous_proposer_boost = ProposerBoost {
root: proposer_boost_root,
score: proposer_score,
};
// A second time, iterate backwards through all indices in `self.nodes`. // A second time, iterate backwards through all indices in `self.nodes`.
// //
// We _must_ perform these functions separate from the weight-updating loop above to ensure // We _must_ perform these functions separate from the weight-updating loop above to ensure
@@ -611,6 +539,7 @@ impl ProtoArray {
best_justified_checkpoint: Checkpoint, best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint,
spec: &ChainSpec, spec: &ChainSpec,
time_into_slot: Duration,
) -> Result<(), Error> { ) -> Result<(), Error> {
// If the block is already known, simply ignore it. // If the block is already known, simply ignore it.
if self.indices.contains_key(&block.root) { if self.indices.contains_key(&block.root) {
@@ -642,6 +571,8 @@ impl ProtoArray {
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint, unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
}) })
} else { } else {
let is_current_slot = current_slot == block.slot;
let execution_payload_block_hash = let execution_payload_block_hash =
block block
.execution_payload_block_hash .execution_payload_block_hash
@@ -712,28 +643,27 @@ impl ProtoArray {
// initialized to all-True, ensuring `is_payload_timely` and // initialized to all-True, ensuring `is_payload_timely` and
// `is_payload_data_available` return true for the anchor. // `is_payload_data_available` return true for the anchor.
payload_timeliness_votes: if is_genesis { payload_timeliness_votes: if is_genesis {
let mut bv = BitVector::new(); all_true_bitvector()
for i in 0..bv.len() {
let _ = bv.set(i, true);
}
bv
} else { } else {
BitVector::default() BitVector::default()
}, },
payload_data_availability_votes: if is_genesis { payload_data_availability_votes: if is_genesis {
let mut bv = BitVector::new(); all_true_bitvector()
for i in 0..bv.len() {
let _ = bv.set(i, true);
}
bv
} else { } else {
BitVector::default() BitVector::default()
}, },
payload_received: is_genesis, payload_received: is_genesis,
proposer_index: block.proposer_index.unwrap_or(0), proposer_index: block.proposer_index.unwrap_or(0),
// TODO(gloas): initialise these based on block timing // Spec: `record_block_timeliness` + `get_forkchoice_store`.
block_timeliness_attestation_threshold: false, // Anchor gets [True, True]. Others computed from time_into_slot.
block_timeliness_ptc_threshold: false, 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
// `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),
equivocating_attestation_score: 0,
}) })
}; };
@@ -776,6 +706,12 @@ impl ProtoArray {
Ok(()) Ok(())
} }
/// Spec: `is_head_weak`.
///
/// The spec adds weight from equivocating validators in the head slot's
/// committees. We approximate this with `equivocating_attestation_score`
/// which tracks equivocating validators that voted for this block (close
/// but not identical to committee membership).
fn is_head_weak<E: EthSpec>( fn is_head_weak<E: EthSpec>(
&self, &self,
head_node: &ProtoNode, head_node: &ProtoNode,
@@ -788,11 +724,10 @@ impl ProtoArray {
) )
.unwrap_or(0); .unwrap_or(0);
let head_weight = head_node.attestation_score(PayloadStatus::Pending); let head_weight = head_node
.attestation_score(PayloadStatus::Pending)
.saturating_add(head_node.equivocating_attestation_score().unwrap_or(0));
// TODO(gloas): missing equivocating weight from spec
// idea: add equivocating_attestation_score on the proto node that is updated whenever
// an equivocation is processed.
head_weight < reorg_threshold head_weight < reorg_threshold
} }
@@ -1207,7 +1142,7 @@ impl ProtoArray {
// In the post-Gloas world, always use a virtual tree walk. // In the post-Gloas world, always use a virtual tree walk.
// //
// Best child/best descendant is dead. // Best child/best descendant is dead.
let (best_fc_node, best_node) = self.find_head_walk::<E>( let best_fc_node = self.find_head_walk::<E>(
justified_index, justified_index,
current_slot, current_slot,
best_justified_checkpoint, best_justified_checkpoint,
@@ -1218,8 +1153,12 @@ impl ProtoArray {
)?; )?;
// Perform a sanity check that the node is indeed valid to be the head. // Perform a sanity check that the node is indeed valid to be the head.
let best_node = self
.nodes
.get(best_fc_node.proto_node_index)
.ok_or(Error::InvalidNodeIndex(best_fc_node.proto_node_index))?;
if !self.node_is_viable_for_head::<E>( if !self.node_is_viable_for_head::<E>(
&best_node, best_node,
current_slot, current_slot,
best_justified_checkpoint, best_justified_checkpoint,
best_finalized_checkpoint, best_finalized_checkpoint,
@@ -1238,80 +1177,79 @@ impl ProtoArray {
Ok((best_fc_node.root, best_fc_node.payload_status)) Ok((best_fc_node.root, best_fc_node.payload_status))
} }
/// Virtual tree walk for `find_head`. /// Spec: `get_head`.
///
/// At each node, determine the preferred payload direction (FULL or EMPTY)
/// by comparing weights. Scan all nodes to find the best child matching
/// the preferred direction.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn find_head_walk<E: EthSpec>( fn find_head_walk<E: EthSpec>(
&self, &self,
start_index: usize, start_index: usize,
current_slot: Slot, current_slot: Slot,
best_justified_checkpoint: Checkpoint, best_justified_checkpoint: Checkpoint,
_best_finalized_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint,
proposer_boost_root: Hash256, proposer_boost_root: Hash256,
justified_balances: &JustifiedBalances, justified_balances: &JustifiedBalances,
spec: &ChainSpec, spec: &ChainSpec,
) -> Result<(IndexedForkChoiceNode, ProtoNode), Error> { ) -> Result<IndexedForkChoiceNode, Error> {
let mut head = IndexedForkChoiceNode { let mut head = IndexedForkChoiceNode {
root: best_justified_checkpoint.root, root: best_justified_checkpoint.root,
proto_node_index: start_index, proto_node_index: start_index,
payload_status: PayloadStatus::Pending, payload_status: PayloadStatus::Pending,
}; };
let mut head_proto_node = self
.nodes
.get(start_index)
.ok_or(Error::NodeUnknown(best_justified_checkpoint.root))?
.clone();
loop { loop {
let children = self.get_node_children(&head)?; 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,
)
})
.collect();
if children.is_empty() { if children.is_empty() {
break; return Ok(head);
} }
let scores = children head = children
.into_iter() .into_iter()
.map(|(child_fc_node, child_proto_node)| { .map(|(child, _)| -> Result<_, Error> {
let proto_node = self
.nodes
.get(child.proto_node_index)
.ok_or(Error::InvalidNodeIndex(child.proto_node_index))?;
let weight = self.get_weight::<E>( let weight = self.get_weight::<E>(
&child_fc_node, &child,
&child_proto_node, proto_node,
proposer_boost_root, proposer_boost_root,
current_slot, current_slot,
justified_balances, justified_balances,
spec, spec,
)?; )?;
let payload_status_tiebreaker = self.get_payload_status_tiebreaker::<E>( let payload_status_tiebreaker = self.get_payload_status_tiebreaker::<E>(
&child_fc_node, &child,
&child_proto_node, proto_node,
current_slot, current_slot,
proposer_boost_root, proposer_boost_root,
)?; )?;
Ok(( Ok((child, weight, payload_status_tiebreaker))
child_fc_node,
child_proto_node,
weight,
payload_status_tiebreaker,
))
}) })
.collect::<Result<Vec<_>, Error>>()?; .collect::<Result<Vec<_>, Error>>()?
// TODO(gloas): proper error
(head, head_proto_node) = scores
.into_iter() .into_iter()
.max_by_key( .max_by_key(|(child, weight, payload_status_tiebreaker)| {
|(child_fc_node, _proto_node, weight, payload_status_tiebreaker)| { (*weight, child.root, *payload_status_tiebreaker)
(*weight, child_fc_node.root, *payload_status_tiebreaker) })
}, .map(|(child, _, _)| child)
) .expect("children is non-empty");
.map(|(child_fc_node, child_proto_node, _, _)| (child_fc_node, child_proto_node)) }
.unwrap();
}
Ok((head, head_proto_node))
} }
/// Spec: `get_weight`.
fn get_weight<E: EthSpec>( fn get_weight<E: EthSpec>(
&self, &self,
fc_node: &IndexedForkChoiceNode, fc_node: &IndexedForkChoiceNode,
@@ -1334,19 +1272,99 @@ impl ProtoArray {
return Ok(attestation_score); return Ok(attestation_score);
} }
// TODO(gloas): I don't think `is_supporting_vote` is necessary here, confirm by // Spec: proposer boost is treated as a synthetic vote.
// checking spec tests or with spec authors. let message = LatestMessage {
let proposer_score = if proto_node.root() == proposer_boost_root { slot: current_slot,
root: proposer_boost_root,
payload_present: false,
};
let proposer_score = if self.is_supporting_vote(fc_node, &message)? {
get_proposer_score::<E>(justified_balances, spec)? get_proposer_score::<E>(justified_balances, spec)?
} else { } else {
0 0
}; };
Ok(attestation_score.saturating_add(proposer_score)) Ok(attestation_score.saturating_add(proposer_score))
} else { } else {
Ok(0) Ok(0)
} }
} }
/// Spec: `is_supporting_vote`.
fn is_supporting_vote(
&self,
node: &IndexedForkChoiceNode,
message: &LatestMessage,
) -> Result<bool, Error> {
let block = self
.nodes
.get(node.proto_node_index)
.ok_or(Error::InvalidNodeIndex(node.proto_node_index))?;
if node.root == message.root {
if node.payload_status == PayloadStatus::Pending {
return Ok(true);
}
if message.slot <= block.slot() {
return Ok(false);
}
if message.payload_present {
Ok(node.payload_status == PayloadStatus::Full)
} else {
Ok(node.payload_status == PayloadStatus::Empty)
}
} else {
let ancestor = self.get_ancestor_node(message.root, block.slot())?;
Ok(node.root == ancestor.root
&& (node.payload_status == PayloadStatus::Pending
|| node.payload_status == ancestor.payload_status))
}
}
/// Spec: `get_ancestor` (modified to return ForkChoiceNode with payload_status).
fn get_ancestor_node(&self, root: Hash256, slot: Slot) -> Result<IndexedForkChoiceNode, Error> {
let index = *self.indices.get(&root).ok_or(Error::NodeUnknown(root))?;
let block = self
.nodes
.get(index)
.ok_or(Error::InvalidNodeIndex(index))?;
if block.slot() <= slot {
return Ok(IndexedForkChoiceNode {
root,
proto_node_index: index,
payload_status: PayloadStatus::Pending,
});
}
// Walk up until we find the ancestor at `slot`.
let mut child_index = index;
let mut current_index = block.parent().ok_or(Error::NodeUnknown(root))?;
loop {
let current = self
.nodes
.get(current_index)
.ok_or(Error::InvalidNodeIndex(current_index))?;
if current.slot() <= slot {
let child = self
.nodes
.get(child_index)
.ok_or(Error::InvalidNodeIndex(child_index))?;
return Ok(IndexedForkChoiceNode {
root: current.root(),
proto_node_index: current_index,
payload_status: child.get_parent_payload_status(),
});
}
child_index = current_index;
current_index = current.parent().ok_or(Error::NodeUnknown(root))?;
}
}
/// Spec: `get_node_children`.
fn get_node_children( fn get_node_children(
&self, &self,
node: &IndexedForkChoiceNode, node: &IndexedForkChoiceNode,
@@ -1355,30 +1373,15 @@ impl ProtoArray {
let proto_node = self let proto_node = self
.nodes .nodes
.get(node.proto_node_index) .get(node.proto_node_index)
.ok_or(Error::InvalidNodeIndex(node.proto_node_index))? .ok_or(Error::InvalidNodeIndex(node.proto_node_index))?;
.clone(); let mut children = vec![(node.with_status(PayloadStatus::Empty), proto_node.clone())];
let mut children = vec![(
IndexedForkChoiceNode {
root: node.root,
proto_node_index: node.proto_node_index,
payload_status: PayloadStatus::Empty,
},
proto_node.clone(),
)];
// The FULL virtual child only exists if the payload has been received. // The FULL virtual child only exists if the payload has been received.
if proto_node.payload_received().is_ok_and(|received| received) { if proto_node.payload_received().is_ok_and(|received| received) {
children.push(( children.push((node.with_status(PayloadStatus::Full), proto_node.clone()));
IndexedForkChoiceNode {
root: node.root,
proto_node_index: node.proto_node_index,
payload_status: PayloadStatus::Full,
},
proto_node,
));
} }
Ok(children) Ok(children)
} else { } else {
let children = self Ok(self
.nodes .nodes
.iter() .iter()
.enumerate() .enumerate()
@@ -1396,8 +1399,7 @@ impl ProtoArray {
child_node.clone(), child_node.clone(),
) )
}) })
.collect(); .collect())
Ok(children)
} }
} }
@@ -1427,8 +1429,10 @@ impl ProtoArray {
proto_node: &ProtoNode, proto_node: &ProtoNode,
proposer_boost_root: Hash256, proposer_boost_root: Hash256,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
// Per spec: `proposer_root == Root()` is one of the `or` conditions that
// makes `should_extend_payload` return True.
if proposer_boost_root.is_zero() { if proposer_boost_root.is_zero() {
return Ok(false); return Ok(true);
} }
let proposer_boost_node_index = *self let proposer_boost_node_index = *self
@@ -1440,20 +1444,18 @@ impl ProtoArray {
.get(proposer_boost_node_index) .get(proposer_boost_node_index)
.ok_or(Error::InvalidNodeIndex(proposer_boost_node_index))?; .ok_or(Error::InvalidNodeIndex(proposer_boost_node_index))?;
// Check if the parent of the proposer boost node matches the fc_node's root let parent_index = proposer_boost_node
let Some(proposer_boost_parent_index) = proposer_boost_node.parent() else { .parent()
// TODO(gloas): could be an error .ok_or(Error::NodeUnknown(proposer_boost_root))?;
return Ok(false); let proposer_boost_parent_root = self
};
let boost_parent_root = self
.nodes .nodes
.get(proposer_boost_parent_index) .get(parent_index)
.ok_or(Error::InvalidNodeIndex(proposer_boost_parent_index))? .ok_or(Error::InvalidNodeIndex(parent_index))?
.root(); .root();
Ok( Ok(
(proto_node.is_payload_timely::<E>() && proto_node.is_payload_data_available::<E>()) (proto_node.is_payload_timely::<E>() && proto_node.is_payload_data_available::<E>())
|| boost_parent_root != fc_node.root || proposer_boost_parent_root != fc_node.root
|| proposer_boost_node.is_parent_node_full(), || proposer_boost_node.is_parent_node_full(),
) )
} }
@@ -1879,15 +1881,14 @@ pub fn calculate_committee_fraction<E: EthSpec>(
.checked_div(100) .checked_div(100)
} }
pub fn get_proposer_score<E: EthSpec>( /// Spec: `get_proposer_score`.
fn get_proposer_score<E: EthSpec>(
justified_balances: &JustifiedBalances, justified_balances: &JustifiedBalances,
spec: &ChainSpec, spec: &ChainSpec,
) -> Result<u64, Error> { ) -> Result<u64, Error> {
let Some(proposer_score_boost) = spec.proposer_score_boost else { let Some(proposer_score_boost) = spec.proposer_score_boost else {
// TODO(gloas): make proposer boost non-optional in spec
return Ok(0); return Ok(0);
}; };
// TODO(gloas): fix error
calculate_committee_fraction::<E>(justified_balances, proposer_score_boost) calculate_committee_fraction::<E>(justified_balances, proposer_score_boost)
.ok_or(Error::ProposerBoostOverflow(0)) .ok_or(Error::ProposerBoostOverflow(0))
} }

View File

@@ -14,6 +14,7 @@ use ssz_derive::{Decode, Encode};
use std::{ use std::{
collections::{BTreeSet, HashMap}, collections::{BTreeSet, HashMap},
fmt, fmt,
time::Duration,
}; };
use types::{ use types::{
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
@@ -75,6 +76,16 @@ pub struct IndexedForkChoiceNode {
pub payload_status: PayloadStatus, pub payload_status: PayloadStatus,
} }
impl IndexedForkChoiceNode {
pub fn with_status(&self, payload_status: PayloadStatus) -> Self {
Self {
root: self.root,
proto_node_index: self.proto_node_index,
payload_status,
}
}
}
impl ExecutionStatus { impl ExecutionStatus {
pub fn is_execution_enabled(&self) -> bool { pub fn is_execution_enabled(&self) -> bool {
!matches!(self, ExecutionStatus::Irrelevant(_)) !matches!(self, ExecutionStatus::Irrelevant(_))
@@ -491,6 +502,10 @@ impl ProtoArrayForkChoice {
justified_checkpoint, justified_checkpoint,
finalized_checkpoint, finalized_checkpoint,
spec, spec,
// Anchor block is always timely (delay=0 ensures both timeliness
// checks pass). Combined with `is_genesis` override in on_block,
// this matches spec's `block_timeliness = {anchor: [True, True]}`.
Duration::ZERO,
) )
.map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?; .map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?;
@@ -590,6 +605,7 @@ impl ProtoArrayForkChoice {
justified_checkpoint: Checkpoint, justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint,
spec: &ChainSpec, spec: &ChainSpec,
time_into_slot: Duration,
) -> Result<(), String> { ) -> Result<(), String> {
if block.parent_root.is_none() { if block.parent_root.is_none() {
return Err("Missing parent root".to_string()); return Err("Missing parent root".to_string());
@@ -602,6 +618,7 @@ impl ProtoArrayForkChoice {
justified_checkpoint, justified_checkpoint,
finalized_checkpoint, finalized_checkpoint,
spec, spec,
time_into_slot,
) )
.map_err(|e| format!("process_block_error: {:?}", e)) .map_err(|e| format!("process_block_error: {:?}", e))
} }
@@ -705,8 +722,10 @@ impl ProtoArrayForkChoice {
.into()); .into());
} }
// Only re-org if the parent's weight is greater than the parents configured committee fraction. // Spec: `is_parent_strong`. Use payload-aware weight matching the
let parent_weight = info.parent_node.weight(); // payload path the head node is on from its parent.
let parent_payload_status = info.head_node.get_parent_payload_status();
let parent_weight = info.parent_node.attestation_score(parent_payload_status);
let re_org_parent_weight_threshold = info.re_org_parent_weight_threshold; let re_org_parent_weight_threshold = info.re_org_parent_weight_threshold;
let parent_strong = parent_weight > re_org_parent_weight_threshold; let parent_strong = parent_weight > re_org_parent_weight_threshold;
if !parent_strong { if !parent_strong {
@@ -1130,6 +1149,7 @@ fn compute_deltas(
delta: 0, delta: 0,
empty_delta: 0, empty_delta: 0,
full_delta: 0, full_delta: 0,
equivocating_attestation_delta: 0,
}; };
indices.len() indices.len()
]; ];
@@ -1171,6 +1191,11 @@ fn compute_deltas(
block_slot(current_delta_index)?, block_slot(current_delta_index)?,
); );
node_delta.sub_payload_delta(status, old_balance, current_delta_index)?; node_delta.sub_payload_delta(status, old_balance, current_delta_index)?;
// Track equivocating weight for `is_head_weak` monotonicity.
node_delta.equivocating_attestation_delta = node_delta
.equivocating_attestation_delta
.saturating_add(old_balance);
} }
vote.current_root = Hash256::zero(); vote.current_root = Hash256::zero();
@@ -1322,6 +1347,7 @@ mod test_compute_deltas {
genesis_checkpoint, genesis_checkpoint,
genesis_checkpoint, genesis_checkpoint,
&spec, &spec,
Duration::ZERO,
) )
.unwrap(); .unwrap();
@@ -1351,6 +1377,7 @@ mod test_compute_deltas {
genesis_checkpoint, genesis_checkpoint,
genesis_checkpoint, genesis_checkpoint,
&spec, &spec,
Duration::ZERO,
) )
.unwrap(); .unwrap();
@@ -1487,6 +1514,7 @@ mod test_compute_deltas {
genesis_checkpoint, genesis_checkpoint,
genesis_checkpoint, genesis_checkpoint,
&spec, &spec,
Duration::ZERO,
) )
.unwrap(); .unwrap();
}; };