From 491b69f364bcc0d75f0183a6bd288cb8684d86f0 Mon Sep 17 00:00:00 2001 From: hopinheimer Date: Thu, 19 Feb 2026 23:07:31 -0500 Subject: [PATCH] proto node versioning --- consensus/fork_choice/src/fork_choice.rs | 30 +++ consensus/proto_array/src/error.rs | 7 + consensus/proto_array/src/lib.rs | 2 +- consensus/proto_array/src/proto_array.rs | 183 ++++++++++++------ .../src/proto_array_fork_choice.rs | 13 ++ 5 files changed, 176 insertions(+), 59 deletions(-) diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 9744b9fa08..5edd9b139d 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -138,6 +138,10 @@ pub enum InvalidBlock { finalized_root: Hash256, block_ancestor: Option, }, + MissingExecutionPayloadBid{ + block_slot: Slot, + block_root: Hash256, + } } #[derive(Debug)] @@ -241,6 +245,7 @@ pub struct QueuedAttestation { attesting_indices: Vec, block_root: Hash256, target_epoch: Epoch, + payload_present: bool, } impl<'a, E: EthSpec> From> for QueuedAttestation { @@ -250,6 +255,7 @@ impl<'a, E: EthSpec> From> for QueuedAttestation { attesting_indices: a.attesting_indices_to_vec(), block_root: a.data().beacon_block_root, target_epoch: a.data().target.epoch, + payload_present: a.data().index == 1, } } } @@ -882,6 +888,25 @@ where ExecutionStatus::irrelevant() }; + let (execution_payload_parent_hash, execution_payload_block_hash) = + if let Ok(signed_bid) = block.body().signed_execution_payload_bid() { + ( + Some(signed_bid.message.parent_block_hash), + Some(signed_bid.message.block_hash), + ) + } else { + if spec.fork_name_at_slot::(block.slot()).gloas_enabled() { + return Err(Error::InvalidBlock( + InvalidBlock::MissingExecutionPayloadBid{ + block_slot: block.slot(), + block_root, + } + + )) + } + (None, None) + }; + // This does not apply a vote to the block, it just makes fork choice aware of the block so // it can still be identified as the head even if it doesn't have any votes. self.proto_array.process_block::( @@ -908,10 +933,14 @@ where execution_status, unrealized_justified_checkpoint: Some(unrealized_justified_checkpoint), unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint), + execution_payload_parent_hash, + execution_payload_block_hash, + }, current_slot, self.justified_checkpoint(), self.finalized_checkpoint(), + spec, )?; Ok(()) @@ -1103,6 +1132,7 @@ where if attestation.data().slot < self.fc_store.get_current_slot() { for validator_index in attestation.attesting_indices_iter() { + let payload_present = attestation.data().index == 1; self.proto_array.process_attestation( *validator_index as usize, attestation.data().beacon_block_root, diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index 35cb4007b7..c3e60277a3 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -54,6 +54,13 @@ pub enum Error { }, InvalidEpochOffset(u64), Arith(ArithError), + GloasNotImplemented, + InvalidNodeVariant{ + block_root: Hash256, + }, + BrokenBlock{ + block_root: Hash256, + }, } impl From for Error { diff --git a/consensus/proto_array/src/lib.rs b/consensus/proto_array/src/lib.rs index 964e836d91..222f927478 100644 --- a/consensus/proto_array/src/lib.rs +++ b/consensus/proto_array/src/lib.rs @@ -9,7 +9,7 @@ pub use crate::justified_balances::JustifiedBalances; pub use crate::proto_array::{InvalidationOperation, calculate_committee_fraction}; pub use crate::proto_array_fork_choice::{ Block, DisallowedReOrgOffsets, DoNotReOrg, ExecutionStatus, ProposerHeadError, - ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, + ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold, PayloadStatus, }; pub use error::Error; diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 5bfcdae463..d7b1ec6313 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -1,5 +1,5 @@ use crate::error::InvalidBestNodeInfo; -use crate::{Block, ExecutionStatus, JustifiedBalances, error::Error}; +use crate::{Block, ExecutionStatus, JustifiedBalances, error::Error, PayloadStatus}; use fixed_bytes::FixedBytesExtended; use serde::{Deserialize, Serialize}; use ssz::Encode; @@ -68,47 +68,68 @@ impl InvalidationOperation { } } -pub type ProtoNode = ProtoNodeV17; #[superstruct( - variants(V17), + variants(V17, V29), variant_attributes(derive(Clone, PartialEq, Debug, Encode, Decode, Serialize, Deserialize)), - no_enum )] +#[derive(PartialEq, Debug, Encode, Decode, Serialize, Deserialize, Clone)] +#[ssz(enum_behaviour = "transparent")] 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))] + #[superstruct(only(V17, V29), partial_getter(copy))] pub justified_checkpoint: Checkpoint, - #[superstruct(only(V17))] + #[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. + #[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, } #[derive(PartialEq, Debug, Encode, Decode, Serialize, Deserialize, Copy, Clone)] @@ -181,16 +202,14 @@ impl ProtoArray { // 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() { + if node.root() == Hash256::zero() { continue; } - let execution_status_is_invalid = node.execution_status.is_invalid(); - - let mut node_delta = if execution_status_is_invalid { + let mut node_delta = if let Ok(proto_node) = node.as_v17() && proto_node.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) + .checked_sub(node.weight() as i64) .ok_or(Error::InvalidExecutionDeltaOverflow(node_index))? } else { deltas @@ -202,7 +221,7 @@ impl ProtoArray { // If we find the node for which the proposer boost was previously applied, decrease // the delta by the previous score amount. if self.previous_proposer_boost.root != Hash256::zero() - && self.previous_proposer_boost.root == node.root + && 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 @@ -217,7 +236,7 @@ impl ProtoArray { // 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 && proposer_boost_root != Hash256::zero() - && proposer_boost_root == node.root + && proposer_boost_root == node.root() // Invalid nodes (or their ancestors) should not receive a proposer boost. && !execution_status_is_invalid { @@ -232,7 +251,7 @@ impl ProtoArray { // Apply the delta to the node. if execution_status_is_invalid { // Invalid nodes always have a weight of 0. - node.weight = 0 + node.weight() = 0 } else if node_delta < 0 { // Note: I am conflicted about whether to use `saturating_sub` or `checked_sub` // here. @@ -243,19 +262,19 @@ impl ProtoArray { // // However, I am not fully convinced that some valid case for `saturating_sub` does // not exist. - node.weight = node - .weight + node.weight() = node + .weight() .checked_sub(node_delta.unsigned_abs()) .ok_or(Error::DeltaOverflow(node_index))?; } else { node.weight = node - .weight + .weight() .checked_add(node_delta as u64) .ok_or(Error::DeltaOverflow(node_index))?; } // Update the parent delta (if any). - if let Some(parent_index) = node.parent { + if let Some(parent_index) = node.parent() { let parent_delta = deltas .get_mut(parent_index) .ok_or(Error::InvalidParentDelta(parent_index))?; @@ -283,7 +302,7 @@ impl ProtoArray { .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 { + if let Some(parent_index) = node.parent() { self.maybe_update_best_child_and_descendant::( parent_index, node_index, @@ -306,6 +325,7 @@ impl ProtoArray { 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) { @@ -314,45 +334,92 @@ impl ProtoArray { let node_index = self.nodes.len(); - let node = ProtoNode { - 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: block - .parent_root - .and_then(|parent| self.indices.get(&parent).copied()), - 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, + 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 parent_payload_status: PayloadStatus = + if let Some(parent_node) = + parent_index.and_then(|idx| self.nodes.get(idx)) + { + let v29 = parent_node + .as_v29() + .map_err(|_| Error::InvalidNodeVariant{block_root: block.root})?; + if execution_payload_block_hash == v29.execution_payload_block_hash + { + PayloadStatus::Empty + } else { + PayloadStatus::Full + } + } else { + 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, + }) }; - // If the parent has an invalid execution status, return an error before adding the block to - // `self`. - if let Some(parent_index) = node.parent { + // If the parent has an invalid execution status, return an error before adding the + // block to `self`. This applies 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))?; - if parent.execution_status.is_invalid() { + + if let Ok(status) = parent.execution_status() && status.is_invalid() { return Err(Error::ParentExecutionStatusIsInvalid { block_root: block.root, - parent_root: parent.root, + parent_root: parent.root(), }); } } - self.indices.insert(node.root, node_index); + self.indices.insert(node.root(), node_index); self.nodes.push(node.clone()); - if let Some(parent_index) = node.parent { + if let Some(parent_index) = node.parent() { self.maybe_update_best_child_and_descendant::( parent_index, node_index, @@ -805,12 +872,12 @@ impl ProtoArray { let change_to_none = (None, None); let change_to_child = ( Some(child_index), - child.best_descendant.or(Some(child_index)), + child.best_descendant().or(Some(child_index)), ); - let no_change = (parent.best_child, parent.best_descendant); + 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 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. @@ -838,16 +905,16 @@ impl ProtoArray { } 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 { + } else if child.weight() == best_child.weight() { // Tie-breaker of equal weights by root. - if child.root >= best_child.root { + if *child.root() >= *best_child.root() { change_to_child } else { no_change } } else { // Choose the winner by weight. - if child.weight > best_child.weight { + if child.weight() > best_child.weight() { change_to_child } else { no_change @@ -867,8 +934,8 @@ impl ProtoArray { .get_mut(parent_index) .ok_or(Error::InvalidNodeIndex(parent_index))?; - parent.best_child = new_best_child; - parent.best_descendant = new_best_descendant; + *parent.best_child_mut() = new_best_child; + *parent.best_descendant_mut() = new_best_descendant; Ok(()) } @@ -883,7 +950,7 @@ impl ProtoArray { best_finalized_checkpoint: Checkpoint, ) -> Result { let best_descendant_is_viable_for_head = - if let Some(best_descendant_index) = node.best_descendant { + if let Some(best_descendant_index) = node.best_descendant() { let best_descendant = self .nodes .get(best_descendant_index) @@ -921,21 +988,21 @@ impl ProtoArray { best_justified_checkpoint: Checkpoint, best_finalized_checkpoint: Checkpoint, ) -> bool { - if node.execution_status.is_invalid() { + 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 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 + 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) + .unwrap_or(*node_justified_checkpoint) } else { // The block is not from a prior epoch, therefore the voting source // is not pulled up. diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 3edf1e0644..a0cd50db8b 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -159,6 +159,10 @@ pub struct Block { pub execution_status: ExecutionStatus, pub unrealized_justified_checkpoint: Option, pub unrealized_finalized_checkpoint: Option, + + /// post-Gloas fields + pub execution_payload_parent_hash: Option, + pub execution_payload_block_hash: Option, } impl Block { @@ -422,6 +426,9 @@ impl ProtoArrayForkChoice { current_epoch_shuffling_id: AttestationShufflingId, next_epoch_shuffling_id: AttestationShufflingId, execution_status: ExecutionStatus, + execution_payload_parent_hash: Option, + execution_payload_block_hash: Option, + ) -> Result { let mut proto_array = ProtoArray { prune_threshold: DEFAULT_PRUNE_THRESHOLD, @@ -445,6 +452,9 @@ impl ProtoArrayForkChoice { execution_status, unrealized_justified_checkpoint: Some(justified_checkpoint), unrealized_finalized_checkpoint: Some(finalized_checkpoint), + execution_payload_parent_hash, + execution_payload_block_hash, + }; proto_array @@ -453,6 +463,7 @@ impl ProtoArrayForkChoice { current_slot, justified_checkpoint, finalized_checkpoint, + spec, ) .map_err(|e| format!("Failed to add finalized block to proto_array: {:?}", e))?; @@ -506,6 +517,7 @@ impl ProtoArrayForkChoice { current_slot: Slot, justified_checkpoint: Checkpoint, finalized_checkpoint: Checkpoint, + spec: &ChainSpec, ) -> Result<(), String> { if block.parent_root.is_none() { return Err("Missing parent root".to_string()); @@ -517,6 +529,7 @@ impl ProtoArrayForkChoice { current_slot, justified_checkpoint, finalized_checkpoint, + spec, ) .map_err(|e| format!("process_block_error: {:?}", e)) }