mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-08 01:05:47 +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:
@@ -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(())
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user