mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-14 18:32:42 +00:00
Enable proposer boost re-orging (#2860)
## Proposed Changes With proposer boosting implemented (#2822) we have an opportunity to re-org out late blocks. This PR adds three flags to the BN to control this behaviour: * `--disable-proposer-reorgs`: turn aggressive re-orging off (it's on by default). * `--proposer-reorg-threshold N`: attempt to orphan blocks with less than N% of the committee vote. If this parameter isn't set then N defaults to 20% when the feature is enabled. * `--proposer-reorg-epochs-since-finalization N`: only attempt to re-org late blocks when the number of epochs since finalization is less than or equal to N. The default is 2 epochs, meaning re-orgs will only be attempted when the chain is finalizing optimally. For safety Lighthouse will only attempt a re-org under very specific conditions: 1. The block being proposed is 1 slot after the canonical head, and the canonical head is 1 slot after its parent. i.e. at slot `n + 1` rather than building on the block from slot `n` we build on the block from slot `n - 1`. 2. The current canonical head received less than N% of the committee vote. N should be set depending on the proposer boost fraction itself, the fraction of the network that is believed to be applying it, and the size of the largest entity that could be hoarding votes. 3. The current canonical head arrived after the attestation deadline from our perspective. This condition was only added to support suppression of forkchoiceUpdated messages, but makes intuitive sense. 4. The block is being proposed in the first 2 seconds of the slot. This gives it time to propagate and receive the proposer boost. ## Additional Info For the initial idea and background, see: https://github.com/ethereum/consensus-specs/pull/2353#issuecomment-950238004 There is also a specification for this feature here: https://github.com/ethereum/consensus-specs/pull/3034 Co-authored-by: Michael Sproul <micsproul@gmail.com> Co-authored-by: pawan <pawandhananjay@gmail.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use safe_arith::ArithError;
|
||||
use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot};
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
@@ -15,6 +16,7 @@ pub enum Error {
|
||||
InvalidNodeDelta(usize),
|
||||
DeltaOverflow(usize),
|
||||
ProposerBoostOverflow(usize),
|
||||
ReOrgThresholdOverflow,
|
||||
IndexOverflow(&'static str),
|
||||
InvalidExecutionDeltaOverflow(usize),
|
||||
InvalidDeltaLen {
|
||||
@@ -48,6 +50,13 @@ pub enum Error {
|
||||
block_root: Hash256,
|
||||
parent_root: Hash256,
|
||||
},
|
||||
Arith(ArithError),
|
||||
}
|
||||
|
||||
impl From<ArithError> for Error {
|
||||
fn from(e: ArithError) -> Self {
|
||||
Error::Arith(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
|
||||
@@ -5,7 +5,7 @@ mod votes;
|
||||
|
||||
use crate::proto_array::CountUnrealizedFull;
|
||||
use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
|
||||
use crate::InvalidationOperation;
|
||||
use crate::{InvalidationOperation, JustifiedBalances};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
use types::{
|
||||
@@ -101,11 +101,14 @@ impl ForkChoiceTestDefinition {
|
||||
justified_state_balances,
|
||||
expected_head,
|
||||
} => {
|
||||
let justified_balances =
|
||||
JustifiedBalances::from_effective_balances(justified_state_balances)
|
||||
.unwrap();
|
||||
let head = fork_choice
|
||||
.find_head::<MainnetEthSpec>(
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
&justified_state_balances,
|
||||
&justified_balances,
|
||||
Hash256::zero(),
|
||||
&equivocating_indices,
|
||||
Slot::new(0),
|
||||
@@ -129,11 +132,14 @@ impl ForkChoiceTestDefinition {
|
||||
expected_head,
|
||||
proposer_boost_root,
|
||||
} => {
|
||||
let justified_balances =
|
||||
JustifiedBalances::from_effective_balances(justified_state_balances)
|
||||
.unwrap();
|
||||
let head = fork_choice
|
||||
.find_head::<MainnetEthSpec>(
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
&justified_state_balances,
|
||||
&justified_balances,
|
||||
proposer_boost_root,
|
||||
&equivocating_indices,
|
||||
Slot::new(0),
|
||||
@@ -155,10 +161,13 @@ impl ForkChoiceTestDefinition {
|
||||
finalized_checkpoint,
|
||||
justified_state_balances,
|
||||
} => {
|
||||
let justified_balances =
|
||||
JustifiedBalances::from_effective_balances(justified_state_balances)
|
||||
.unwrap();
|
||||
let result = fork_choice.find_head::<MainnetEthSpec>(
|
||||
justified_checkpoint,
|
||||
finalized_checkpoint,
|
||||
&justified_state_balances,
|
||||
&justified_balances,
|
||||
Hash256::zero(),
|
||||
&equivocating_indices,
|
||||
Slot::new(0),
|
||||
|
||||
@@ -999,7 +999,7 @@ pub fn get_execution_status_test_definition_03() -> ForkChoiceTestDefinition {
|
||||
});
|
||||
ops.push(Operation::AssertWeight {
|
||||
block_root: get_root(3),
|
||||
// This is a "magic number" generated from `calculate_proposer_boost`.
|
||||
// This is a "magic number" generated from `calculate_committee_fraction`.
|
||||
weight: 31_000,
|
||||
});
|
||||
|
||||
|
||||
62
consensus/proto_array/src/justified_balances.rs
Normal file
62
consensus/proto_array/src/justified_balances.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use safe_arith::{ArithError, SafeArith};
|
||||
use types::{BeaconState, EthSpec};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct JustifiedBalances {
|
||||
/// The effective balances for every validator in a given justified state.
|
||||
///
|
||||
/// Any validator who is not active in the epoch of the justified state is assigned a balance of
|
||||
/// zero.
|
||||
pub effective_balances: Vec<u64>,
|
||||
/// The sum of `self.effective_balances`.
|
||||
pub total_effective_balance: u64,
|
||||
/// The number of active validators included in `self.effective_balances`.
|
||||
pub num_active_validators: u64,
|
||||
}
|
||||
|
||||
impl JustifiedBalances {
|
||||
pub fn from_justified_state<T: EthSpec>(state: &BeaconState<T>) -> Result<Self, ArithError> {
|
||||
let current_epoch = state.current_epoch();
|
||||
let mut total_effective_balance = 0u64;
|
||||
let mut num_active_validators = 0u64;
|
||||
|
||||
let effective_balances = state
|
||||
.validators()
|
||||
.iter()
|
||||
.map(|validator| {
|
||||
if validator.is_active_at(current_epoch) {
|
||||
total_effective_balance.safe_add_assign(validator.effective_balance)?;
|
||||
num_active_validators.safe_add_assign(1)?;
|
||||
|
||||
Ok(validator.effective_balance)
|
||||
} else {
|
||||
Ok(0)
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(Self {
|
||||
effective_balances,
|
||||
total_effective_balance,
|
||||
num_active_validators,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_effective_balances(effective_balances: Vec<u64>) -> Result<Self, ArithError> {
|
||||
let mut total_effective_balance = 0;
|
||||
let mut num_active_validators = 0;
|
||||
|
||||
for &balance in &effective_balances {
|
||||
if balance != 0 {
|
||||
total_effective_balance.safe_add_assign(balance)?;
|
||||
num_active_validators.safe_add_assign(1)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
effective_balances,
|
||||
total_effective_balance,
|
||||
num_active_validators,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
mod error;
|
||||
pub mod fork_choice_test_definition;
|
||||
mod justified_balances;
|
||||
mod proto_array;
|
||||
mod proto_array_fork_choice;
|
||||
mod ssz_container;
|
||||
|
||||
pub use crate::proto_array::{CountUnrealizedFull, InvalidationOperation};
|
||||
pub use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
|
||||
pub use crate::justified_balances::JustifiedBalances;
|
||||
pub use crate::proto_array::{
|
||||
calculate_committee_fraction, CountUnrealizedFull, InvalidationOperation,
|
||||
};
|
||||
pub use crate::proto_array_fork_choice::{
|
||||
Block, DoNotReOrg, ExecutionStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice,
|
||||
ReOrgThreshold,
|
||||
};
|
||||
pub use error::Error;
|
||||
|
||||
pub mod core {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::error::InvalidBestNodeInfo;
|
||||
use crate::{error::Error, Block, ExecutionStatus};
|
||||
use crate::{error::Error, Block, ExecutionStatus, JustifiedBalances};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use ssz::four_byte_option_impl;
|
||||
use ssz::Encode;
|
||||
@@ -169,7 +169,7 @@ impl ProtoArray {
|
||||
mut deltas: Vec<i64>,
|
||||
justified_checkpoint: Checkpoint,
|
||||
finalized_checkpoint: Checkpoint,
|
||||
new_balances: &[u64],
|
||||
new_justified_balances: &JustifiedBalances,
|
||||
proposer_boost_root: Hash256,
|
||||
current_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
@@ -241,9 +241,11 @@ impl ProtoArray {
|
||||
// Invalid nodes (or their ancestors) should not receive a proposer boost.
|
||||
&& !execution_status_is_invalid
|
||||
{
|
||||
proposer_score =
|
||||
calculate_proposer_boost::<E>(new_balances, proposer_score_boost)
|
||||
.ok_or(Error::ProposerBoostOverflow(node_index))?;
|
||||
proposer_score = calculate_committee_fraction::<E>(
|
||||
new_justified_balances,
|
||||
proposer_score_boost,
|
||||
)
|
||||
.ok_or(Error::ProposerBoostOverflow(node_index))?;
|
||||
node_delta = node_delta
|
||||
.checked_add(proposer_score as i64)
|
||||
.ok_or(Error::DeltaOverflow(node_index))?;
|
||||
@@ -1006,32 +1008,19 @@ impl ProtoArray {
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper method to calculate the proposer boost based on the given `validator_balances`.
|
||||
/// This does *not* do any verification about whether a boost should or should not be applied.
|
||||
/// The `validator_balances` array used here is assumed to be structured like the one stored in
|
||||
/// the `BalancesCache`, where *effective* balances are stored and inactive balances are defaulted
|
||||
/// to zero.
|
||||
///
|
||||
/// Returns `None` if there is an overflow or underflow when calculating the score.
|
||||
/// A helper method to calculate the proposer boost based on the given `justified_balances`.
|
||||
///
|
||||
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_latest_attesting_balance
|
||||
pub fn calculate_proposer_boost<E: EthSpec>(
|
||||
validator_balances: &[u64],
|
||||
pub fn calculate_committee_fraction<E: EthSpec>(
|
||||
justified_balances: &JustifiedBalances,
|
||||
proposer_score_boost: u64,
|
||||
) -> Option<u64> {
|
||||
let mut total_balance: u64 = 0;
|
||||
let mut num_validators: u64 = 0;
|
||||
for &balance in validator_balances {
|
||||
// We need to filter zero balances here to get an accurate active validator count.
|
||||
// This is because we default inactive validator balances to zero when creating
|
||||
// this balances array.
|
||||
if balance != 0 {
|
||||
total_balance = total_balance.checked_add(balance)?;
|
||||
num_validators = num_validators.checked_add(1)?;
|
||||
}
|
||||
}
|
||||
let average_balance = total_balance.checked_div(num_validators)?;
|
||||
let committee_size = num_validators.checked_div(E::slots_per_epoch())?;
|
||||
let average_balance = justified_balances
|
||||
.total_effective_balance
|
||||
.checked_div(justified_balances.num_active_validators)?;
|
||||
let committee_size = justified_balances
|
||||
.num_active_validators
|
||||
.checked_div(E::slots_per_epoch())?;
|
||||
let committee_weight = committee_size.checked_mul(average_balance)?;
|
||||
committee_weight
|
||||
.checked_mul(proposer_score_boost)?
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::error::Error;
|
||||
use crate::proto_array::CountUnrealizedFull;
|
||||
use crate::proto_array::{
|
||||
calculate_proposer_boost, InvalidationOperation, Iter, ProposerBoost, ProtoArray, ProtoNode,
|
||||
use crate::{
|
||||
error::Error,
|
||||
proto_array::{
|
||||
calculate_committee_fraction, CountUnrealizedFull, InvalidationOperation, Iter,
|
||||
ProposerBoost, ProtoArray, ProtoNode,
|
||||
},
|
||||
ssz_container::SszContainer,
|
||||
JustifiedBalances,
|
||||
};
|
||||
use crate::ssz_container::SszContainer;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use ssz::{Decode, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
@@ -170,11 +173,128 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the proposer head used for opportunistic re-orgs.
|
||||
#[derive(Clone)]
|
||||
pub struct ProposerHeadInfo {
|
||||
/// Information about the *current* head block, which may be re-orged.
|
||||
pub head_node: ProtoNode,
|
||||
/// Information about the parent of the current head, which should be selected as the parent
|
||||
/// for a new proposal *if* a re-org is decided on.
|
||||
pub parent_node: ProtoNode,
|
||||
/// The computed fraction of the active committee balance below which we can re-org.
|
||||
pub re_org_weight_threshold: u64,
|
||||
/// The current slot from fork choice's point of view, may lead the wall-clock slot by upto
|
||||
/// 500ms.
|
||||
pub current_slot: Slot,
|
||||
}
|
||||
|
||||
/// Error type to enable short-circuiting checks in `get_proposer_head`.
|
||||
///
|
||||
/// This type intentionally does not implement `Debug` so that callers are forced to handle the
|
||||
/// enum.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ProposerHeadError<E> {
|
||||
DoNotReOrg(DoNotReOrg),
|
||||
Error(E),
|
||||
}
|
||||
|
||||
impl<E> From<DoNotReOrg> for ProposerHeadError<E> {
|
||||
fn from(e: DoNotReOrg) -> ProposerHeadError<E> {
|
||||
Self::DoNotReOrg(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for ProposerHeadError<Error> {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E1> ProposerHeadError<E1> {
|
||||
pub fn convert_inner_error<E2>(self) -> ProposerHeadError<E2>
|
||||
where
|
||||
E2: From<E1>,
|
||||
{
|
||||
self.map_inner_error(E2::from)
|
||||
}
|
||||
|
||||
pub fn map_inner_error<E2>(self, f: impl FnOnce(E1) -> E2) -> ProposerHeadError<E2> {
|
||||
match self {
|
||||
ProposerHeadError::DoNotReOrg(reason) => ProposerHeadError::DoNotReOrg(reason),
|
||||
ProposerHeadError::Error(error) => ProposerHeadError::Error(f(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons why a re-org should not be attempted.
|
||||
///
|
||||
/// This type intentionally does not implement `Debug` so that the `Display` impl must be used.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum DoNotReOrg {
|
||||
MissingHeadOrParentNode,
|
||||
MissingHeadFinalizedCheckpoint,
|
||||
ParentDistance,
|
||||
HeadDistance,
|
||||
ShufflingUnstable,
|
||||
JustificationAndFinalizationNotCompetitive,
|
||||
ChainNotFinalizing {
|
||||
epochs_since_finalization: u64,
|
||||
},
|
||||
HeadNotWeak {
|
||||
head_weight: u64,
|
||||
re_org_weight_threshold: u64,
|
||||
},
|
||||
HeadNotLate,
|
||||
NotProposing,
|
||||
ReOrgsDisabled,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DoNotReOrg {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::MissingHeadOrParentNode => write!(f, "unknown head or parent"),
|
||||
Self::MissingHeadFinalizedCheckpoint => write!(f, "finalized checkpoint missing"),
|
||||
Self::ParentDistance => write!(f, "parent too far from head"),
|
||||
Self::HeadDistance => write!(f, "head too far from current slot"),
|
||||
Self::ShufflingUnstable => write!(f, "shuffling unstable at epoch boundary"),
|
||||
Self::JustificationAndFinalizationNotCompetitive => {
|
||||
write!(f, "justification or finalization not competitive")
|
||||
}
|
||||
Self::ChainNotFinalizing {
|
||||
epochs_since_finalization,
|
||||
} => write!(
|
||||
f,
|
||||
"chain not finalizing ({epochs_since_finalization} epochs since finalization)"
|
||||
),
|
||||
Self::HeadNotWeak {
|
||||
head_weight,
|
||||
re_org_weight_threshold,
|
||||
} => {
|
||||
write!(f, "head not weak ({head_weight}/{re_org_weight_threshold})")
|
||||
}
|
||||
Self::HeadNotLate => {
|
||||
write!(f, "head arrived on time")
|
||||
}
|
||||
Self::NotProposing => {
|
||||
write!(f, "not proposing at next slot")
|
||||
}
|
||||
Self::ReOrgsDisabled => {
|
||||
write!(f, "re-orgs disabled in config")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// New-type for the re-org threshold percentage.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ReOrgThreshold(pub u64);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct ProtoArrayForkChoice {
|
||||
pub(crate) proto_array: ProtoArray,
|
||||
pub(crate) votes: ElasticList<VoteTracker>,
|
||||
pub(crate) balances: Vec<u64>,
|
||||
pub(crate) balances: JustifiedBalances,
|
||||
}
|
||||
|
||||
impl ProtoArrayForkChoice {
|
||||
@@ -223,7 +343,7 @@ impl ProtoArrayForkChoice {
|
||||
Ok(Self {
|
||||
proto_array,
|
||||
votes: ElasticList::default(),
|
||||
balances: vec![],
|
||||
balances: JustifiedBalances::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -282,21 +402,20 @@ impl ProtoArrayForkChoice {
|
||||
&mut self,
|
||||
justified_checkpoint: Checkpoint,
|
||||
finalized_checkpoint: Checkpoint,
|
||||
justified_state_balances: &[u64],
|
||||
justified_state_balances: &JustifiedBalances,
|
||||
proposer_boost_root: Hash256,
|
||||
equivocating_indices: &BTreeSet<u64>,
|
||||
current_slot: Slot,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Hash256, String> {
|
||||
let old_balances = &mut self.balances;
|
||||
|
||||
let new_balances = justified_state_balances;
|
||||
|
||||
let deltas = compute_deltas(
|
||||
&self.proto_array.indices,
|
||||
&mut self.votes,
|
||||
old_balances,
|
||||
new_balances,
|
||||
&old_balances.effective_balances,
|
||||
&new_balances.effective_balances,
|
||||
equivocating_indices,
|
||||
)
|
||||
.map_err(|e| format!("find_head compute_deltas failed: {:?}", e))?;
|
||||
@@ -313,13 +432,129 @@ impl ProtoArrayForkChoice {
|
||||
)
|
||||
.map_err(|e| format!("find_head apply_score_changes failed: {:?}", e))?;
|
||||
|
||||
*old_balances = new_balances.to_vec();
|
||||
*old_balances = new_balances.clone();
|
||||
|
||||
self.proto_array
|
||||
.find_head::<E>(&justified_checkpoint.root, current_slot)
|
||||
.map_err(|e| format!("find_head failed: {:?}", e))
|
||||
}
|
||||
|
||||
/// Get the block to propose on during `current_slot`.
|
||||
///
|
||||
/// This function returns a *definitive* result which should be acted on.
|
||||
pub fn get_proposer_head<E: EthSpec>(
|
||||
&self,
|
||||
current_slot: Slot,
|
||||
canonical_head: Hash256,
|
||||
justified_balances: &JustifiedBalances,
|
||||
re_org_threshold: ReOrgThreshold,
|
||||
max_epochs_since_finalization: Epoch,
|
||||
) -> Result<ProposerHeadInfo, ProposerHeadError<Error>> {
|
||||
let info = self.get_proposer_head_info::<E>(
|
||||
current_slot,
|
||||
canonical_head,
|
||||
justified_balances,
|
||||
re_org_threshold,
|
||||
max_epochs_since_finalization,
|
||||
)?;
|
||||
|
||||
// Only re-org a single slot. This prevents cascading failures during asynchrony.
|
||||
let head_slot_ok = info.head_node.slot + 1 == current_slot;
|
||||
if !head_slot_ok {
|
||||
return Err(DoNotReOrg::HeadDistance.into());
|
||||
}
|
||||
|
||||
// Only re-org if the head's weight is less than the configured committee fraction.
|
||||
let head_weight = info.head_node.weight;
|
||||
let re_org_weight_threshold = info.re_org_weight_threshold;
|
||||
let weak_head = head_weight < re_org_weight_threshold;
|
||||
if !weak_head {
|
||||
return Err(DoNotReOrg::HeadNotWeak {
|
||||
head_weight,
|
||||
re_org_weight_threshold,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// All checks have passed, build upon the parent to re-org the head.
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Get information about the block to propose on during `current_slot`.
|
||||
///
|
||||
/// This function returns a *partial* result which must be processed further.
|
||||
pub fn get_proposer_head_info<E: EthSpec>(
|
||||
&self,
|
||||
current_slot: Slot,
|
||||
canonical_head: Hash256,
|
||||
justified_balances: &JustifiedBalances,
|
||||
re_org_threshold: ReOrgThreshold,
|
||||
max_epochs_since_finalization: Epoch,
|
||||
) -> Result<ProposerHeadInfo, ProposerHeadError<Error>> {
|
||||
let mut nodes = self
|
||||
.proto_array
|
||||
.iter_nodes(&canonical_head)
|
||||
.take(2)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let parent_node = nodes.pop().ok_or(DoNotReOrg::MissingHeadOrParentNode)?;
|
||||
let head_node = nodes.pop().ok_or(DoNotReOrg::MissingHeadOrParentNode)?;
|
||||
|
||||
let parent_slot = parent_node.slot;
|
||||
let head_slot = head_node.slot;
|
||||
let re_org_block_slot = head_slot + 1;
|
||||
|
||||
// Check finalization distance.
|
||||
let proposal_epoch = re_org_block_slot.epoch(E::slots_per_epoch());
|
||||
let finalized_epoch = head_node
|
||||
.unrealized_finalized_checkpoint
|
||||
.ok_or(DoNotReOrg::MissingHeadFinalizedCheckpoint)?
|
||||
.epoch;
|
||||
let epochs_since_finalization = proposal_epoch.saturating_sub(finalized_epoch).as_u64();
|
||||
if epochs_since_finalization > max_epochs_since_finalization.as_u64() {
|
||||
return Err(DoNotReOrg::ChainNotFinalizing {
|
||||
epochs_since_finalization,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// Check parent distance from head.
|
||||
// Do not check head distance from current slot, as that condition needs to be
|
||||
// late-evaluated and is elided when `current_slot == head_slot`.
|
||||
let parent_slot_ok = parent_slot + 1 == head_slot;
|
||||
if !parent_slot_ok {
|
||||
return Err(DoNotReOrg::ParentDistance.into());
|
||||
}
|
||||
|
||||
// Check shuffling stability.
|
||||
let shuffling_stable = re_org_block_slot % E::slots_per_epoch() != 0;
|
||||
if !shuffling_stable {
|
||||
return Err(DoNotReOrg::ShufflingUnstable.into());
|
||||
}
|
||||
|
||||
// Check FFG.
|
||||
let ffg_competitive = parent_node.unrealized_justified_checkpoint
|
||||
== head_node.unrealized_justified_checkpoint
|
||||
&& parent_node.unrealized_finalized_checkpoint
|
||||
== head_node.unrealized_finalized_checkpoint;
|
||||
if !ffg_competitive {
|
||||
return Err(DoNotReOrg::JustificationAndFinalizationNotCompetitive.into());
|
||||
}
|
||||
|
||||
// Compute re-org weight threshold.
|
||||
let re_org_weight_threshold =
|
||||
calculate_committee_fraction::<E>(justified_balances, re_org_threshold.0)
|
||||
.ok_or(Error::ReOrgThresholdOverflow)?;
|
||||
|
||||
Ok(ProposerHeadInfo {
|
||||
head_node,
|
||||
parent_node,
|
||||
re_org_weight_threshold,
|
||||
current_slot,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if there are any blocks in `self` with an `INVALID` execution payload status.
|
||||
///
|
||||
/// This will operate on *all* blocks, even those that do not descend from the finalized
|
||||
@@ -368,7 +603,7 @@ impl ProtoArrayForkChoice {
|
||||
if vote.current_root == node.root {
|
||||
// Any voting validator that does not have a balance should be
|
||||
// ignored. This is consistent with `compute_deltas`.
|
||||
self.balances.get(validator_index)
|
||||
self.balances.effective_balances.get(validator_index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -382,9 +617,11 @@ impl ProtoArrayForkChoice {
|
||||
// Compute the score based upon the current balances. We can't rely on
|
||||
// the `previous_proposr_boost.score` since it is set to zero with an
|
||||
// invalid node.
|
||||
let proposer_score =
|
||||
calculate_proposer_boost::<E>(&self.balances, proposer_score_boost)
|
||||
.ok_or("Failed to compute proposer boost")?;
|
||||
let proposer_score = calculate_committee_fraction::<E>(
|
||||
&self.balances,
|
||||
proposer_score_boost,
|
||||
)
|
||||
.ok_or("Failed to compute proposer boost")?;
|
||||
// Store the score we've applied here so it can be removed in
|
||||
// a later call to `apply_score_changes`.
|
||||
self.proto_array.previous_proposer_boost.score = proposer_score;
|
||||
@@ -538,10 +775,11 @@ impl ProtoArrayForkChoice {
|
||||
bytes: &[u8],
|
||||
count_unrealized_full: CountUnrealizedFull,
|
||||
) -> Result<Self, String> {
|
||||
SszContainer::from_ssz_bytes(bytes)
|
||||
.map(|container| (container, count_unrealized_full))
|
||||
.map(Into::into)
|
||||
.map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e))
|
||||
let container = SszContainer::from_ssz_bytes(bytes)
|
||||
.map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e))?;
|
||||
(container, count_unrealized_full)
|
||||
.try_into()
|
||||
.map_err(|e| format!("Failed to initialize ProtoArrayForkChoice: {e:?}"))
|
||||
}
|
||||
|
||||
/// Returns a read-lock to core `ProtoArray` struct.
|
||||
|
||||
@@ -2,10 +2,12 @@ use crate::proto_array::ProposerBoost;
|
||||
use crate::{
|
||||
proto_array::{CountUnrealizedFull, ProtoArray, ProtoNode},
|
||||
proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker},
|
||||
Error, JustifiedBalances,
|
||||
};
|
||||
use ssz::{four_byte_option_impl, Encode};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use types::{Checkpoint, Hash256};
|
||||
|
||||
// Define a "legacy" implementation of `Option<usize>` which uses four bytes for encoding the union
|
||||
@@ -30,7 +32,7 @@ impl From<&ProtoArrayForkChoice> for SszContainer {
|
||||
|
||||
Self {
|
||||
votes: from.votes.0.clone(),
|
||||
balances: from.balances.clone(),
|
||||
balances: from.balances.effective_balances.clone(),
|
||||
prune_threshold: proto_array.prune_threshold,
|
||||
justified_checkpoint: proto_array.justified_checkpoint,
|
||||
finalized_checkpoint: proto_array.finalized_checkpoint,
|
||||
@@ -41,8 +43,12 @@ impl From<&ProtoArrayForkChoice> for SszContainer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(SszContainer, CountUnrealizedFull)> for ProtoArrayForkChoice {
|
||||
fn from((from, count_unrealized_full): (SszContainer, CountUnrealizedFull)) -> Self {
|
||||
impl TryFrom<(SszContainer, CountUnrealizedFull)> for ProtoArrayForkChoice {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(
|
||||
(from, count_unrealized_full): (SszContainer, CountUnrealizedFull),
|
||||
) -> Result<Self, Error> {
|
||||
let proto_array = ProtoArray {
|
||||
prune_threshold: from.prune_threshold,
|
||||
justified_checkpoint: from.justified_checkpoint,
|
||||
@@ -53,10 +59,10 @@ impl From<(SszContainer, CountUnrealizedFull)> for ProtoArrayForkChoice {
|
||||
count_unrealized_full,
|
||||
};
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
proto_array,
|
||||
votes: ElasticList(from.votes),
|
||||
balances: from.balances,
|
||||
}
|
||||
balances: JustifiedBalances::from_effective_balances(from.balances)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user