use crate::error::InvalidBestNodeInfo; use crate::{Block, ExecutionStatus, JustifiedBalances, PayloadStatus, error::Error}; use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz::four_byte_option_impl; use ssz_derive::{Decode, Encode}; use std::collections::{HashMap, HashSet}; use superstruct::superstruct; use types::{ AttestationShufflingId, ChainSpec, Checkpoint, Epoch, EthSpec, ExecutionBlockHash, Hash256, Slot, }; // Define a "legacy" implementation of `Option` which uses four bytes for encoding the union // selector. four_byte_option_impl!(four_byte_option_usize, usize); four_byte_option_impl!(four_byte_option_checkpoint, Checkpoint); /// Defines an operation which may invalidate the `execution_status` of some nodes. #[derive(Clone, Debug)] pub enum InvalidationOperation { /// Invalidate only `block_root` and it's descendants. Don't invalidate any ancestors. InvalidateOne { block_root: Hash256 }, /// Invalidate blocks between `head_block_root` and `latest_valid_ancestor`. /// /// If the `latest_valid_ancestor` is known to fork choice, invalidate all blocks between /// `head_block_root` and `latest_valid_ancestor`. The `head_block_root` will be invalidated, /// whilst the `latest_valid_ancestor` will not. /// /// If `latest_valid_ancestor` is *not* known to fork choice, only invalidate the /// `head_block_root` if `always_invalidate_head == true`. InvalidateMany { head_block_root: Hash256, always_invalidate_head: bool, latest_valid_ancestor: ExecutionBlockHash, }, } impl InvalidationOperation { pub fn block_root(&self) -> Hash256 { match self { InvalidationOperation::InvalidateOne { block_root } => *block_root, InvalidationOperation::InvalidateMany { head_block_root, .. } => *head_block_root, } } pub fn latest_valid_ancestor(&self) -> Option { match self { InvalidationOperation::InvalidateOne { .. } => None, InvalidationOperation::InvalidateMany { latest_valid_ancestor, .. } => Some(*latest_valid_ancestor), } } pub fn invalidate_block_root(&self) -> bool { match self { InvalidationOperation::InvalidateOne { .. } => true, InvalidationOperation::InvalidateMany { always_invalidate_head, .. } => *always_invalidate_head, } } } #[superstruct( variants(V17, V29), variant_attributes(derive(Clone, PartialEq, Debug, Encode, Decode, Serialize, Deserialize)) )] #[derive(PartialEq, Debug, Encode, Decode, Serialize, Deserialize, Clone)] #[ssz(enum_behaviour = "union")] pub struct ProtoNode { /// The `slot` is not necessary for `ProtoArray`, it just exists so external components can /// easily query the block slot. This is useful for upstream fork choice logic. #[superstruct(getter(copy))] pub slot: Slot, /// The `state_root` is not necessary for `ProtoArray` either, it also just exists for upstream /// components (namely attestation verification). #[superstruct(getter(copy))] pub state_root: Hash256, /// The root that would be used for the `attestation.data.target.root` if a LMD vote was cast /// for this block. /// /// The `target_root` is not necessary for `ProtoArray` either, it also just exists for upstream /// components (namely fork choice attestation verification). #[superstruct(getter(copy))] pub target_root: Hash256, pub current_epoch_shuffling_id: AttestationShufflingId, pub next_epoch_shuffling_id: AttestationShufflingId, #[superstruct(getter(copy))] pub root: Hash256, #[superstruct(getter(copy))] #[ssz(with = "four_byte_option_usize")] pub parent: Option, #[superstruct(only(V17, V29), partial_getter(copy))] pub justified_checkpoint: Checkpoint, #[superstruct(only(V17, V29), partial_getter(copy))] pub finalized_checkpoint: Checkpoint, #[superstruct(getter(copy))] pub weight: u64, #[superstruct(getter(copy))] #[ssz(with = "four_byte_option_usize")] pub best_child: Option, #[superstruct(getter(copy))] #[ssz(with = "four_byte_option_usize")] pub best_descendant: Option, /// Indicates if an execution node has marked this block as valid. Also contains the execution /// block hash. This is only used pre-Gloas. #[superstruct(only(V17), partial_getter(copy))] pub execution_status: ExecutionStatus, #[superstruct(getter(copy))] #[ssz(with = "four_byte_option_checkpoint")] pub unrealized_justified_checkpoint: Option, #[superstruct(getter(copy))] #[ssz(with = "four_byte_option_checkpoint")] pub unrealized_finalized_checkpoint: Option, /// We track the parent payload status from which the current node was extended. #[superstruct(only(V29), partial_getter(copy))] pub parent_payload_status: PayloadStatus, #[superstruct(only(V29), partial_getter(copy))] pub empty_payload_weight: u64, #[superstruct(only(V29), partial_getter(copy))] pub full_payload_weight: u64, #[superstruct(only(V29), partial_getter(copy))] pub execution_payload_block_hash: ExecutionBlockHash, /// PTC timeliness vote bitfield, indexed by PTC committee position. /// Bit i set means PTC member i voted `payload_present = true`. /// Tiebreak derived as: `count_ones() > ptc_size / 2`. #[superstruct(only(V29))] pub payload_timeliness_votes: Vec, /// PTC data availability vote bitfield, indexed by PTC committee position. /// Bit i set means PTC member i voted `blob_data_available = true`. /// Tiebreak derived as: `count_ones() > ptc_size / 2`. #[superstruct(only(V29))] pub payload_data_availability_votes: Vec, } #[derive(PartialEq, Debug, Encode, Decode, Serialize, Deserialize, Copy, Clone)] pub struct ProposerBoost { pub root: Hash256, pub score: u64, } impl Default for ProposerBoost { fn default() -> Self { Self { root: Hash256::zero(), score: 0, } } } #[derive(Clone, PartialEq, Debug, Copy)] pub struct NodeDelta { pub delta: i64, pub empty_delta: i64, pub full_delta: i64, } impl NodeDelta { /// Determine the payload bucket for a vote based on whether the vote's slot matches the /// block's slot (Pending), or the vote's `payload_present` flag (Full/Empty). pub fn payload_status( vote_slot: Slot, payload_present: bool, block_slot: Slot, ) -> PayloadStatus { if vote_slot == block_slot { PayloadStatus::Pending } else if payload_present { PayloadStatus::Full } else { PayloadStatus::Empty } } /// Add a balance to the appropriate payload status. pub fn add_payload_delta( &mut self, status: PayloadStatus, balance: u64, index: usize, ) -> Result<(), Error> { let field = match status { PayloadStatus::Full => &mut self.full_delta, PayloadStatus::Empty => &mut self.empty_delta, PayloadStatus::Pending => return Ok(()), }; *field = field .checked_add(balance as i64) .ok_or(Error::DeltaOverflow(index))?; Ok(()) } /// Create a delta that only affects the aggregate `delta` field. pub fn from_delta(delta: i64) -> Self { Self { delta, empty_delta: 0, full_delta: 0, } } /// Subtract a balance from the appropriate payload status. pub fn sub_payload_delta( &mut self, status: PayloadStatus, balance: u64, index: usize, ) -> Result<(), Error> { let field = match status { PayloadStatus::Full => &mut self.full_delta, PayloadStatus::Empty => &mut self.empty_delta, PayloadStatus::Pending => return Ok(()), }; *field = field .checked_sub(balance as i64) .ok_or(Error::DeltaOverflow(index))?; Ok(()) } } /// Compare NodeDelta with i64 by comparing the aggregate `delta` field. /// This is used by tests that only care about the total weight delta. impl PartialEq for NodeDelta { fn eq(&self, other: &i64) -> bool { self.delta == *other } } #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] 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 nodes: Vec, pub indices: HashMap, pub previous_proposer_boost: ProposerBoost, } impl ProtoArray { /// Iterate backwards through the array, touching all nodes and their parents and potentially /// the best-child of each parent. /// /// The structure of the `self.nodes` array ensures that the child of each node is always /// touched before its parent. /// /// For each node, the following is done: /// /// - Update the node's weight with the corresponding delta. /// - Back-propagate each node's delta to its parents delta. /// - Compare the current node with the parents best-child, updating it if the current node /// should become the best child. /// - If required, update the parents best-descendant with the current node or its best-descendant. #[allow(clippy::too_many_arguments)] pub fn apply_score_changes( &mut self, mut deltas: Vec, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, new_justified_balances: &JustifiedBalances, proposer_boost_root: Hash256, current_slot: Slot, spec: &ChainSpec, ) -> Result<(), Error> { if deltas.len() != self.indices.len() { return Err(Error::InvalidDeltaLen { deltas: deltas.len(), indices: self.indices.len(), }); } // Default the proposer boost score to zero. let mut proposer_score = 0; // Iterate backwards through all indices in `self.nodes`. for node_index in (0..self.nodes.len()).rev() { let node = self .nodes .get_mut(node_index) .ok_or(Error::InvalidNodeIndex(node_index))?; // There is no need to adjust the balances or manage parent of the zero hash since it // is an alias to the genesis block. The weight applied to the genesis block is // irrelevant as we _always_ choose it and it's impossible for it to have a parent. if node.root() == Hash256::zero() { continue; } let execution_status_is_invalid = if let Ok(proto_node) = node.as_v17() && proto_node.execution_status.is_invalid() { true } else { false }; let node_delta = deltas .get(node_index) .copied() .ok_or(Error::InvalidNodeDelta(node_index))?; let mut delta = if execution_status_is_invalid { // If the node has an invalid execution payload, reduce its weight to zero. 0_i64 .checked_sub(node.weight() as i64) .ok_or(Error::InvalidExecutionDeltaOverflow(node_index))? } else { node_delta.delta }; let (node_empty_delta, node_full_delta) = if node.as_v29().is_ok() { (node_delta.empty_delta, node_delta.full_delta) } else { (0, 0) }; // If we find the node for which the proposer boost was previously applied, decrease // the delta by the previous score amount. // TODO(gloas): implement `should_apply_proposer_boost` from the Gloas spec. // The spec conditionally applies proposer boost based on parent weakness and // early equivocations. Currently boost is applied unconditionally. if self.previous_proposer_boost.root != Hash256::zero() && self.previous_proposer_boost.root == node.root() // Invalid nodes will always have a weight of zero so there's no need to subtract // the proposer boost delta. && !execution_status_is_invalid { delta = delta .checked_sub(self.previous_proposer_boost.score as i64) .ok_or(Error::DeltaOverflow(node_index))?; } // If we find the node matching the current proposer boost root, increase // the delta by the new score amount (unless the block has an invalid execution status). // // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_latest_attesting_balance // // TODO(gloas): proposer boost should also be subtracted from `empty_delta` per spec, // since the spec creates a virtual vote with `payload_present=False` for the proposer // boost, biasing toward Empty for non-current-slot payload decisions. 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::(new_justified_balances, proposer_score_boost) .ok_or(Error::ProposerBoostOverflow(node_index))?; delta = delta .checked_add(proposer_score as i64) .ok_or(Error::DeltaOverflow(node_index))?; } // Apply the delta to the node. if execution_status_is_invalid { *node.weight_mut() = 0; } else { *node.weight_mut() = apply_delta(node.weight(), delta, node_index)?; } // Apply post-Gloas score deltas. if let Ok(node) = node.as_v29_mut() { node.empty_payload_weight = apply_delta(node.empty_payload_weight, node_empty_delta, node_index)?; node.full_payload_weight = apply_delta(node.full_payload_weight, node_full_delta, node_index)?; } // Update the parent delta (if any). if let Some(parent_index) = node.parent() { let parent_delta = deltas .get_mut(parent_index) .ok_or(Error::InvalidParentDelta(parent_index))?; // Back-propagate the node's delta to its parent. parent_delta.delta = parent_delta .delta .checked_add(delta) .ok_or(Error::DeltaOverflow(parent_index))?; // Per spec's `is_supporting_vote`: a vote for descendant B supports // ancestor A's payload status based on B's `parent_payload_status`. // Route the child's *total* weight delta to the parent's appropriate // payload bucket. match node.parent_payload_status() { Ok(PayloadStatus::Full) => { parent_delta.full_delta = parent_delta .full_delta .checked_add(delta) .ok_or(Error::DeltaOverflow(parent_index))?; } Ok(PayloadStatus::Empty) => { parent_delta.empty_delta = parent_delta .empty_delta .checked_add(delta) .ok_or(Error::DeltaOverflow(parent_index))?; } // Pending or V17 nodes: no payload propagation. _ => {} } } } // After applying all deltas, update the `previous_proposer_boost`. self.previous_proposer_boost = ProposerBoost { root: proposer_boost_root, score: proposer_score, }; // A second time, iterate backwards through all indices in `self.nodes`. // // We _must_ perform these functions separate from the weight-updating loop above to ensure // that we have a fully coherent set of weights before updating parent // best-child/descendant. for node_index in (0..self.nodes.len()).rev() { let node = self .nodes .get_mut(node_index) .ok_or(Error::InvalidNodeIndex(node_index))?; // If the node has a parent, try to update its best-child and best-descendant. if let Some(parent_index) = node.parent() { self.maybe_update_best_child_and_descendant::( parent_index, node_index, current_slot, best_justified_checkpoint, best_finalized_checkpoint, )?; } } Ok(()) } /// Register a block with the fork choice. /// /// It is only sane to supply a `None` parent for the genesis block. pub fn on_block( &mut self, block: Block, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, spec: &ChainSpec, ) -> Result<(), Error> { // If the block is already known, simply ignore it. if self.indices.contains_key(&block.root) { return Ok(()); } let node_index = self.nodes.len(); let parent_index = block .parent_root .and_then(|parent| self.indices.get(&parent).copied()); let node = if !spec.fork_name_at_slot::(current_slot).gloas_enabled() { ProtoNode::V17(ProtoNodeV17 { slot: block.slot, root: block.root, target_root: block.target_root, current_epoch_shuffling_id: block.current_epoch_shuffling_id, next_epoch_shuffling_id: block.next_epoch_shuffling_id, state_root: block.state_root, parent: parent_index, justified_checkpoint: block.justified_checkpoint, finalized_checkpoint: block.finalized_checkpoint, weight: 0, best_child: None, best_descendant: None, execution_status: block.execution_status, unrealized_justified_checkpoint: block.unrealized_justified_checkpoint, unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint, }) } else { let execution_payload_block_hash = block .execution_payload_block_hash .ok_or(Error::BrokenBlock { block_root: block.root, })?; let execution_payload_parent_hash = block .execution_payload_parent_hash .ok_or(Error::BrokenBlock { block_root: block.root, })?; let parent_payload_status: PayloadStatus = if let Some(parent_node) = parent_index.and_then(|idx| self.nodes.get(idx)) { // Get the parent's execution block hash, handling both V17 and V29 nodes. // V17 parents occur during the Gloas fork transition. // TODO(gloas): the spec's `get_parent_payload_status` assumes all blocks are // post-Gloas with bids. Revisit once the spec clarifies fork-transition behavior. let parent_el_block_hash = match parent_node { ProtoNode::V29(v29) => Some(v29.execution_payload_block_hash), ProtoNode::V17(v17) => v17.execution_status.block_hash(), }; // Per spec's `is_parent_node_full`: if the child's EL parent hash // matches the parent's EL block hash, the child extends the parent's // payload chain, meaning the parent was Full. if parent_el_block_hash.is_some_and(|hash| execution_payload_parent_hash == hash) { PayloadStatus::Full } else { PayloadStatus::Empty } } else { // Parent is missing (genesis or pruned due to finalization). Default to Full // since this path should only be hit at Gloas genesis, and extending the payload // chain is the safe default. PayloadStatus::Full }; ProtoNode::V29(ProtoNodeV29 { slot: block.slot, root: block.root, target_root: block.target_root, current_epoch_shuffling_id: block.current_epoch_shuffling_id, next_epoch_shuffling_id: block.next_epoch_shuffling_id, state_root: block.state_root, parent: parent_index, justified_checkpoint: block.justified_checkpoint, finalized_checkpoint: block.finalized_checkpoint, weight: 0, best_child: None, best_descendant: None, unrealized_justified_checkpoint: block.unrealized_justified_checkpoint, unrealized_finalized_checkpoint: block.unrealized_finalized_checkpoint, parent_payload_status, empty_payload_weight: 0, full_payload_weight: 0, execution_payload_block_hash, payload_timeliness_votes: empty_ptc_bitfield(E::ptc_size()), payload_data_availability_votes: empty_ptc_bitfield(E::ptc_size()), }) }; // If the parent has an invalid execution status, return an error before adding the // block to `self`. This applies only when the parent is a V17 node with execution tracking. if let Some(parent_index) = node.parent() { let parent = self .nodes .get(parent_index) .ok_or(Error::InvalidNodeIndex(parent_index))?; // Execution status tracking only exists on V17 (pre-Gloas) nodes. if let Ok(v17) = parent.as_v17() && v17.execution_status.is_invalid() { return Err(Error::ParentExecutionStatusIsInvalid { block_root: block.root, parent_root: parent.root(), }); } } self.indices.insert(node.root(), node_index); self.nodes.push(node.clone()); if let Some(parent_index) = node.parent() { self.maybe_update_best_child_and_descendant::( parent_index, node_index, current_slot, best_justified_checkpoint, best_finalized_checkpoint, )?; if matches!(block.execution_status, ExecutionStatus::Valid(_)) { self.propagate_execution_payload_validation_by_index(parent_index)?; } } Ok(()) } /// Process an excution payload for a Gloas block. /// /// this function assumes the pub fn on_valid_execution_payload(&mut self, block_root: Hash256) -> Result<(), Error> { let index = *self .indices .get(&block_root) .ok_or(Error::NodeUnknown(block_root))?; let node = self .nodes .get_mut(index) .ok_or(Error::InvalidNodeIndex(index))?; let v29 = node .as_v29_mut() .map_err(|_| Error::InvalidNodeVariant { block_root })?; // A valid execution payload means the payload is timely and data is available. // Set all bits to ensure the threshold is met regardless of PTC size. v29.payload_timeliness_votes.fill(0xFF); v29.payload_data_availability_votes.fill(0xFF); Ok(()) } /// Updates the `block_root` and all ancestors to have validated execution payloads. /// /// Returns an error if: /// /// - The `block-root` is unknown. /// - Any of the to-be-validated payloads are already invalid. pub fn propagate_execution_payload_validation( &mut self, block_root: Hash256, ) -> Result<(), Error> { let index = *self .indices .get(&block_root) .ok_or(Error::NodeUnknown(block_root))?; self.propagate_execution_payload_validation_by_index(index) } /// Updates the `verified_node_index` and all ancestors to have validated execution payloads. /// /// Returns an error if: /// /// - The `verified_node_index` is unknown. /// - Any of the to-be-validated payloads are already invalid. fn propagate_execution_payload_validation_by_index( &mut self, verified_node_index: usize, ) -> Result<(), Error> { let mut index = verified_node_index; loop { let node = self .nodes .get_mut(index) .ok_or(Error::InvalidNodeIndex(index))?; let parent_index = match node { ProtoNode::V17(node) => match node.execution_status { // We have reached a node that we already know is valid. No need to iterate further // since we assume an ancestors have already been set to valid. ExecutionStatus::Valid(_) => return Ok(()), // We have reached an irrelevant node, this node is prior to a terminal execution // block. There's no need to iterate further, it's impossible for this block to have // any relevant ancestors. ExecutionStatus::Irrelevant(_) => return Ok(()), // The block has an unknown status, set it to valid since any ancestor of a valid // payload can be considered valid. ExecutionStatus::Optimistic(payload_block_hash) => { node.execution_status = ExecutionStatus::Valid(payload_block_hash); if let Some(parent_index) = node.parent { parent_index } else { // We have reached the root block, iteration complete. return Ok(()); } } // An ancestor of the valid payload was invalid. This is a serious error which // indicates a consensus failure in the execution node. This is unrecoverable. ExecutionStatus::Invalid(ancestor_payload_block_hash) => { return Err(Error::InvalidAncestorOfValidPayload { ancestor_block_root: node.root, ancestor_payload_block_hash, }); } }, // Gloas nodes don't carry `ExecutionStatus`. ProtoNode::V29(node) => { if let Some(parent_index) = node.parent { parent_index } else { return Ok(()); } } }; index = parent_index; } } /// Invalidate zero or more blocks, as specified by the `InvalidationOperation`. /// /// See the documentation of `InvalidationOperation` for usage. pub fn propagate_execution_payload_invalidation( &mut self, op: &InvalidationOperation, best_finalized_checkpoint: Checkpoint, ) -> Result<(), Error> { let mut invalidated_indices: HashSet = <_>::default(); let head_block_root = op.block_root(); /* * Step 1: * * Find the `head_block_root` and maybe iterate backwards and invalidate ancestors. Record * all invalidated block indices in `invalidated_indices`. */ let mut index = *self .indices .get(&head_block_root) .ok_or(Error::NodeUnknown(head_block_root))?; // Try to map the ancestor payload *hash* to an ancestor beacon block *root*. let latest_valid_ancestor_root = op .latest_valid_ancestor() .and_then(|hash| self.execution_block_hash_to_beacon_block_root(&hash)); // Set to `true` if both conditions are satisfied: // // 1. The `head_block_root` is a descendant of `latest_valid_ancestor_hash` // 2. The `latest_valid_ancestor_hash` is equal to or a descendant of the finalized block. 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::( ancestor_root, best_finalized_checkpoint, ) }); // Collect all *ancestors* which were declared invalid since they reside between the // `head_block_root` and the `latest_valid_ancestor_root`. loop { let node = self .nodes .get_mut(index) .ok_or(Error::InvalidNodeIndex(index))?; let node_execution_status = node.execution_status(); match node_execution_status { Ok(ExecutionStatus::Valid(hash)) | Ok(ExecutionStatus::Invalid(hash)) | Ok(ExecutionStatus::Optimistic(hash)) => { // If we're no longer processing the `head_block_root` and the last valid // ancestor is unknown, exit this loop and proceed to invalidate and // descendants of `head_block_root`/`latest_valid_ancestor_root`. // // In effect, this means that if an unknown hash (junk or pre-finalization) is // supplied, don't validate any ancestors. The alternative is to invalidate // *all* ancestors, which would likely involve shutting down the client due to // an invalid justified checkpoint. if !latest_valid_ancestor_is_descendant && node.root() != head_block_root { break; } else if op.latest_valid_ancestor() == Some(hash) { // If the `best_child` or `best_descendant` of the latest valid hash was // invalidated, set those fields to `None`. // // In theory, an invalid `best_child` necessarily infers an invalid // `best_descendant`. However, we check each variable independently to // defend against errors which might result in an invalid block being set as // head. if node .best_child() .is_some_and(|i| invalidated_indices.contains(&i)) { *node.best_child_mut() = None } if node .best_descendant() .is_some_and(|i| invalidated_indices.contains(&i)) { *node.best_descendant_mut() = None } break; } } Ok(ExecutionStatus::Irrelevant(_)) => break, Err(_) => break, } // Only invalidate the head block if either: // // - The head block was specifically indicated to be invalidated. // - The latest valid hash is a known ancestor. if node.root() != head_block_root || op.invalidate_block_root() || latest_valid_ancestor_is_descendant { match node.execution_status() { // It's illegal for an execution client to declare that some previously-valid block // is now invalid. This is a consensus failure on their behalf. Ok(ExecutionStatus::Valid(hash)) => { return Err(Error::ValidExecutionStatusBecameInvalid { block_root: node.root(), payload_block_hash: hash, }); } Ok(ExecutionStatus::Optimistic(hash)) => { invalidated_indices.insert(index); if let ProtoNode::V17(node) = node { node.execution_status = ExecutionStatus::Invalid(hash); } // It's impossible for an invalid block to lead to a "best" block, so set these // fields to `None`. // // Failing to set these values will result in `Self::node_leads_to_viable_head` // returning `false` for *valid* ancestors of invalid blocks. *node.best_child_mut() = None; *node.best_descendant_mut() = None; } // The block is already invalid, but keep going backwards to ensure all ancestors // are updated. Ok(ExecutionStatus::Invalid(_)) => (), // This block is pre-merge, therefore it has no execution status. Nor do its // ancestors. Ok(ExecutionStatus::Irrelevant(_)) => break, Err(_) => (), } } if let Some(parent_index) = node.parent() { index = parent_index } else { // The root of the block tree has been reached (aka the finalized block), without // matching `latest_valid_ancestor_hash`. It's not possible or useful to go any // further back: the finalized checkpoint is invalid so all is lost! break; } } /* * Step 2: * * Start at either the `latest_valid_ancestor` or the `head_block_root` and iterate * *forwards* to invalidate all descendants of all blocks in `invalidated_indices`. */ let starting_block_root = latest_valid_ancestor_root .filter(|_| latest_valid_ancestor_is_descendant) .unwrap_or(head_block_root); let latest_valid_ancestor_index = *self .indices .get(&starting_block_root) .ok_or(Error::NodeUnknown(starting_block_root))?; let first_potential_descendant = latest_valid_ancestor_index + 1; // Collect all *descendants* which have been declared invalid since they're the descendant of a block // with an invalid execution payload. for index in first_potential_descendant..self.nodes.len() { let node = self .nodes .get_mut(index) .ok_or(Error::InvalidNodeIndex(index))?; if let Some(parent_index) = node.parent() && invalidated_indices.contains(&parent_index) { match node.execution_status() { Ok(ExecutionStatus::Valid(hash)) => { return Err(Error::ValidExecutionStatusBecameInvalid { block_root: node.root(), payload_block_hash: hash, }); } Ok(ExecutionStatus::Optimistic(hash)) | Ok(ExecutionStatus::Invalid(hash)) => { if let ProtoNode::V17(node) = node { node.execution_status = ExecutionStatus::Invalid(hash) } } Ok(ExecutionStatus::Irrelevant(_)) => { return Err(Error::IrrelevantDescendant { block_root: node.root(), }); } Err(_) => (), } invalidated_indices.insert(index); } } Ok(()) } /// Follows the best-descendant links to find the best-block (i.e., head-block). /// /// ## Notes /// /// The result of this function is not guaranteed to be accurate if `Self::on_new_block` has /// been called without a subsequent `Self::apply_score_changes` call. This is because /// `on_new_block` does not attempt to walk backwards through the tree and update the /// best-child/best-descendant links. pub fn find_head( &self, justified_root: &Hash256, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, ) -> Result { let justified_index = self .indices .get(justified_root) .copied() .ok_or(Error::JustifiedNodeUnknown(*justified_root))?; let justified_node = self .nodes .get(justified_index) .ok_or(Error::InvalidJustifiedIndex(justified_index))?; // Since there are no valid descendants of a justified block with an invalid execution // payload, there would be no head to choose from. // // Fork choice is effectively broken until a new justified root is set. It might not be // practically possible to set a new justified root if we are unable to find a new head. // // This scenario is *unsupported*. It represents a serious consensus failure. // Execution status tracking only exists on V17 (pre-Gloas) nodes. if let Ok(v17) = justified_node.as_v17() && v17.execution_status.is_invalid() { return Err(Error::InvalidJustifiedCheckpointExecutionStatus { justified_root: *justified_root, }); } let best_descendant_index = justified_node.best_descendant().unwrap_or(justified_index); let best_node = self .nodes .get(best_descendant_index) .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::( 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: 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(), }))); } Ok(best_node.root()) } /// Update the tree with new finalization information. The tree is only actually pruned if both /// of the two following criteria are met: /// /// - The supplied finalized epoch and root are different to the current values. /// - The number of nodes in `self` is at least `self.prune_threshold`. /// /// # Errors /// /// Returns errors if: /// /// - The finalized epoch is less than the current one. /// - The finalized epoch is equal to the current one, but the finalized root is different. /// - There is some internal error relating to invalid indices inside `self`. pub fn maybe_prune(&mut self, finalized_root: Hash256) -> Result<(), Error> { let finalized_index = *self .indices .get(&finalized_root) .ok_or(Error::FinalizedNodeUnknown(finalized_root))?; if finalized_index < self.prune_threshold { // Pruning at small numbers incurs more cost than benefit. return Ok(()); } // Remove the `self.indices` key/values for all the to-be-deleted nodes. for node_index in 0..finalized_index { let root = &self .nodes .get(node_index) .ok_or(Error::InvalidNodeIndex(node_index))? .root(); self.indices.remove(root); } // Drop all the nodes prior to finalization. self.nodes = self.nodes.split_off(finalized_index); // Adjust the indices map. for (_root, index) in self.indices.iter_mut() { *index = index .checked_sub(finalized_index) .ok_or(Error::IndexOverflow("indices"))?; } // Iterate through all the existing nodes and adjust their indices to match the new layout // of `self.nodes`. for node in self.nodes.iter_mut() { if let Some(parent) = node.parent() { // If `node.parent` is less than `finalized_index`, set it to `None`. *node.parent_mut() = parent.checked_sub(finalized_index); } if let Some(best_child) = node.best_child() { *node.best_child_mut() = Some( best_child .checked_sub(finalized_index) .ok_or(Error::IndexOverflow("best_child"))?, ); } if let Some(best_descendant) = node.best_descendant() { *node.best_descendant_mut() = Some( best_descendant .checked_sub(finalized_index) .ok_or(Error::IndexOverflow("best_descendant"))?, ); } } Ok(()) } /// Observe the parent at `parent_index` with respect to the child at `child_index` and /// potentially modify the `parent.best_child` and `parent.best_descendant` values. /// /// ## Detail /// /// There are four outcomes: /// /// - The child is already the best child but it's now invalid due to a FFG change and should be removed. /// - The child is already the best child and the parent is updated with the new /// best-descendant. /// - The child is not the best child but becomes the best child. /// - The child is not the best child and does not become the best child. fn maybe_update_best_child_and_descendant( &mut self, parent_index: usize, child_index: usize, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, ) -> Result<(), Error> { let child = self .nodes .get(child_index) .ok_or(Error::InvalidNodeIndex(child_index))?; let parent = self .nodes .get(parent_index) .ok_or(Error::InvalidNodeIndex(parent_index))?; let child_leads_to_viable_head = self.node_leads_to_viable_head::( 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. // // I use the aliases to assist readability. let change_to_none = (None, None); let change_to_child = ( Some(child_index), child.best_descendant().or(Some(child_index)), ); let no_change = (parent.best_child(), parent.best_descendant()); let (new_best_child, new_best_descendant) = if let Some(best_child_index) = parent.best_child() { if best_child_index == child_index && !child_leads_to_viable_head { // If the child is already the best-child of the parent but it's not viable for // the head, remove it. change_to_none } else if best_child_index == child_index { // If the child is the best-child already, set it again to ensure that the // best-descendant of the parent is updated. change_to_child } else { let best_child = self .nodes .get(best_child_index) .ok_or(Error::InvalidBestDescendant(best_child_index))?; let best_child_leads_to_viable_head = self.node_leads_to_viable_head::( 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. change_to_child } else if !child_leads_to_viable_head && best_child_leads_to_viable_head { // The best child leads to a viable head, but the child doesn't. no_change } else if child.weight() > best_child.weight() { // Weight is the primary ordering criterion. change_to_child } else if child.weight() < best_child.weight() { no_change } else { // Equal weights: for V29 parents, prefer the child whose // parent_payload_status matches the parent's payload preference // (full vs empty). This corresponds to the spec's // `get_payload_status_tiebreaker` ordering in `get_head`. let child_matches = child_matches_parent_payload_preference( parent, child, current_slot, E::ptc_size(), ); let best_child_matches = child_matches_parent_payload_preference( parent, best_child, current_slot, E::ptc_size(), ); if child_matches && !best_child_matches { // Child extends the preferred payload chain, best_child doesn't. change_to_child } else if !child_matches && best_child_matches { // Best child extends the preferred payload chain, child doesn't. no_change } else if *child.root() >= *best_child.root() { // Final tie-breaker: both match or both don't, break by root. change_to_child } else { no_change } } } } else if child_leads_to_viable_head { // There is no current best-child and the child is viable. change_to_child } else { // There is no current best-child but the child is not viable. no_change }; let parent = self .nodes .get_mut(parent_index) .ok_or(Error::InvalidNodeIndex(parent_index))?; *parent.best_child_mut() = new_best_child; *parent.best_descendant_mut() = new_best_descendant; Ok(()) } /// Indicates if the node itself is viable for the head, or if its best descendant is viable /// for the head. fn node_leads_to_viable_head( &self, node: &ProtoNode, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, ) -> Result { let best_descendant_is_viable_for_head = if let Some(best_descendant_index) = node.best_descendant() { let best_descendant = self .nodes .get(best_descendant_index) .ok_or(Error::InvalidBestDescendant(best_descendant_index))?; self.node_is_viable_for_head::( 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::( node, current_slot, best_justified_checkpoint, best_finalized_checkpoint, )) } /// This is the equivalent to the `filter_block_tree` function in the eth2 spec: /// /// https://github.com/ethereum/eth2.0-specs/blob/v0.10.0/specs/phase0/fork-choice.md#filter_block_tree /// /// Any node that has a different finalized or justified epoch should not be viable for the /// head. fn node_is_viable_for_head( &self, node: &ProtoNode, current_slot: Slot, best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, ) -> bool { if let Ok(proto_node) = node.as_v17() && proto_node.execution_status.is_invalid() { return false; } let genesis_epoch = Epoch::new(0); let current_epoch = current_slot.epoch(E::slots_per_epoch()); let node_epoch = node.slot().epoch(E::slots_per_epoch()); let node_justified_checkpoint = node.justified_checkpoint(); let voting_source = if current_epoch > node_epoch { // The block is from a prior epoch, the voting source will be pulled-up. node.unrealized_justified_checkpoint() // Sometimes we don't track the unrealized justification. In // that case, just use the fully-realized justified checkpoint. .unwrap_or(*node_justified_checkpoint) } else { // The block is not from a prior epoch, therefore the voting source // is not pulled up. *node_justified_checkpoint }; 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 = best_finalized_checkpoint.epoch == genesis_epoch || self .is_finalized_checkpoint_or_descendant::(node.root(), best_finalized_checkpoint); correct_justified && correct_finalized } /// Return a reverse iterator over the nodes which comprise the chain ending at `block_root`. pub fn iter_nodes<'a>(&'a self, block_root: &Hash256) -> Iter<'a> { let next_node_index = self.indices.get(block_root).copied(); Iter { next_node_index, proto_array: self, } } /// Return a reverse iterator over the block roots of the chain ending at `block_root`. /// /// Note that unlike many other iterators, this one WILL NOT yield anything at skipped slots. pub fn iter_block_roots<'a>( &'a self, block_root: &Hash256, ) -> impl Iterator + 'a { self.iter_nodes(block_root) .map(|node| (node.root(), node.slot())) } /// Returns `true` if the `descendant_root` has an ancestor with `ancestor_root`. Always /// returns `false` if either input root is unknown. /// /// ## Notes /// /// Still returns `true` if `ancestor_root` is known and `ancestor_root == descendant_root`. /// /// ## Warning /// /// Do not use this function to check if a block is a descendant of the /// finalized checkpoint. Use `Self::is_finalized_checkpoint_or_descendant` /// instead. pub fn is_descendant(&self, ancestor_root: Hash256, descendant_root: Hash256) -> bool { self.indices .get(&ancestor_root) .and_then(|ancestor_index| self.nodes.get(*ancestor_index)) .and_then(|ancestor| { self.iter_block_roots(&descendant_root) .take_while(|(_root, slot)| *slot >= ancestor.slot()) .find(|(_root, slot)| *slot == ancestor.slot()) .map(|(root, _slot)| root == ancestor_root) }) .unwrap_or(false) } /// Returns `true` if `root` is equal to or a descendant of /// `self.finalized_checkpoint`. /// /// Notably, this function is checking ancestory of the finalized /// *checkpoint* not the finalized *block*. pub fn is_finalized_checkpoint_or_descendant( &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()); let Some(mut node) = self .indices .get(&root) .and_then(|index| self.nodes.get(*index)) else { // An unknown root is not a finalized descendant. This line can only // be reached if the user supplies a root that is not known to fork // choice. return false; }; // The finalized and justified checkpoints represent a list of known // ancestors of `node` that are likely to coincide with the store's // finalized checkpoint. // // Run this check once, outside of the loop rather than inside the loop. // 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 == best_finalized_checkpoint { return true; } } for checkpoint in &[ node.unrealized_finalized_checkpoint(), node.unrealized_justified_checkpoint(), ] { if checkpoint.is_some_and(|cp| cp == best_finalized_checkpoint) { return true; } } loop { // If `node` is less than or equal to the finalized slot then `node` // must be the finalized block. if node.slot() <= finalized_slot { return node.root() == finalized_root; } // Since `node` is from a higher slot that the finalized checkpoint, // replace `node` with the parent of `node`. if let Some(parent) = node.parent().and_then(|index| self.nodes.get(index)) { node = parent } else { // If `node` is not the finalized block and its parent does not // exist in fork choice, then the parent must have been pruned. // Proto-array only prunes blocks prior to the finalized block, // so this means the parent conflicts with finality. return false; }; } } /// Returns the first *beacon block root* which contains an execution payload with the given /// `block_hash`, if any. pub fn execution_block_hash_to_beacon_block_root( &self, block_hash: &ExecutionBlockHash, ) -> Option { self.nodes .iter() .rev() .find(|node| { node.execution_status() .ok() .and_then(|execution_status| execution_status.block_hash()) .is_some_and(|node_block_hash| node_block_hash == *block_hash) }) .map(|node| node.root()) } /// Returns all nodes that have zero children and are descended from the finalized checkpoint. /// /// 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( &self, best_finalized_checkpoint: Checkpoint, ) -> Vec<&ProtoNode> { self.nodes .iter() .filter(|node| { node.best_child().is_none() && self.is_finalized_checkpoint_or_descendant::( node.root(), best_finalized_checkpoint, ) }) .collect() } } /// For V29 parents, returns `true` if the child's `parent_payload_status` matches the parent's /// preferred payload status. When full and empty weights are unequal, the higher weight wins. /// When equal, the tiebreaker uses the parent's `payload_tiebreak`: prefer Full if the block /// was timely and data is available; otherwise prefer Empty. /// For V17 parents (or mixed), always returns `true` (no payload preference). /// /// TODO(gloas): the spec's `should_extend_payload` has additional conditions beyond the /// tiebreaker: it also checks proposer_boost_root (empty, different parent, or extends full). /// See: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md#new-should_extend_payload /// /// TODO(gloas): the spec's `should_extend_payload` has additional conditions beyond the /// tiebreaker: it also checks proposer_boost_root (empty, different parent, or extends full). /// See: https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md#new-should_extend_payload fn child_matches_parent_payload_preference( parent: &ProtoNode, child: &ProtoNode, current_slot: Slot, ptc_size: usize, ) -> bool { let (Ok(parent_v29), Ok(child_v29)) = (parent.as_v29(), child.as_v29()) else { return true; }; // Per spec `get_weight`: FULL/EMPTY virtual nodes at `current_slot - 1` have weight 0. // The PTC is still voting, so payload preference is determined solely by the tiebreaker. let use_tiebreaker_only = parent.slot() + 1 == current_slot; let prefers_full = if !use_tiebreaker_only && parent_v29.full_payload_weight > parent_v29.empty_payload_weight { true } else if !use_tiebreaker_only && parent_v29.empty_payload_weight > parent_v29.full_payload_weight { false } else { // Equal weights (or current-slot parent): tiebreaker per spec. is_payload_timely(&parent_v29.payload_timeliness_votes, ptc_size) && is_payload_data_available(&parent_v29.payload_data_availability_votes, ptc_size) }; if prefers_full { child_v29.parent_payload_status == PayloadStatus::Full } else { child_v29.parent_payload_status == PayloadStatus::Empty } } /// Count the number of set bits in a byte-slice bitfield. pub fn count_set_bits(bitfield: &[u8]) -> usize { bitfield.iter().map(|b| b.count_ones() as usize).sum() } /// Create a zero-initialized bitfield for the given PTC size. pub fn empty_ptc_bitfield(ptc_size: usize) -> Vec { vec![0u8; ptc_size.div_ceil(8)] } /// Derive `is_payload_timely` from the timeliness vote bitfield. pub fn is_payload_timely(timeliness_votes: &[u8], ptc_size: usize) -> bool { count_set_bits(timeliness_votes) > ptc_size / 2 } /// Derive `is_payload_data_available` from the data availability vote bitfield. pub fn is_payload_data_available(availability_votes: &[u8], ptc_size: usize) -> bool { count_set_bits(availability_votes) > ptc_size / 2 } /// 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_committee_fraction( justified_balances: &JustifiedBalances, proposer_score_boost: u64, ) -> Option { let committee_weight = justified_balances .total_effective_balance .checked_div(E::slots_per_epoch())?; committee_weight .checked_mul(proposer_score_boost)? .checked_div(100) } /// Apply a signed delta to an unsigned weight, returning an error on overflow. fn apply_delta(weight: u64, delta: i64, index: usize) -> Result { if delta < 0 { weight .checked_sub(delta.unsigned_abs()) .ok_or(Error::DeltaOverflow(index)) } else { weight .checked_add(delta as u64) .ok_or(Error::DeltaOverflow(index)) } } /// Reverse iterator over one path through a `ProtoArray`. pub struct Iter<'a> { next_node_index: Option, proto_array: &'a ProtoArray, } impl<'a> Iterator for Iter<'a> { type Item = &'a ProtoNode; fn next(&mut self) -> Option { let next_node_index = self.next_node_index?; let node = self.proto_array.nodes.get(next_node_index)?; self.next_node_index = node.parent(); Some(node) } }