mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-07 00:42:42 +00:00
Merge remote-tracking branch 'origin/unstable' into capella-update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use crate::{ForkChoiceStore, InvalidationOperation};
|
||||
use proto_array::{
|
||||
Block as ProtoBlock, CountUnrealizedFull, ExecutionStatus, ProtoArrayForkChoice,
|
||||
Block as ProtoBlock, CountUnrealizedFull, ExecutionStatus, ProposerHeadError, ProposerHeadInfo,
|
||||
ProtoArrayForkChoice, ReOrgThreshold,
|
||||
};
|
||||
use slog::{crit, debug, warn, Logger};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
@@ -23,7 +24,8 @@ pub enum Error<T> {
|
||||
InvalidAttestation(InvalidAttestation),
|
||||
InvalidAttesterSlashing(AttesterSlashingValidationError),
|
||||
InvalidBlock(InvalidBlock),
|
||||
ProtoArrayError(String),
|
||||
ProtoArrayStringError(String),
|
||||
ProtoArrayError(proto_array::Error),
|
||||
InvalidProtoArrayBytes(String),
|
||||
InvalidLegacyProtoArrayBytes(String),
|
||||
FailedToProcessInvalidExecutionPayload(String),
|
||||
@@ -45,6 +47,7 @@ pub enum Error<T> {
|
||||
ForkChoiceStoreError(T),
|
||||
UnableToSetJustifiedCheckpoint(T),
|
||||
AfterBlockFailed(T),
|
||||
ProposerHeadError(T),
|
||||
InvalidAnchor {
|
||||
block_slot: Slot,
|
||||
state_slot: Slot,
|
||||
@@ -60,6 +63,13 @@ pub enum Error<T> {
|
||||
MissingFinalizedBlock {
|
||||
finalized_checkpoint: Checkpoint,
|
||||
},
|
||||
WrongSlotForGetProposerHead {
|
||||
current_slot: Slot,
|
||||
fc_store_slot: Slot,
|
||||
},
|
||||
ProposerBoostNotExpiredForGetProposerHead {
|
||||
proposer_boost_root: Hash256,
|
||||
},
|
||||
UnrealizedVoteProcessing(state_processing::EpochProcessingError),
|
||||
ParticipationCacheBuild(BeaconStateError),
|
||||
ValidatorStatuses(BeaconStateError),
|
||||
@@ -154,6 +164,12 @@ pub enum InvalidAttestation {
|
||||
|
||||
impl<T> From<String> for Error<T> {
|
||||
fn from(e: String) -> Self {
|
||||
Error::ProtoArrayStringError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<proto_array::Error> for Error<T> {
|
||||
fn from(e: proto_array::Error) -> Self {
|
||||
Error::ProtoArrayError(e)
|
||||
}
|
||||
}
|
||||
@@ -555,6 +571,69 @@ where
|
||||
Ok(head_root)
|
||||
}
|
||||
|
||||
/// Get the block to build on as proposer, taking into account proposer re-orgs.
|
||||
///
|
||||
/// You *must* call `get_head` for the proposal slot prior to calling this function and pass
|
||||
/// in the result of `get_head` as `canonical_head`.
|
||||
pub fn get_proposer_head(
|
||||
&self,
|
||||
current_slot: Slot,
|
||||
canonical_head: Hash256,
|
||||
re_org_threshold: ReOrgThreshold,
|
||||
max_epochs_since_finalization: Epoch,
|
||||
) -> Result<ProposerHeadInfo, ProposerHeadError<Error<proto_array::Error>>> {
|
||||
// Ensure that fork choice has already been updated for the current slot. This prevents
|
||||
// us from having to take a write lock or do any dequeueing of attestations in this
|
||||
// function.
|
||||
let fc_store_slot = self.fc_store.get_current_slot();
|
||||
if current_slot != fc_store_slot {
|
||||
return Err(ProposerHeadError::Error(
|
||||
Error::WrongSlotForGetProposerHead {
|
||||
current_slot,
|
||||
fc_store_slot,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Similarly, the proposer boost for the previous head should already have expired.
|
||||
let proposer_boost_root = self.fc_store.proposer_boost_root();
|
||||
if !proposer_boost_root.is_zero() {
|
||||
return Err(ProposerHeadError::Error(
|
||||
Error::ProposerBoostNotExpiredForGetProposerHead {
|
||||
proposer_boost_root,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
self.proto_array
|
||||
.get_proposer_head::<E>(
|
||||
current_slot,
|
||||
canonical_head,
|
||||
self.fc_store.justified_balances(),
|
||||
re_org_threshold,
|
||||
max_epochs_since_finalization,
|
||||
)
|
||||
.map_err(ProposerHeadError::convert_inner_error)
|
||||
}
|
||||
|
||||
pub fn get_preliminary_proposer_head(
|
||||
&self,
|
||||
canonical_head: Hash256,
|
||||
re_org_threshold: ReOrgThreshold,
|
||||
max_epochs_since_finalization: Epoch,
|
||||
) -> Result<ProposerHeadInfo, ProposerHeadError<Error<proto_array::Error>>> {
|
||||
let current_slot = self.fc_store.get_current_slot();
|
||||
self.proto_array
|
||||
.get_proposer_head_info::<E>(
|
||||
current_slot,
|
||||
canonical_head,
|
||||
self.fc_store.justified_balances(),
|
||||
re_org_threshold,
|
||||
max_epochs_since_finalization,
|
||||
)
|
||||
.map_err(ProposerHeadError::convert_inner_error)
|
||||
}
|
||||
|
||||
/// Return information about:
|
||||
///
|
||||
/// - The LMD head of the chain.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use proto_array::JustifiedBalances;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Debug;
|
||||
use types::{AbstractExecPayload, BeaconBlockRef, BeaconState, Checkpoint, EthSpec, Hash256, Slot};
|
||||
@@ -44,7 +45,7 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
|
||||
fn justified_checkpoint(&self) -> &Checkpoint;
|
||||
|
||||
/// Returns balances from the `state` identified by `justified_checkpoint.root`.
|
||||
fn justified_balances(&self) -> &[u64];
|
||||
fn justified_balances(&self) -> &JustifiedBalances;
|
||||
|
||||
/// Returns the `best_justified_checkpoint`.
|
||||
fn best_justified_checkpoint(&self) -> &Checkpoint;
|
||||
|
||||
@@ -378,9 +378,13 @@ impl ForkChoiceTest {
|
||||
|
||||
assert_eq!(
|
||||
&balances[..],
|
||||
fc.fc_store().justified_balances(),
|
||||
&fc.fc_store().justified_balances().effective_balances,
|
||||
"balances should match"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
balances.iter().sum::<u64>(),
|
||||
fc.fc_store().justified_balances().total_effective_balance
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns an attestation that is valid for some slot in the given `chain`.
|
||||
|
||||
@@ -15,3 +15,4 @@ eth2_ssz_derive = "0.3.1"
|
||||
serde = "1.0.116"
|
||||
serde_derive = "1.0.116"
|
||||
serde_yaml = "0.8.13"
|
||||
safe_arith = { path = "../safe_arith" }
|
||||
|
||||
@@ -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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ pub mod graffiti;
|
||||
pub mod historical_batch;
|
||||
pub mod indexed_attestation;
|
||||
pub mod light_client_bootstrap;
|
||||
pub mod light_client_finality_update;
|
||||
pub mod light_client_optimistic_update;
|
||||
pub mod light_client_update;
|
||||
pub mod pending_attestation;
|
||||
@@ -152,6 +153,8 @@ pub use crate::historical_batch::HistoricalBatch;
|
||||
pub use crate::indexed_attestation::IndexedAttestation;
|
||||
pub use crate::kzg_commitment::KzgCommitment;
|
||||
pub use crate::kzg_proof::KzgProof;
|
||||
pub use crate::light_client_finality_update::LightClientFinalityUpdate;
|
||||
pub use crate::light_client_optimistic_update::LightClientOptimisticUpdate;
|
||||
pub use crate::participation_flags::ParticipationFlags;
|
||||
pub use crate::participation_list::ParticipationList;
|
||||
pub use crate::payload::{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::{BeaconBlockHeader, EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee};
|
||||
use crate::{light_client_update::*, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec};
|
||||
use safe_arith::ArithError;
|
||||
use super::{
|
||||
BeaconBlockHeader, EthSpec, FixedVector, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock,
|
||||
Slot, SyncAggregate,
|
||||
};
|
||||
use crate::{light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use ssz_types::typenum::{U5, U6};
|
||||
use std::sync::Arc;
|
||||
use test_random_derive::TestRandom;
|
||||
use tree_hash::TreeHash;
|
||||
|
||||
@@ -28,43 +28,38 @@ pub struct LightClientFinalityUpdate<T: EthSpec> {
|
||||
|
||||
impl<T: EthSpec> LightClientFinalityUpdate<T> {
|
||||
pub fn new(
|
||||
chain_spec: ChainSpec,
|
||||
beacon_state: BeaconState<T>,
|
||||
block: BeaconBlock<T>,
|
||||
chain_spec: &ChainSpec,
|
||||
beacon_state: &BeaconState<T>,
|
||||
block: &SignedBeaconBlock<T>,
|
||||
attested_state: &mut BeaconState<T>,
|
||||
finalized_block: BeaconBlock<T>,
|
||||
finalized_block: &SignedBlindedBeaconBlock<T>,
|
||||
) -> Result<Self, Error> {
|
||||
let altair_fork_epoch = chain_spec
|
||||
.altair_fork_epoch
|
||||
.ok_or(Error::AltairForkNotActive)?;
|
||||
if attested_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch {
|
||||
if beacon_state.slot().epoch(T::slots_per_epoch()) < altair_fork_epoch {
|
||||
return Err(Error::AltairForkNotActive);
|
||||
}
|
||||
|
||||
let sync_aggregate = block.body().sync_aggregate()?;
|
||||
let sync_aggregate = block.message().body().sync_aggregate()?;
|
||||
if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize {
|
||||
return Err(Error::NotEnoughSyncCommitteeParticipants);
|
||||
}
|
||||
|
||||
// Compute and validate attested header.
|
||||
let mut attested_header = attested_state.latest_block_header().clone();
|
||||
attested_header.state_root = attested_state.tree_hash_root();
|
||||
attested_header.state_root = attested_state.update_tree_hash_cache()?;
|
||||
// Build finalized header from finalized block
|
||||
let finalized_header = BeaconBlockHeader {
|
||||
slot: finalized_block.slot(),
|
||||
proposer_index: finalized_block.proposer_index(),
|
||||
parent_root: finalized_block.parent_root(),
|
||||
state_root: finalized_block.state_root(),
|
||||
body_root: finalized_block.body_root(),
|
||||
};
|
||||
let finalized_header = finalized_block.message().block_header();
|
||||
|
||||
if finalized_header.tree_hash_root() != beacon_state.finalized_checkpoint().root {
|
||||
return Err(Error::InvalidFinalizedBlock);
|
||||
}
|
||||
|
||||
let finality_branch = attested_state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?;
|
||||
Ok(Self {
|
||||
attested_header: attested_header,
|
||||
finalized_header: finalized_header,
|
||||
attested_header,
|
||||
finalized_header,
|
||||
finality_branch: FixedVector::new(finality_branch)?,
|
||||
sync_aggregate: sync_aggregate.clone(),
|
||||
signature_slot: block.slot(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{BeaconBlockHeader, EthSpec, Slot, SyncAggregate};
|
||||
use crate::{
|
||||
light_client_update::Error, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec,
|
||||
light_client_update::Error, test_utils::TestRandom, BeaconState, ChainSpec, SignedBeaconBlock,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
@@ -23,9 +23,9 @@ pub struct LightClientOptimisticUpdate<T: EthSpec> {
|
||||
|
||||
impl<T: EthSpec> LightClientOptimisticUpdate<T> {
|
||||
pub fn new(
|
||||
chain_spec: ChainSpec,
|
||||
block: BeaconBlock<T>,
|
||||
attested_state: BeaconState<T>,
|
||||
chain_spec: &ChainSpec,
|
||||
block: &SignedBeaconBlock<T>,
|
||||
attested_state: &BeaconState<T>,
|
||||
) -> Result<Self, Error> {
|
||||
let altair_fork_epoch = chain_spec
|
||||
.altair_fork_epoch
|
||||
@@ -34,7 +34,7 @@ impl<T: EthSpec> LightClientOptimisticUpdate<T> {
|
||||
return Err(Error::AltairForkNotActive);
|
||||
}
|
||||
|
||||
let sync_aggregate = block.body().sync_aggregate()?;
|
||||
let sync_aggregate = block.message().body().sync_aggregate()?;
|
||||
if sync_aggregate.num_set_bits() < chain_spec.min_sync_committee_participants as usize {
|
||||
return Err(Error::NotEnoughSyncCommitteeParticipants);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user