proto node versioning

This commit is contained in:
hopinheimer
2026-02-19 23:07:31 -05:00
parent 54b3576145
commit 491b69f364
5 changed files with 176 additions and 59 deletions

View File

@@ -138,6 +138,10 @@ pub enum InvalidBlock {
finalized_root: Hash256,
block_ancestor: Option<Hash256>,
},
MissingExecutionPayloadBid{
block_slot: Slot,
block_root: Hash256,
}
}
#[derive(Debug)]
@@ -241,6 +245,7 @@ pub struct QueuedAttestation {
attesting_indices: Vec<u64>,
block_root: Hash256,
target_epoch: Epoch,
payload_present: bool,
}
impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
@@ -250,6 +255,7 @@ impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> 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::<E>(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::<E>(
@@ -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,

View File

@@ -54,6 +54,13 @@ pub enum Error {
},
InvalidEpochOffset(u64),
Arith(ArithError),
GloasNotImplemented,
InvalidNodeVariant{
block_root: Hash256,
},
BrokenBlock{
block_root: Hash256,
},
}
impl From<ArithError> for Error {

View File

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

View File

@@ -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<usize>,
#[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<usize>,
#[superstruct(getter(copy))]
#[ssz(with = "four_byte_option_usize")]
pub best_descendant: Option<usize>,
/// 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<Checkpoint>,
#[superstruct(getter(copy))]
#[ssz(with = "four_byte_option_checkpoint")]
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
/// 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::<E>(
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::<E>(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::<E>(
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<bool, Error> {
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.

View File

@@ -159,6 +159,10 @@ pub struct Block {
pub execution_status: ExecutionStatus,
pub unrealized_justified_checkpoint: Option<Checkpoint>,
pub unrealized_finalized_checkpoint: Option<Checkpoint>,
/// post-Gloas fields
pub execution_payload_parent_hash: Option<ExecutionBlockHash>,
pub execution_payload_block_hash: Option<ExecutionBlockHash>,
}
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<ExecutionBlockHash>,
execution_payload_block_hash: Option<ExecutionBlockHash>,
) -> Result<Self, String> {
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))
}