Merge remote-tracking branch 'origin/unstable' into capella-update

This commit is contained in:
Michael Sproul
2022-12-14 13:00:41 +11:00
81 changed files with 3532 additions and 427 deletions

View File

@@ -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" }

View File

@@ -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)]

View File

@@ -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),

View File

@@ -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,
});

View 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,
})
}
}

View File

@@ -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 {

View File

@@ -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)?

View File

@@ -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.

View File

@@ -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)?,
})
}
}