Resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-01-02 08:52:14 -06:00
918 changed files with 49304 additions and 37273 deletions

View File

@@ -11,6 +11,7 @@ path = "src/bin.rs"
[dependencies]
ethereum_ssz = { workspace = true }
ethereum_ssz_derive = { workspace = true }
fixed_bytes = { workspace = true }
safe_arith = { workspace = true }
serde = { workspace = true }
serde_yaml = { workspace = true }

View File

@@ -5,11 +5,12 @@ mod votes;
use crate::proto_array_fork_choice::{Block, ExecutionStatus, ProtoArrayForkChoice};
use crate::{InvalidationOperation, JustifiedBalances};
use fixed_bytes::FixedBytesExtended;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap};
use types::{
AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, FixedBytesExtended,
Hash256, MainnetEthSpec, Slot,
AttestationShufflingId, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
MainnetEthSpec, Slot,
};
pub use execution_status::*;
@@ -213,7 +214,12 @@ impl ForkChoiceTestDefinition {
unrealized_finalized_checkpoint: None,
};
fork_choice
.process_block::<MainnetEthSpec>(block, slot)
.process_block::<MainnetEthSpec>(
block,
slot,
self.justified_checkpoint,
self.finalized_checkpoint,
)
.unwrap_or_else(|e| {
panic!(
"process_block op at index {} returned error: {:?}",
@@ -273,7 +279,10 @@ impl ForkChoiceTestDefinition {
}
};
fork_choice
.process_execution_payload_invalidation::<MainnetEthSpec>(&op)
.process_execution_payload_invalidation::<MainnetEthSpec>(
&op,
self.finalized_checkpoint,
)
.unwrap()
}
Operation::AssertWeight { block_root, weight } => assert_eq!(
@@ -306,9 +315,10 @@ fn get_checkpoint(i: u64) -> Checkpoint {
}
fn check_bytes_round_trip(original: &ProtoArrayForkChoice) {
let bytes = original.as_bytes();
let decoded =
ProtoArrayForkChoice::from_bytes(&bytes).expect("fork choice should decode from bytes");
// The checkpoint are ignored `ProtoArrayForkChoice::from_bytes` so any value is ok
let bytes = original.as_bytes(Checkpoint::default(), Checkpoint::default());
let decoded = ProtoArrayForkChoice::from_bytes(&bytes, original.balances.clone())
.expect("fork choice should decode from bytes");
assert!(
*original == decoded,
"fork choice should encode and decode without change"

View File

@@ -1,4 +1,4 @@
use types::FixedBytesExtended;
use fixed_bytes::FixedBytesExtended;
use super::*;

View File

@@ -6,7 +6,7 @@ mod proto_array_fork_choice;
mod ssz_container;
pub use crate::justified_balances::JustifiedBalances;
pub use crate::proto_array::{calculate_committee_fraction, InvalidationOperation};
pub use crate::proto_array::{InvalidationOperation, calculate_committee_fraction};
pub use crate::proto_array_fork_choice::{
Block, DisallowedReOrgOffsets, DoNotReOrg, ExecutionStatus, ProposerHeadError,
ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold,
@@ -16,5 +16,5 @@ pub use error::Error;
pub mod core {
pub use super::proto_array::{ProposerBoost, ProtoArray, ProtoNode};
pub use super::proto_array_fork_choice::VoteTracker;
pub use super::ssz_container::{SszContainer, SszContainerV17};
pub use super::ssz_container::{SszContainer, SszContainerV17, SszContainerV28};
}

View File

@@ -1,15 +1,16 @@
use crate::error::InvalidBestNodeInfo;
use crate::{error::Error, Block, ExecutionStatus, JustifiedBalances};
use crate::{Block, ExecutionStatus, JustifiedBalances, error::Error};
use fixed_bytes::FixedBytesExtended;
use serde::{Deserialize, Serialize};
use ssz::four_byte_option_impl;
use ssz::Encode;
use ssz::four_byte_option_impl;
use ssz_derive::{Decode, Encode};
use std::collections::{HashMap, HashSet};
use superstruct::superstruct;
use tracing::info;
use types::{
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash,
FixedBytesExtended, Hash256, Slot,
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
Slot,
};
// Define a "legacy" implementation of `Option<usize>` which uses four bytes for encoding the union
@@ -131,8 +132,6 @@ pub struct ProtoArray {
/// Do not attempt to prune the tree unless it has at least this many nodes. Small prunes
/// simply waste time.
pub prune_threshold: usize,
pub justified_checkpoint: Checkpoint,
pub finalized_checkpoint: Checkpoint,
pub nodes: Vec<ProtoNode>,
pub indices: HashMap<Hash256, usize>,
pub unsatisfied_inclusion_list_blocks: HashMap<Slot, Hash256>,
@@ -157,8 +156,8 @@ impl ProtoArray {
pub fn apply_score_changes<E: EthSpec>(
&mut self,
mut deltas: Vec<i64>,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
new_justified_balances: &JustifiedBalances,
proposer_boost_root: Hash256,
current_slot: Slot,
@@ -171,13 +170,6 @@ impl ProtoArray {
});
}
if justified_checkpoint != self.justified_checkpoint
|| finalized_checkpoint != self.finalized_checkpoint
{
self.justified_checkpoint = justified_checkpoint;
self.finalized_checkpoint = finalized_checkpoint;
}
// Default the proposer boost score to zero.
let mut proposer_score = 0;
@@ -242,21 +234,18 @@ impl ProtoArray {
// the delta by the new score amount (unless the block has an invalid execution status).
//
// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_latest_attesting_balance
if let Some(proposer_score_boost) = spec.proposer_score_boost {
if proposer_boost_root != Hash256::zero()
if let Some(proposer_score_boost) = spec.proposer_score_boost
&& proposer_boost_root != Hash256::zero()
&& proposer_boost_root == node.root
// Invalid nodes (or their ancestors) should not receive a proposer boost.
&& !execution_status_is_invalid
{
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))?;
}
{
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))?;
}
// Apply the delta to the node.
@@ -318,6 +307,8 @@ impl ProtoArray {
parent_index,
node_index,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
)?;
}
}
@@ -328,7 +319,13 @@ impl ProtoArray {
/// Register a block with the fork choice.
///
/// It is only sane to supply a `None` parent for the genesis block.
pub fn on_block<E: EthSpec>(&mut self, block: Block, current_slot: Slot) -> Result<(), Error> {
pub fn on_block<E: EthSpec>(
&mut self,
block: Block,
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
) -> Result<(), Error> {
// If the block is already known, simply ignore it.
if self.indices.contains_key(&block.root) {
return Ok(());
@@ -379,6 +376,8 @@ impl ProtoArray {
parent_index,
node_index,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
)?;
if matches!(block.execution_status, ExecutionStatus::Valid(_)) {
@@ -447,7 +446,7 @@ impl ProtoArray {
return Err(Error::InvalidAncestorOfValidPayload {
ancestor_block_root: node.root,
ancestor_payload_block_hash,
})
});
}
};
@@ -461,6 +460,7 @@ impl ProtoArray {
pub fn propagate_execution_payload_invalidation<E: EthSpec>(
&mut self,
op: &InvalidationOperation,
best_finalized_checkpoint: Checkpoint,
) -> Result<(), Error> {
let mut invalidated_indices: HashSet<usize> = <_>::default();
let head_block_root = op.block_root();
@@ -489,7 +489,10 @@ impl ProtoArray {
let latest_valid_ancestor_is_descendant =
latest_valid_ancestor_root.is_some_and(|ancestor_root| {
self.is_descendant(ancestor_root, head_block_root)
&& self.is_finalized_checkpoint_or_descendant::<E>(ancestor_root)
&& self.is_finalized_checkpoint_or_descendant::<E>(
ancestor_root,
best_finalized_checkpoint,
)
});
// Collect all *ancestors* which were declared invalid since they reside between the
@@ -556,7 +559,7 @@ impl ProtoArray {
return Err(Error::ValidExecutionStatusBecameInvalid {
block_root: node.root,
payload_block_hash: *hash,
})
});
}
ExecutionStatus::Optimistic(hash) => {
invalidated_indices.insert(index);
@@ -613,27 +616,27 @@ impl ProtoArray {
.get_mut(index)
.ok_or(Error::InvalidNodeIndex(index))?;
if let Some(parent_index) = node.parent {
if invalidated_indices.contains(&parent_index) {
match &node.execution_status {
ExecutionStatus::Valid(hash) => {
return Err(Error::ValidExecutionStatusBecameInvalid {
block_root: node.root,
payload_block_hash: *hash,
})
}
ExecutionStatus::Optimistic(hash) | ExecutionStatus::Invalid(hash) => {
node.execution_status = ExecutionStatus::Invalid(*hash)
}
ExecutionStatus::Irrelevant(_) => {
return Err(Error::IrrelevantDescendant {
block_root: node.root,
})
}
if let Some(parent_index) = node.parent
&& invalidated_indices.contains(&parent_index)
{
match &node.execution_status {
ExecutionStatus::Valid(hash) => {
return Err(Error::ValidExecutionStatusBecameInvalid {
block_root: node.root,
payload_block_hash: *hash,
});
}
ExecutionStatus::Optimistic(hash) | ExecutionStatus::Invalid(hash) => {
node.execution_status = ExecutionStatus::Invalid(*hash)
}
ExecutionStatus::Irrelevant(_) => {
return Err(Error::IrrelevantDescendant {
block_root: node.root,
});
}
invalidated_indices.insert(index);
}
invalidated_indices.insert(index);
}
}
@@ -652,6 +655,8 @@ impl ProtoArray {
&self,
justified_root: &Hash256,
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
) -> Result<Hash256, Error> {
let justified_index = self
.indices
@@ -685,12 +690,17 @@ impl ProtoArray {
.ok_or(Error::InvalidBestDescendant(best_descendant_index))?;
// Perform a sanity check that the node is indeed valid to be the head.
if !self.node_is_viable_for_head::<E>(best_node, current_slot) {
if !self.node_is_viable_for_head::<E>(
best_node,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
) {
return Err(Error::InvalidBestNode(Box::new(InvalidBestNodeInfo {
current_slot,
start_root: *justified_root,
justified_checkpoint: self.justified_checkpoint,
finalized_checkpoint: self.finalized_checkpoint,
justified_checkpoint: best_justified_checkpoint,
finalized_checkpoint: best_finalized_checkpoint,
head_root: best_node.root,
head_justified_checkpoint: best_node.justified_checkpoint,
head_finalized_checkpoint: best_node.finalized_checkpoint,
@@ -787,6 +797,8 @@ impl ProtoArray {
parent_index: usize,
child_index: usize,
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
) -> Result<(), Error> {
let child = self
.nodes
@@ -798,8 +810,12 @@ impl ProtoArray {
.get(parent_index)
.ok_or(Error::InvalidNodeIndex(parent_index))?;
let child_leads_to_viable_head =
self.node_leads_to_viable_head::<E>(child, current_slot)?;
let child_leads_to_viable_head = self.node_leads_to_viable_head::<E>(
child,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
)?;
// These three variables are aliases to the three options that we may set the
// `parent.best_child` and `parent.best_descendant` to.
@@ -828,8 +844,12 @@ impl ProtoArray {
.get(best_child_index)
.ok_or(Error::InvalidBestDescendant(best_child_index))?;
let best_child_leads_to_viable_head =
self.node_leads_to_viable_head::<E>(best_child, current_slot)?;
let best_child_leads_to_viable_head = self.node_leads_to_viable_head::<E>(
best_child,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
)?;
if child_leads_to_viable_head && !best_child_leads_to_viable_head {
// The child leads to a viable head, but the current best-child doesn't.
@@ -878,6 +898,8 @@ impl ProtoArray {
&self,
node: &ProtoNode,
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
) -> Result<bool, Error> {
let best_descendant_is_viable_for_head =
if let Some(best_descendant_index) = node.best_descendant {
@@ -886,13 +908,23 @@ impl ProtoArray {
.get(best_descendant_index)
.ok_or(Error::InvalidBestDescendant(best_descendant_index))?;
self.node_is_viable_for_head::<E>(best_descendant, current_slot)
self.node_is_viable_for_head::<E>(
best_descendant,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
)
} else {
false
};
Ok(best_descendant_is_viable_for_head
|| self.node_is_viable_for_head::<E>(node, current_slot))
|| self.node_is_viable_for_head::<E>(
node,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
))
}
/// This is the equivalent to the `filter_block_tree` function in the eth2 spec:
@@ -901,7 +933,13 @@ impl ProtoArray {
///
/// Any node that has a different finalized or justified epoch should not be viable for the
/// head.
fn node_is_viable_for_head<E: EthSpec>(&self, node: &ProtoNode, current_slot: Slot) -> bool {
fn node_is_viable_for_head<E: EthSpec>(
&self,
node: &ProtoNode,
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
) -> bool {
if node.execution_status.is_invalid() {
return false;
}
@@ -938,12 +976,13 @@ impl ProtoArray {
node_justified_checkpoint
};
let correct_justified = self.justified_checkpoint.epoch == genesis_epoch
|| voting_source.epoch == self.justified_checkpoint.epoch
let correct_justified = best_justified_checkpoint.epoch == genesis_epoch
|| voting_source.epoch == best_justified_checkpoint.epoch
|| voting_source.epoch + 2 >= current_epoch;
let correct_finalized = self.finalized_checkpoint.epoch == genesis_epoch
|| self.is_finalized_checkpoint_or_descendant::<E>(node.root);
let correct_finalized = best_finalized_checkpoint.epoch == genesis_epoch
|| self
.is_finalized_checkpoint_or_descendant::<E>(node.root, best_finalized_checkpoint);
correct_justified && correct_finalized
}
@@ -998,10 +1037,13 @@ impl ProtoArray {
///
/// Notably, this function is checking ancestory of the finalized
/// *checkpoint* not the finalized *block*.
pub fn is_finalized_checkpoint_or_descendant<E: EthSpec>(&self, root: Hash256) -> bool {
let finalized_root = self.finalized_checkpoint.root;
let finalized_slot = self
.finalized_checkpoint
pub fn is_finalized_checkpoint_or_descendant<E: EthSpec>(
&self,
root: Hash256,
best_finalized_checkpoint: Checkpoint,
) -> bool {
let finalized_root = best_finalized_checkpoint.root;
let finalized_slot = best_finalized_checkpoint
.epoch
.start_slot(E::slots_per_epoch());
@@ -1024,7 +1066,7 @@ impl ProtoArray {
// If the conditions don't match for this node then they're unlikely to
// start matching for its ancestors.
for checkpoint in &[node.finalized_checkpoint, node.justified_checkpoint] {
if checkpoint == &self.finalized_checkpoint {
if checkpoint == &best_finalized_checkpoint {
return true;
}
}
@@ -1033,7 +1075,7 @@ impl ProtoArray {
node.unrealized_finalized_checkpoint,
node.unrealized_justified_checkpoint,
] {
if checkpoint.is_some_and(|cp| cp == self.finalized_checkpoint) {
if checkpoint.is_some_and(|cp| cp == best_finalized_checkpoint) {
return true;
}
}
@@ -1081,12 +1123,18 @@ impl ProtoArray {
/// For informational purposes like the beacon HTTP API, we use this as the list of known heads,
/// even though some of them might not be viable. We do this to maintain consistency between the
/// definition of "head" used by pruning (which does not consider viability) and fork choice.
pub fn heads_descended_from_finalization<E: EthSpec>(&self) -> Vec<&ProtoNode> {
pub fn heads_descended_from_finalization<E: EthSpec>(
&self,
best_finalized_checkpoint: Checkpoint,
) -> Vec<&ProtoNode> {
self.nodes
.iter()
.filter(|node| {
node.best_child.is_none()
&& self.is_finalized_checkpoint_or_descendant::<E>(node.root)
&& self.is_finalized_checkpoint_or_descendant::<E>(
node.root,
best_finalized_checkpoint,
)
})
.collect()
}

View File

@@ -1,12 +1,13 @@
use crate::{
JustifiedBalances,
error::Error,
proto_array::{
calculate_committee_fraction, InvalidationOperation, Iter, ProposerBoost, ProtoArray,
ProtoNode,
InvalidationOperation, Iter, ProposerBoost, ProtoArray, ProtoNode,
calculate_committee_fraction,
},
ssz_container::SszContainer,
JustifiedBalances,
};
use fixed_bytes::FixedBytesExtended;
use serde::{Deserialize, Serialize};
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
@@ -15,8 +16,8 @@ use std::{
fmt,
};
use types::{
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash,
FixedBytesExtended, Hash256, Slot,
AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256,
Slot,
};
pub const DEFAULT_PRUNE_THRESHOLD: usize = 256;
@@ -160,6 +161,56 @@ pub struct Block {
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
}
impl Block {
/// Compute the proposer shuffling decision root of a child block in `child_block_epoch`.
///
/// This function assumes that `child_block_epoch >= self.epoch`. It is the responsibility of
/// the caller to check this condition, or else incorrect results will be produced.
pub fn proposer_shuffling_root_for_child_block(
&self,
child_block_epoch: Epoch,
spec: &ChainSpec,
) -> Hash256 {
let block_epoch = self.current_epoch_shuffling_id.shuffling_epoch;
// For child blocks in the Fulu fork epoch itself, we want to use the old logic. There is no
// lookahead in the first Fulu epoch. So we check whether Fulu is enabled at
// `child_block_epoch - 1`, i.e. whether `child_block_epoch > fulu_fork_epoch`.
if !spec
.fork_name_at_epoch(child_block_epoch.saturating_sub(1_u64))
.fulu_enabled()
{
// Prior to Fulu the proposer shuffling decision root for the current epoch is the same
// as the attestation shuffling for the *next* epoch, i.e. it is determined at the start
// of the current epoch.
if block_epoch == child_block_epoch {
self.next_epoch_shuffling_id.shuffling_decision_block
} else {
// Otherwise, the child block epoch is greater, so its decision root is its parent
// root itself (this block's root).
self.root
}
} else {
// After Fulu the proposer shuffling is determined with lookahead, so if the block
// lies in the same epoch as its parent, its decision root is the same as the
// parent's current epoch attester shuffling
//
// i.e. the block from the end of epoch N - 2.
if child_block_epoch == block_epoch {
self.current_epoch_shuffling_id.shuffling_decision_block
} else if child_block_epoch == block_epoch + 1 {
// If the block is the next epoch, then it instead shares its decision root with
// the parent's *next epoch* attester shuffling.
self.next_epoch_shuffling_id.shuffling_decision_block
} else {
// The child block lies in the future beyond the lookahead, at the point where this
// block (its parent) will be the decision block.
self.root
}
}
}
}
/// A Vec-wrapper which will grow to match any request.
///
/// E.g., a `get` or `insert` to an out-of-bounds element will cause the Vec to grow (using
@@ -375,8 +426,6 @@ impl ProtoArrayForkChoice {
) -> Result<Self, String> {
let mut proto_array = ProtoArray {
prune_threshold: DEFAULT_PRUNE_THRESHOLD,
justified_checkpoint,
finalized_checkpoint,
nodes: Vec::with_capacity(1),
indices: HashMap::with_capacity(1),
unsatisfied_inclusion_list_blocks,
@@ -401,7 +450,12 @@ impl ProtoArrayForkChoice {
};
proto_array
.on_block::<E>(block, current_slot)
.on_block::<E>(
block,
current_slot,
justified_checkpoint,
finalized_checkpoint,
)
.map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?;
Ok(Self {
@@ -425,9 +479,10 @@ impl ProtoArrayForkChoice {
pub fn process_execution_payload_invalidation<E: EthSpec>(
&mut self,
op: &InvalidationOperation,
finalized_checkpoint: Checkpoint,
) -> Result<(), String> {
self.proto_array
.propagate_execution_payload_invalidation::<E>(op)
.propagate_execution_payload_invalidation::<E>(op, finalized_checkpoint)
.map_err(|e| format!("Failed to process invalid payload: {:?}", e))
}
@@ -451,13 +506,20 @@ impl ProtoArrayForkChoice {
&mut self,
block: Block,
current_slot: Slot,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> Result<(), String> {
if block.parent_root.is_none() {
return Err("Missing parent root".to_string());
}
self.proto_array
.on_block::<E>(block, current_slot)
.on_block::<E>(
block,
current_slot,
justified_checkpoint,
finalized_checkpoint,
)
.map_err(|e| format!("process_block_error: {:?}", e))
}
@@ -499,7 +561,12 @@ impl ProtoArrayForkChoice {
*old_balances = new_balances.clone();
self.proto_array
.find_head::<E>(&justified_checkpoint.root, current_slot)
.find_head::<E>(
&justified_checkpoint.root,
current_slot,
justified_checkpoint,
finalized_checkpoint,
)
.map_err(|e| format!("find_head failed: {:?}", e))
}
@@ -707,24 +774,22 @@ impl ProtoArrayForkChoice {
// If the invalid root was boosted, apply the weight to it and
// ancestors.
if let Some(proposer_score_boost) = spec.proposer_score_boost {
if self.proto_array.previous_proposer_boost.root == node.root {
// 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_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;
// Apply this boost to this node.
restored_weight = restored_weight
.checked_add(proposer_score)
.ok_or("Overflow when adding boost to weight")?;
}
if let Some(proposer_score_boost) = spec.proposer_score_boost
&& self.proto_array.previous_proposer_boost.root == node.root
{
// 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_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;
// Apply this boost to this node.
restored_weight = restored_weight
.checked_add(proposer_score)
.ok_or("Overflow when adding boost to weight")?;
}
// Add the restored weight to the node and all ancestors.
@@ -838,9 +903,10 @@ impl ProtoArrayForkChoice {
pub fn is_finalized_checkpoint_or_descendant<E: EthSpec>(
&self,
descendant_root: Hash256,
best_finalized_checkpoint: Checkpoint,
) -> bool {
self.proto_array
.is_finalized_checkpoint_or_descendant::<E>(descendant_root)
.is_finalized_checkpoint_or_descendant::<E>(descendant_root, best_finalized_checkpoint)
}
pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> {
@@ -858,7 +924,7 @@ impl ProtoArrayForkChoice {
}
/// See `ProtoArray::iter_nodes`
pub fn iter_nodes(&self, block_root: &Hash256) -> Iter {
pub fn iter_nodes(&self, block_root: &Hash256) -> Iter<'_> {
self.proto_array.iter_nodes(block_root)
}
@@ -866,18 +932,38 @@ impl ProtoArrayForkChoice {
pub fn iter_block_roots(
&self,
block_root: &Hash256,
) -> impl Iterator<Item = (Hash256, Slot)> + use<'_> {
) -> impl Iterator<Item = (Hash256, Slot)> + '_ {
self.proto_array.iter_block_roots(block_root)
}
pub fn as_bytes(&self) -> Vec<u8> {
SszContainer::from(self).as_ssz_bytes()
pub fn as_ssz_container(
&self,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> SszContainer {
SszContainer::from_proto_array(self, justified_checkpoint, finalized_checkpoint)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
pub fn as_bytes(
&self,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> Vec<u8> {
self.as_ssz_container(justified_checkpoint, finalized_checkpoint)
.as_ssz_bytes()
}
pub fn from_bytes(bytes: &[u8], balances: JustifiedBalances) -> Result<Self, String> {
let container = SszContainer::from_ssz_bytes(bytes)
.map_err(|e| format!("Failed to decode ProtoArrayForkChoice: {:?}", e))?;
container
Self::from_container(container, balances)
}
pub fn from_container(
container: SszContainer,
balances: JustifiedBalances,
) -> Result<Self, String> {
(container, balances)
.try_into()
.map_err(|e| format!("Failed to initialize ProtoArrayForkChoice: {e:?}"))
}
@@ -897,8 +983,12 @@ impl ProtoArrayForkChoice {
}
/// Returns all nodes that have zero children and are descended from the finalized checkpoint.
pub fn heads_descended_from_finalization<E: EthSpec>(&self) -> Vec<&ProtoNode> {
self.proto_array.heads_descended_from_finalization::<E>()
pub fn heads_descended_from_finalization<E: EthSpec>(
&self,
best_finalized_checkpoint: Checkpoint,
) -> Vec<&ProtoNode> {
self.proto_array
.heads_descended_from_finalization::<E>(best_finalized_checkpoint)
}
}
@@ -1008,7 +1098,8 @@ fn compute_deltas(
#[cfg(test)]
mod test_compute_deltas {
use super::*;
use types::{FixedBytesExtended, MainnetEthSpec};
use fixed_bytes::FixedBytesExtended;
use types::MainnetEthSpec;
/// Gives a hash that is not the zero hash (unless i is `usize::MAX)`.
fn hash_from_index(i: usize) -> Hash256 {
@@ -1069,6 +1160,8 @@ mod test_compute_deltas {
unrealized_finalized_checkpoint: Some(genesis_checkpoint),
},
genesis_slot + 1,
genesis_checkpoint,
genesis_checkpoint,
)
.unwrap();
@@ -1092,6 +1185,8 @@ mod test_compute_deltas {
unrealized_finalized_checkpoint: None,
},
genesis_slot + 1,
genesis_checkpoint,
genesis_checkpoint,
)
.unwrap();
@@ -1105,10 +1200,24 @@ mod test_compute_deltas {
assert!(!fc.is_descendant(finalized_root, not_finalized_desc));
assert!(!fc.is_descendant(finalized_root, unknown));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(finalized_root));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(finalized_desc));
assert!(!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(not_finalized_desc));
assert!(!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(unknown));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
finalized_root,
genesis_checkpoint
));
assert!(fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
finalized_desc,
genesis_checkpoint
));
assert!(!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
not_finalized_desc,
genesis_checkpoint
));
assert!(
!fc.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
unknown,
genesis_checkpoint
)
);
assert!(!fc.is_descendant(finalized_desc, not_finalized_desc));
assert!(fc.is_descendant(finalized_desc, finalized_desc));
@@ -1205,6 +1314,8 @@ mod test_compute_deltas {
unrealized_finalized_checkpoint: Some(genesis_checkpoint),
},
Slot::from(block.slot),
genesis_checkpoint,
genesis_checkpoint,
)
.unwrap();
};
@@ -1259,29 +1370,34 @@ mod test_compute_deltas {
// Set the finalized checkpoint to finalize the first slot of epoch 1 on
// the canonical chain.
fc.proto_array.finalized_checkpoint = Checkpoint {
let finalized_checkpoint = Checkpoint {
root: finalized_root,
epoch: Epoch::new(1),
};
assert!(
fc.proto_array
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(finalized_root),
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
finalized_root,
finalized_checkpoint
),
"the finalized checkpoint is the finalized checkpoint"
);
assert!(
fc.proto_array
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(get_block_root(
canonical_slot
)),
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
get_block_root(canonical_slot),
finalized_checkpoint
),
"the canonical block is a descendant of the finalized checkpoint"
);
assert!(
!fc.proto_array
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(get_block_root(
non_canonical_slot
)),
.is_finalized_checkpoint_or_descendant::<MainnetEthSpec>(
get_block_root(non_canonical_slot),
finalized_checkpoint
),
"although the non-canonical block is a descendant of the finalized block, \
it's not a descendant of the finalized checkpoint"
);

View File

@@ -1,10 +1,10 @@
use crate::proto_array::ProposerBoost;
use crate::{
Error, JustifiedBalances,
proto_array::{ProtoArray, ProtoNodeV17},
proto_array_fork_choice::{ElasticList, ProtoArrayForkChoice, VoteTracker},
Error, JustifiedBalances,
};
use ssz::{four_byte_option_impl, Encode};
use ssz::{Encode, four_byte_option_impl};
use ssz_derive::{Decode, Encode};
use std::collections::HashMap;
use superstruct::superstruct;
@@ -14,32 +14,41 @@ use types::{Checkpoint, Hash256, Slot};
// selector.
four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint);
pub type SszContainer = SszContainerV17;
pub type SszContainer = SszContainerV28;
#[superstruct(variants(V17), variant_attributes(derive(Encode, Decode)), no_enum)]
#[superstruct(
variants(V17, V28),
variant_attributes(derive(Encode, Decode, Clone)),
no_enum
)]
pub struct SszContainer {
pub votes: Vec<VoteTracker>,
#[superstruct(only(V17))]
pub balances: Vec<u64>,
pub prune_threshold: usize,
pub justified_checkpoint: Checkpoint,
pub finalized_checkpoint: Checkpoint,
#[superstruct(only(V17))]
// Deprecated, remove in a future schema migration
justified_checkpoint: Checkpoint,
// Deprecated, remove in a future schema migration
finalized_checkpoint: Checkpoint,
pub nodes: Vec<ProtoNodeV17>,
pub indices: Vec<(Hash256, usize)>,
pub previous_proposer_boost: ProposerBoost,
pub unsatisfied_inclusion_list_blocks: Vec<(Slot, Hash256)>,
}
impl From<&ProtoArrayForkChoice> for SszContainer {
fn from(from: &ProtoArrayForkChoice) -> Self {
impl SszContainer {
pub fn from_proto_array(
from: &ProtoArrayForkChoice,
justified_checkpoint: Checkpoint,
finalized_checkpoint: Checkpoint,
) -> Self {
let proto_array = &from.proto_array;
Self {
votes: from.votes.0.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,
justified_checkpoint,
finalized_checkpoint,
nodes: proto_array.nodes.clone(),
indices: proto_array.indices.iter().map(|(k, v)| (*k, *v)).collect(),
previous_proposer_boost: proto_array.previous_proposer_boost,
@@ -52,14 +61,12 @@ impl From<&ProtoArrayForkChoice> for SszContainer {
}
}
impl TryFrom<SszContainer> for ProtoArrayForkChoice {
impl TryFrom<(SszContainer, JustifiedBalances)> for ProtoArrayForkChoice {
type Error = Error;
fn try_from(from: SszContainer) -> Result<Self, Error> {
fn try_from((from, balances): (SszContainer, JustifiedBalances)) -> Result<Self, Error> {
let proto_array = ProtoArray {
prune_threshold: from.prune_threshold,
justified_checkpoint: from.justified_checkpoint,
finalized_checkpoint: from.finalized_checkpoint,
nodes: from.nodes,
indices: from.indices.into_iter().collect::<HashMap<_, _>>(),
previous_proposer_boost: from.previous_proposer_boost,
@@ -72,7 +79,40 @@ impl TryFrom<SszContainer> for ProtoArrayForkChoice {
Ok(Self {
proto_array,
votes: ElasticList(from.votes),
balances: JustifiedBalances::from_effective_balances(from.balances)?,
balances,
})
}
}
// Convert V17 to V28 by dropping balances.
impl From<SszContainerV17> for SszContainerV28 {
fn from(v17: SszContainerV17) -> Self {
Self {
votes: v17.votes,
prune_threshold: v17.prune_threshold,
justified_checkpoint: v17.justified_checkpoint,
finalized_checkpoint: v17.finalized_checkpoint,
nodes: v17.nodes,
indices: v17.indices,
previous_proposer_boost: v17.previous_proposer_boost,
unsatisfied_inclusion_list_blocks: v17.unsatisfied_inclusion_list_blocks,
}
}
}
// Convert V28 to V17 by re-adding balances.
impl From<(SszContainerV28, JustifiedBalances)> for SszContainerV17 {
fn from((v28, balances): (SszContainerV28, JustifiedBalances)) -> Self {
Self {
votes: v28.votes,
balances: balances.effective_balances.clone(),
prune_threshold: v28.prune_threshold,
justified_checkpoint: v28.justified_checkpoint,
finalized_checkpoint: v28.finalized_checkpoint,
nodes: v28.nodes,
indices: v28.indices,
previous_proposer_boost: v28.previous_proposer_boost,
unsatisfied_inclusion_list_blocks: v28.unsatisfied_inclusion_list_blocks,
}
}
}