mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-20 22:38:34 +00:00
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:
@@ -10,6 +10,7 @@ use fixed_bytes::FixedBytesExtended;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz::BitVector;
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration;
|
||||
use types::{
|
||||
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
|
||||
MainnetEthSpec, Slot,
|
||||
@@ -288,6 +289,7 @@ impl ForkChoiceTestDefinition {
|
||||
self.justified_checkpoint,
|
||||
self.finalized_checkpoint,
|
||||
&spec,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::error::InvalidBestNodeInfo;
|
||||
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 serde::{Deserialize, Serialize};
|
||||
use ssz::BitVector;
|
||||
@@ -8,6 +10,7 @@ use ssz::Encode;
|
||||
use ssz::four_byte_option_impl;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
use superstruct::superstruct;
|
||||
use typenum::U512;
|
||||
use types::{
|
||||
@@ -20,6 +23,14 @@ use types::{
|
||||
four_byte_option_impl!(four_byte_option_usize, usize);
|
||||
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.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InvalidationOperation {
|
||||
@@ -160,22 +171,27 @@ pub struct ProtoNode {
|
||||
/// to detect equivocations at the parent's slot.
|
||||
#[superstruct(only(V29), partial_getter(copy))]
|
||||
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 {
|
||||
/// Generic version of spec's `parent_payload_status` that works for pre-Gloas nodes by
|
||||
/// 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)
|
||||
}
|
||||
|
||||
fn is_parent_node_full(&self) -> bool {
|
||||
pub fn is_parent_node_full(&self) -> bool {
|
||||
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 {
|
||||
// TODO(gloas): rename weight and remove proposer boost from it?
|
||||
PayloadStatus::Pending => self.weight(),
|
||||
PayloadStatus::Empty => self.empty_payload_weight().unwrap_or(0),
|
||||
PayloadStatus::Full => self.full_payload_weight().unwrap_or(0),
|
||||
@@ -187,8 +203,7 @@ impl ProtoNode {
|
||||
return false;
|
||||
};
|
||||
|
||||
// If the payload is not locally available, the payload
|
||||
// is not considered available regardless of the PTC vote
|
||||
// Equivalent to `if root not in store.payload_states` in the spec.
|
||||
if !node.payload_received {
|
||||
return false;
|
||||
}
|
||||
@@ -201,8 +216,7 @@ impl ProtoNode {
|
||||
return false;
|
||||
};
|
||||
|
||||
// If the payload is not locally available, the payload
|
||||
// is not considered available regardless of the PTC vote
|
||||
// Equivalent to `if root not in store.payload_states` in the spec.
|
||||
if !node.payload_received {
|
||||
return false;
|
||||
}
|
||||
@@ -252,6 +266,8 @@ pub struct NodeDelta {
|
||||
pub empty_delta: i64,
|
||||
/// Weight change from `PayloadStatus::Full` votes.
|
||||
pub full_delta: i64,
|
||||
/// Weight from equivocating validators that voted for this node.
|
||||
pub equivocating_attestation_delta: u64,
|
||||
}
|
||||
|
||||
impl NodeDelta {
|
||||
@@ -308,6 +324,7 @@ impl NodeDelta {
|
||||
delta,
|
||||
empty_delta: 0,
|
||||
full_delta: 0,
|
||||
equivocating_attestation_delta: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,10 +387,10 @@ impl ProtoArray {
|
||||
mut deltas: Vec<NodeDelta>,
|
||||
best_justified_checkpoint: Checkpoint,
|
||||
best_finalized_checkpoint: Checkpoint,
|
||||
new_justified_balances: &JustifiedBalances,
|
||||
proposer_boost_root: Hash256,
|
||||
_new_justified_balances: &JustifiedBalances,
|
||||
_proposer_boost_root: Hash256,
|
||||
current_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
_spec: &ChainSpec,
|
||||
) -> Result<(), Error> {
|
||||
if deltas.len() != self.indices.len() {
|
||||
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`.
|
||||
for node_index in (0..self.nodes.len()).rev() {
|
||||
let node = self
|
||||
@@ -412,7 +426,7 @@ impl ProtoArray {
|
||||
.copied()
|
||||
.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.
|
||||
0_i64
|
||||
.checked_sub(node.weight() as i64)
|
||||
@@ -427,37 +441,9 @@ impl ProtoArray {
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
// If we find the node for which the proposer boost was previously applied, decrease
|
||||
// the delta by the previous score amount.
|
||||
// TODO(gloas): implement `should_apply_proposer_boost` from the Gloas spec.
|
||||
// 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))?;
|
||||
}
|
||||
// Proposer boost is NOT applied here. It is computed on-the-fly
|
||||
// during the virtual tree walk in `get_weight`, matching the spec's
|
||||
// `get_weight` which adds boost separately from `get_attestation_score`.
|
||||
|
||||
// Apply the delta to the node.
|
||||
if execution_status_is_invalid {
|
||||
@@ -473,6 +459,9 @@ impl ProtoArray {
|
||||
apply_delta(node.empty_payload_weight, node_empty_delta, node_index)?;
|
||||
node.full_payload_weight =
|
||||
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).
|
||||
@@ -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`.
|
||||
//
|
||||
// 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_finalized_checkpoint: Checkpoint,
|
||||
spec: &ChainSpec,
|
||||
time_into_slot: Duration,
|
||||
) -> Result<(), Error> {
|
||||
// If the block is already known, simply ignore it.
|
||||
if self.indices.contains_key(&block.root) {
|
||||
@@ -642,6 +571,8 @@ impl ProtoArray {
|
||||
unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint,
|
||||
})
|
||||
} else {
|
||||
let is_current_slot = current_slot == block.slot;
|
||||
|
||||
let execution_payload_block_hash =
|
||||
block
|
||||
.execution_payload_block_hash
|
||||
@@ -712,28 +643,27 @@ impl ProtoArray {
|
||||
// initialized to all-True, ensuring `is_payload_timely` and
|
||||
// `is_payload_data_available` return true for the anchor.
|
||||
payload_timeliness_votes: if is_genesis {
|
||||
let mut bv = BitVector::new();
|
||||
for i in 0..bv.len() {
|
||||
let _ = bv.set(i, true);
|
||||
}
|
||||
bv
|
||||
all_true_bitvector()
|
||||
} else {
|
||||
BitVector::default()
|
||||
},
|
||||
payload_data_availability_votes: if is_genesis {
|
||||
let mut bv = BitVector::new();
|
||||
for i in 0..bv.len() {
|
||||
let _ = bv.set(i, true);
|
||||
}
|
||||
bv
|
||||
all_true_bitvector()
|
||||
} else {
|
||||
BitVector::default()
|
||||
},
|
||||
payload_received: is_genesis,
|
||||
proposer_index: block.proposer_index.unwrap_or(0),
|
||||
// TODO(gloas): initialise these based on block timing
|
||||
block_timeliness_attestation_threshold: false,
|
||||
block_timeliness_ptc_threshold: false,
|
||||
// Spec: `record_block_timeliness` + `get_forkchoice_store`.
|
||||
// Anchor gets [True, True]. Others computed from time_into_slot.
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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>(
|
||||
&self,
|
||||
head_node: &ProtoNode,
|
||||
@@ -788,11 +724,10 @@ impl ProtoArray {
|
||||
)
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -1207,7 +1142,7 @@ impl ProtoArray {
|
||||
// In the post-Gloas world, always use a virtual tree walk.
|
||||
//
|
||||
// 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,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
@@ -1218,8 +1153,12 @@ impl ProtoArray {
|
||||
)?;
|
||||
|
||||
// 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>(
|
||||
&best_node,
|
||||
best_node,
|
||||
current_slot,
|
||||
best_justified_checkpoint,
|
||||
best_finalized_checkpoint,
|
||||
@@ -1238,80 +1177,79 @@ impl ProtoArray {
|
||||
Ok((best_fc_node.root, best_fc_node.payload_status))
|
||||
}
|
||||
|
||||
/// Virtual tree walk for `find_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.
|
||||
/// Spec: `get_head`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn find_head_walk<E: EthSpec>(
|
||||
&self,
|
||||
start_index: usize,
|
||||
current_slot: Slot,
|
||||
best_justified_checkpoint: Checkpoint,
|
||||
_best_finalized_checkpoint: Checkpoint,
|
||||
best_finalized_checkpoint: Checkpoint,
|
||||
proposer_boost_root: Hash256,
|
||||
justified_balances: &JustifiedBalances,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<(IndexedForkChoiceNode, ProtoNode), Error> {
|
||||
) -> Result<IndexedForkChoiceNode, Error> {
|
||||
let mut head = IndexedForkChoiceNode {
|
||||
root: best_justified_checkpoint.root,
|
||||
proto_node_index: start_index,
|
||||
payload_status: PayloadStatus::Pending,
|
||||
};
|
||||
let mut head_proto_node = self
|
||||
.nodes
|
||||
.get(start_index)
|
||||
.ok_or(Error::NodeUnknown(best_justified_checkpoint.root))?
|
||||
.clone();
|
||||
|
||||
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() {
|
||||
break;
|
||||
return Ok(head);
|
||||
}
|
||||
|
||||
let scores = children
|
||||
head = children
|
||||
.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>(
|
||||
&child_fc_node,
|
||||
&child_proto_node,
|
||||
&child,
|
||||
proto_node,
|
||||
proposer_boost_root,
|
||||
current_slot,
|
||||
justified_balances,
|
||||
spec,
|
||||
)?;
|
||||
let payload_status_tiebreaker = self.get_payload_status_tiebreaker::<E>(
|
||||
&child_fc_node,
|
||||
&child_proto_node,
|
||||
&child,
|
||||
proto_node,
|
||||
current_slot,
|
||||
proposer_boost_root,
|
||||
)?;
|
||||
Ok((
|
||||
child_fc_node,
|
||||
child_proto_node,
|
||||
weight,
|
||||
payload_status_tiebreaker,
|
||||
))
|
||||
Ok((child, weight, payload_status_tiebreaker))
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
// TODO(gloas): proper error
|
||||
(head, head_proto_node) = scores
|
||||
.collect::<Result<Vec<_>, Error>>()?
|
||||
.into_iter()
|
||||
.max_by_key(
|
||||
|(child_fc_node, _proto_node, weight, payload_status_tiebreaker)| {
|
||||
(*weight, child_fc_node.root, *payload_status_tiebreaker)
|
||||
},
|
||||
)
|
||||
.map(|(child_fc_node, child_proto_node, _, _)| (child_fc_node, child_proto_node))
|
||||
.unwrap();
|
||||
.max_by_key(|(child, weight, payload_status_tiebreaker)| {
|
||||
(*weight, child.root, *payload_status_tiebreaker)
|
||||
})
|
||||
.map(|(child, _, _)| child)
|
||||
.expect("children is non-empty");
|
||||
}
|
||||
|
||||
Ok((head, head_proto_node))
|
||||
}
|
||||
|
||||
/// Spec: `get_weight`.
|
||||
fn get_weight<E: EthSpec>(
|
||||
&self,
|
||||
fc_node: &IndexedForkChoiceNode,
|
||||
@@ -1334,19 +1272,99 @@ impl ProtoArray {
|
||||
return Ok(attestation_score);
|
||||
}
|
||||
|
||||
// TODO(gloas): I don't think `is_supporting_vote` is necessary here, confirm by
|
||||
// checking spec tests or with spec authors.
|
||||
let proposer_score = if proto_node.root() == proposer_boost_root {
|
||||
// Spec: proposer boost is treated as a synthetic vote.
|
||||
let message = LatestMessage {
|
||||
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)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Ok(attestation_score.saturating_add(proposer_score))
|
||||
} else {
|
||||
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(
|
||||
&self,
|
||||
node: &IndexedForkChoiceNode,
|
||||
@@ -1355,30 +1373,15 @@ impl ProtoArray {
|
||||
let proto_node = self
|
||||
.nodes
|
||||
.get(node.proto_node_index)
|
||||
.ok_or(Error::InvalidNodeIndex(node.proto_node_index))?
|
||||
.clone();
|
||||
let mut children = vec![(
|
||||
IndexedForkChoiceNode {
|
||||
root: node.root,
|
||||
proto_node_index: node.proto_node_index,
|
||||
payload_status: PayloadStatus::Empty,
|
||||
},
|
||||
proto_node.clone(),
|
||||
)];
|
||||
.ok_or(Error::InvalidNodeIndex(node.proto_node_index))?;
|
||||
let mut children = vec![(node.with_status(PayloadStatus::Empty), proto_node.clone())];
|
||||
// The FULL virtual child only exists if the payload has been received.
|
||||
if proto_node.payload_received().is_ok_and(|received| received) {
|
||||
children.push((
|
||||
IndexedForkChoiceNode {
|
||||
root: node.root,
|
||||
proto_node_index: node.proto_node_index,
|
||||
payload_status: PayloadStatus::Full,
|
||||
},
|
||||
proto_node,
|
||||
));
|
||||
children.push((node.with_status(PayloadStatus::Full), proto_node.clone()));
|
||||
}
|
||||
Ok(children)
|
||||
} else {
|
||||
let children = self
|
||||
Ok(self
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -1396,8 +1399,7 @@ impl ProtoArray {
|
||||
child_node.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(children)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,8 +1429,10 @@ impl ProtoArray {
|
||||
proto_node: &ProtoNode,
|
||||
proposer_boost_root: Hash256,
|
||||
) -> 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() {
|
||||
return Ok(false);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let proposer_boost_node_index = *self
|
||||
@@ -1440,20 +1444,18 @@ impl ProtoArray {
|
||||
.get(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 Some(proposer_boost_parent_index) = proposer_boost_node.parent() else {
|
||||
// TODO(gloas): could be an error
|
||||
return Ok(false);
|
||||
};
|
||||
let boost_parent_root = self
|
||||
let parent_index = proposer_boost_node
|
||||
.parent()
|
||||
.ok_or(Error::NodeUnknown(proposer_boost_root))?;
|
||||
let proposer_boost_parent_root = self
|
||||
.nodes
|
||||
.get(proposer_boost_parent_index)
|
||||
.ok_or(Error::InvalidNodeIndex(proposer_boost_parent_index))?
|
||||
.get(parent_index)
|
||||
.ok_or(Error::InvalidNodeIndex(parent_index))?
|
||||
.root();
|
||||
|
||||
Ok(
|
||||
(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(),
|
||||
)
|
||||
}
|
||||
@@ -1879,15 +1881,14 @@ pub fn calculate_committee_fraction<E: EthSpec>(
|
||||
.checked_div(100)
|
||||
}
|
||||
|
||||
pub fn get_proposer_score<E: EthSpec>(
|
||||
/// Spec: `get_proposer_score`.
|
||||
fn get_proposer_score<E: EthSpec>(
|
||||
justified_balances: &JustifiedBalances,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<u64, Error> {
|
||||
let Some(proposer_score_boost) = spec.proposer_score_boost else {
|
||||
// TODO(gloas): make proposer boost non-optional in spec
|
||||
return Ok(0);
|
||||
};
|
||||
// TODO(gloas): fix error
|
||||
calculate_committee_fraction::<E>(justified_balances, proposer_score_boost)
|
||||
.ok_or(Error::ProposerBoostOverflow(0))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use ssz_derive::{Decode, Encode};
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
fmt,
|
||||
time::Duration,
|
||||
};
|
||||
use types::{
|
||||
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
|
||||
@@ -75,6 +76,16 @@ pub struct IndexedForkChoiceNode {
|
||||
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 {
|
||||
pub fn is_execution_enabled(&self) -> bool {
|
||||
!matches!(self, ExecutionStatus::Irrelevant(_))
|
||||
@@ -491,6 +502,10 @@ impl ProtoArrayForkChoice {
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
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))?;
|
||||
|
||||
@@ -590,6 +605,7 @@ impl ProtoArrayForkChoice {
|
||||
justified_checkpoint: Checkpoint,
|
||||
finalized_checkpoint: Checkpoint,
|
||||
spec: &ChainSpec,
|
||||
time_into_slot: Duration,
|
||||
) -> Result<(), String> {
|
||||
if block.parent_root.is_none() {
|
||||
return Err("Missing parent root".to_string());
|
||||
@@ -602,6 +618,7 @@ impl ProtoArrayForkChoice {
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
spec,
|
||||
time_into_slot,
|
||||
)
|
||||
.map_err(|e| format!("process_block_error: {:?}", e))
|
||||
}
|
||||
@@ -705,8 +722,10 @@ impl ProtoArrayForkChoice {
|
||||
.into());
|
||||
}
|
||||
|
||||
// Only re-org if the parent's weight is greater than the parents configured committee fraction.
|
||||
let parent_weight = info.parent_node.weight();
|
||||
// Spec: `is_parent_strong`. Use payload-aware weight matching the
|
||||
// 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 parent_strong = parent_weight > re_org_parent_weight_threshold;
|
||||
if !parent_strong {
|
||||
@@ -1130,6 +1149,7 @@ fn compute_deltas(
|
||||
delta: 0,
|
||||
empty_delta: 0,
|
||||
full_delta: 0,
|
||||
equivocating_attestation_delta: 0,
|
||||
};
|
||||
indices.len()
|
||||
];
|
||||
@@ -1171,6 +1191,11 @@ fn compute_deltas(
|
||||
block_slot(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();
|
||||
@@ -1322,6 +1347,7 @@ mod test_compute_deltas {
|
||||
genesis_checkpoint,
|
||||
genesis_checkpoint,
|
||||
&spec,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1351,6 +1377,7 @@ mod test_compute_deltas {
|
||||
genesis_checkpoint,
|
||||
genesis_checkpoint,
|
||||
&spec,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1487,6 +1514,7 @@ mod test_compute_deltas {
|
||||
genesis_checkpoint,
|
||||
genesis_checkpoint,
|
||||
&spec,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user