Resolve merge confclits from unstable

This commit is contained in:
Eitan Seri- Levi
2026-04-04 22:36:06 -07:00
28 changed files with 483 additions and 313 deletions

View File

@@ -26,6 +26,7 @@ use types::{
#[derive(Debug)]
pub enum Error<T> {
InvalidAttestation(InvalidAttestation),
InvalidPayloadAttestation(InvalidPayloadAttestation),
InvalidAttesterSlashing(AttesterSlashingValidationError),
InvalidBlock(InvalidBlock),
ProtoArrayStringError(String),
@@ -85,6 +86,12 @@ impl<T> From<InvalidAttestation> for Error<T> {
}
}
impl<T> From<InvalidPayloadAttestation> for Error<T> {
fn from(e: InvalidPayloadAttestation) -> Self {
Error::InvalidPayloadAttestation(e)
}
}
impl<T> From<AttesterSlashingValidationError> for Error<T> {
fn from(e: AttesterSlashingValidationError) -> Self {
Error::InvalidAttesterSlashing(e)
@@ -177,14 +184,26 @@ pub enum InvalidAttestation {
/// Post-Gloas: attestation with index == 1 (payload_present) requires the block's
/// payload to have been received (`root in store.payload_states`).
PayloadNotReceived { beacon_block_root: Hash256 },
/// A payload attestation votes payload_present for a block in the current slot, which is
/// invalid because the payload cannot be known yet.
PayloadPresentDuringSameSlot { slot: Slot },
}
#[derive(Debug, Clone, PartialEq)]
pub enum InvalidPayloadAttestation {
/// The payload attestation's attesting indices were empty.
EmptyAggregationBitfield,
/// The `payload_attestation.data.beacon_block_root` block is unknown.
UnknownHeadBlock { beacon_block_root: Hash256 },
/// The payload attestation is attesting to a block that is later than itself.
AttestsToFutureBlock { block: Slot, attestation: Slot },
/// A gossip payload attestation must be for the current slot.
PayloadAttestationNotCurrentSlot {
attestation_slot: Slot,
current_slot: Slot,
},
/// One or more payload attesters are not part of the PTC.
PayloadAttestationAttestersNotInPtc {
attesting_indices_len: usize,
attesting_indices_in_ptc: usize,
},
}
impl<T> From<String> for Error<T> {
@@ -654,6 +673,20 @@ where
}
}
/// Mark a Gloas payload envelope as valid and received.
///
/// This must only be called for valid Gloas payloads.
pub fn on_valid_payload_envelope_received(
&mut self,
block_root: Hash256,
) -> Result<(), Error<T::Error>> {
self.proto_array
.on_valid_payload_envelope_received(block_root)
.map_err(Error::FailedToProcessValidExecutionPayload)
}
/// Pre-Gloas only.
///
/// See `ProtoArrayForkChoice::process_execution_payload_validation` for documentation.
pub fn on_valid_execution_payload(
&mut self,
@@ -664,6 +697,8 @@ where
.map_err(Error::FailedToProcessValidExecutionPayload)
}
/// Pre-Gloas only.
///
/// See `ProtoArrayForkChoice::process_execution_payload_invalidation` for documentation.
pub fn on_invalid_execution_payload(
&mut self,
@@ -977,12 +1012,6 @@ where
Ok(())
}
pub fn on_execution_payload(&mut self, block_root: Hash256) -> Result<(), Error<T::Error>> {
self.proto_array
.on_execution_payload(block_root)
.map_err(Error::FailedToProcessValidExecutionPayload)
}
/// Update checkpoints in store if necessary
fn update_checkpoints(
&mut self,
@@ -1158,50 +1187,48 @@ where
&self,
indexed_payload_attestation: &IndexedPayloadAttestation<E>,
is_from_block: AttestationFromBlock,
) -> Result<(), InvalidAttestation> {
) -> Result<(), InvalidPayloadAttestation> {
// This check is from `is_valid_indexed_payload_attestation`, but we do it immediately to
// avoid wasting time on junk attestations.
if indexed_payload_attestation.attesting_indices.is_empty() {
return Err(InvalidAttestation::EmptyAggregationBitfield);
return Err(InvalidPayloadAttestation::EmptyAggregationBitfield);
}
// PTC attestation must be for a known block. If block is unknown, delay consideration until
// the block is found (responsibility of caller).
let block = self
.proto_array
.get_block(&indexed_payload_attestation.data.beacon_block_root)
.ok_or(InvalidAttestation::UnknownHeadBlock {
.ok_or(InvalidPayloadAttestation::UnknownHeadBlock {
beacon_block_root: indexed_payload_attestation.data.beacon_block_root,
})?;
// Not strictly part of the spec, but payload attestations to future slots are MORE INVALID
// than payload attestations to blocks at previous slots.
if block.slot > indexed_payload_attestation.data.slot {
return Err(InvalidAttestation::AttestsToFutureBlock {
return Err(InvalidPayloadAttestation::AttestsToFutureBlock {
block: block.slot,
attestation: indexed_payload_attestation.data.slot,
});
}
// Spec: `if data.slot != state.slot: return` — PTC votes can only
// change the vote for their assigned beacon block.
// PTC votes can only change the vote for their assigned beacon block, return early otherwise
if block.slot != indexed_payload_attestation.data.slot {
return Ok(());
}
// Gossip payload attestations must be for the current slot.
// NOTE: signature is assumed to have been verified by caller.
// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md
if matches!(is_from_block, AttestationFromBlock::False)
&& indexed_payload_attestation.data.slot != self.fc_store.get_current_slot()
{
return Err(InvalidAttestation::PayloadAttestationNotCurrentSlot {
attestation_slot: indexed_payload_attestation.data.slot,
current_slot: self.fc_store.get_current_slot(),
});
}
// A payload attestation voting payload_present for a block in the current slot is
// invalid: the payload cannot be known yet. This only applies to gossip attestations;
// payload attestations from blocks have already been validated by the block producer.
if matches!(is_from_block, AttestationFromBlock::False)
&& self.fc_store.get_current_slot() == block.slot
&& indexed_payload_attestation.data.payload_present
{
return Err(InvalidAttestation::PayloadPresentDuringSameSlot { slot: block.slot });
return Err(
InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot {
attestation_slot: indexed_payload_attestation.data.slot,
current_slot: self.fc_store.get_current_slot(),
},
);
}
Ok(())
@@ -1308,10 +1335,22 @@ where
// Resolve validator indices to PTC committee positions.
let ptc_indices: Vec<usize> = attestation
.attesting_indices_iter()
.attesting_indices
.iter()
.filter_map(|vi| ptc.iter().position(|&p| p == *vi as usize))
.collect();
// Check that all the attesters are in the PTC
if ptc_indices.len() != attestation.attesting_indices.len() {
return Err(
InvalidPayloadAttestation::PayloadAttestationAttestersNotInPtc {
attesting_indices_len: attestation.attesting_indices.len(),
attesting_indices_in_ptc: ptc_indices.len(),
}
.into(),
);
}
for &ptc_index in &ptc_indices {
self.proto_array.process_payload_attestation(
attestation.data.beacon_block_root,
@@ -1614,7 +1653,6 @@ where
persisted_proto_array: proto_array::core::SszContainer,
justified_balances: JustifiedBalances,
reset_payload_statuses: ResetPayloadStatuses,
spec: &ChainSpec,
) -> Result<ProtoArrayForkChoice, Error<T::Error>> {
let mut proto_array = ProtoArrayForkChoice::from_container(
persisted_proto_array.clone(),
@@ -1639,7 +1677,7 @@ where
// Reset all blocks back to being "optimistic". This helps recover from an EL consensus
// fault where an invalid payload becomes valid.
if let Err(e) = proto_array.set_all_blocks_to_optimistic::<E>(spec) {
if let Err(e) = proto_array.set_all_blocks_to_optimistic::<E>() {
// If there is an error resetting the optimistic status then log loudly and revert
// back to a proto-array which does not have the reset applied. This indicates a
// significant error in Lighthouse and warrants detailed investigation.
@@ -1669,7 +1707,6 @@ where
persisted.proto_array,
justified_balances,
reset_payload_statuses,
spec,
)?;
let current_slot = fc_store.get_current_slot();
@@ -1703,7 +1740,7 @@ where
// get a different result.
fork_choice
.proto_array
.set_all_blocks_to_optimistic::<E>(spec)?;
.set_all_blocks_to_optimistic::<E>()?;
// If the second attempt at finding a head fails, return an error since we do not
// expect this scenario.
let _ = fork_choice.get_head(current_slot, spec)?;

View File

@@ -4,8 +4,9 @@ mod metrics;
pub use crate::fork_choice::{
AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters,
InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice,
PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation, ResetPayloadStatuses,
InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, PayloadVerificationStatus,
PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation,
ResetPayloadStatuses,
};
pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::{

View File

@@ -11,7 +11,7 @@ use bls::AggregateSignature;
use fixed_bytes::FixedBytesExtended;
use fork_choice::{
AttestationFromBlock, ForkChoiceStore, InvalidAttestation, InvalidBlock,
PayloadVerificationStatus, QueuedAttestation,
InvalidPayloadAttestation, PayloadVerificationStatus, QueuedAttestation,
};
use state_processing::state_advance::complete_state_advance;
use std::fmt;
@@ -969,8 +969,8 @@ async fn non_block_payload_attestation_for_previous_slot_is_rejected() {
assert!(
matches!(
result,
Err(ForkChoiceError::InvalidAttestation(
InvalidAttestation::PayloadAttestationNotCurrentSlot { .. }
Err(ForkChoiceError::InvalidPayloadAttestation(
InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { .. }
))
),
"gossip payload attestation for previous slot should be rejected, got: {:?}",

View File

@@ -98,7 +98,7 @@ pub enum Operation {
},
/// Simulate receiving and validating an execution payload for `block_root`.
/// Sets `payload_received = true` on the V29 node via the live validation path.
ProcessExecutionPayload {
ProcessExecutionPayloadEnvelope {
block_root: Hash256,
},
AssertPayloadReceived {
@@ -500,9 +500,9 @@ impl ForkChoiceTestDefinition {
// the payload to be in payload_states (payload_received).
node_v29.payload_received = is_timely || is_data_available;
}
Operation::ProcessExecutionPayload { block_root } => {
Operation::ProcessExecutionPayloadEnvelope { block_root } => {
fork_choice
.on_execution_payload(block_root)
.on_valid_payload_envelope_received(block_root)
.unwrap_or_else(|e| {
panic!(
"on_execution_payload op at index {} returned error: {}",

View File

@@ -53,7 +53,7 @@ pub fn get_gloas_chain_following_test_definition() -> ForkChoiceTestDefinition {
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
ops.push(Operation::ProcessExecutionPayloadEnvelope {
block_root: get_root(1),
});
@@ -263,7 +263,7 @@ pub fn get_gloas_find_head_vote_transition_test_definition() -> ForkChoiceTestDe
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
ops.push(Operation::ProcessExecutionPayloadEnvelope {
block_root: get_root(1),
});
@@ -368,7 +368,7 @@ pub fn get_gloas_weight_priority_over_payload_preference_test_definition()
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
ops.push(Operation::ProcessExecutionPayloadEnvelope {
block_root: get_root(1),
});
@@ -538,7 +538,7 @@ pub fn get_gloas_interleaved_attestations_test_definition() -> ForkChoiceTestDef
// Mark root_1 as having received its execution payload so that
// its FULL virtual node exists in the Gloas fork choice tree.
ops.push(Operation::ProcessExecutionPayload {
ops.push(Operation::ProcessExecutionPayloadEnvelope {
block_root: get_root(1),
});
@@ -674,8 +674,8 @@ pub fn get_gloas_payload_received_interleaving_test_definition() -> ForkChoiceTe
expected_payload_status: None,
});
// ProcessExecutionPayload on genesis is a no-op (already received at init).
ops.push(Operation::ProcessExecutionPayload {
// ProcessExecutionPayloadEnvelope on genesis is a no-op (already received at init).
ops.push(Operation::ProcessExecutionPayloadEnvelope {
block_root: get_root(0),
});
@@ -778,7 +778,7 @@ mod tests {
// Mark root 2's execution payload as received so the Full virtual child exists.
if first_gloas_block_full {
ops.push(Operation::ProcessExecutionPayload {
ops.push(Operation::ProcessExecutionPayloadEnvelope {
block_root: get_root(2),
});
}

View File

@@ -165,8 +165,6 @@ pub struct ProtoNode {
pub payload_data_availability_votes: BitVector<U512>,
/// Whether the execution payload for this block has been received and validated locally.
/// Maps to `root in store.payload_states` in the spec.
/// When true, `is_payload_timely` and `is_payload_data_available` return true
/// regardless of PTC vote counts.
#[superstruct(only(V29), partial_getter(copy))]
pub payload_received: bool,
/// The proposer index for this block, used by `should_apply_proposer_boost`
@@ -213,7 +211,7 @@ impl ProtoNode {
return false;
}
node.payload_timeliness_votes.num_set_bits() > E::ptc_size() / 2
node.payload_timeliness_votes.num_set_bits() > E::payload_timely_threshold()
}
pub fn is_payload_data_available<E: EthSpec>(&self) -> bool {
@@ -226,8 +224,8 @@ impl ProtoNode {
return false;
}
// TODO(gloas): add function on EthSpec for DATA_AVAILABILITY_TIMELY_THRESHOLD
node.payload_data_availability_votes.num_set_bits() > E::ptc_size() / 2
node.payload_data_availability_votes.num_set_bits()
> E::data_availability_timely_threshold()
}
}
@@ -369,7 +367,6 @@ pub struct ProtoArray {
pub prune_threshold: usize,
pub nodes: Vec<ProtoNode>,
pub indices: HashMap<Hash256, usize>,
pub previous_proposer_boost: ProposerBoost,
}
impl ProtoArray {
@@ -492,20 +489,14 @@ impl ProtoArray {
.ok_or(Error::DeltaOverflow(parent_index))?;
}
} else {
// V17 child of a V29 parent (fork transition): treat as FULL
// since V17 nodes always have execution payloads inline.
parent_delta.full_delta = parent_delta
.full_delta
.checked_add(delta)
.ok_or(Error::DeltaOverflow(parent_index))?;
// This is a v17 node with a v17 parent.
// There is no empty or full weight for v17 nodes, so nothing to propagate.
// In the tree walk, the v17 nodes have an empty child with 0 weight, which
// wins by default (it is the only child).
}
}
}
// Proposer boost is now applied on-the-fly in `get_weight` during the
// walk, so clear any stale boost from a prior call.
self.previous_proposer_boost = ProposerBoost::default();
Ok(())
}
@@ -641,11 +632,9 @@ impl ProtoArray {
// Anchor gets [True, True]. Others computed from time_into_slot.
block_timeliness_attestation_threshold: is_genesis
|| (is_current_slot
&& time_into_slot < spec.get_unaggregated_attestation_due()),
// TODO(gloas): use Gloas-specific PTC due threshold once
// `get_payload_attestation_due_ms` is on ChainSpec.
&& time_into_slot < spec.get_attestation_due::<E>(current_slot)),
block_timeliness_ptc_threshold: is_genesis
|| (is_current_slot && time_into_slot < spec.get_slot_duration() / 2),
|| (is_current_slot && time_into_slot < spec.get_payload_attestation_due()),
equivocating_attestation_score: 0,
})
};
@@ -682,11 +671,17 @@ impl ProtoArray {
}
/// Spec: `is_head_weak`.
///
/// The spec adds weight from equivocating validators in the head slot's
/// committees. We approximate this with `equivocating_attestation_score`
/// which tracks equivocating validators that voted for this block (close
/// but not identical to committee membership).
// TODO(gloas): the spec adds weight from equivocating validators in the
// head slot's *committees*, regardless of who they voted for. We approximate
// with `equivocating_attestation_score` which only tracks equivocating
// validators whose vote pointed at this block. This under-counts when an
// equivocating validator is in the committee but voted for a different fork,
// which could allow a re-org the spec wouldn't. In practice the deviation
// is small — it requires equivocating validators voting for competing forks
// AND the head weight to be exactly at the reorg threshold boundary.
// Fixing this properly requires committee computation from BeaconState,
// which is not available in proto_array. The fix would be to pass
// pre-computed equivocating committee weight from the beacon_chain caller.
fn is_head_weak<E: EthSpec>(
&self,
head_node: &ProtoNode,
@@ -729,7 +724,6 @@ impl ProtoArray {
.nodes
.get(block_index)
.ok_or(Error::InvalidNodeIndex(block_index))?;
// TODO(gloas): handle parent unknown case?
let parent_index = block
.parent()
.ok_or(Error::NodeUnknown(proposer_boost_root))?;
@@ -753,7 +747,6 @@ impl ProtoArray {
// the parent's slot from the same proposer.
let parent_slot = parent.slot();
let parent_root = parent.root();
// TODO(gloas): handle proposer index for pre-Gloas blocks?
let parent_proposer = parent.proposer_index();
let has_equivocation = self.nodes.iter().any(|node| {
@@ -773,12 +766,10 @@ impl ProtoArray {
Ok(!has_equivocation)
}
/// Process an execution payload for a Gloas block.
/// Process a valid execution payload envelope for a Gloas block.
///
/// Sets `payload_received` to true, which makes `is_payload_timely` and
/// `is_payload_data_available` return true regardless of PTC votes.
/// This maps to `store.payload_states[root] = state` in the spec.
pub fn on_valid_execution_payload(&mut self, block_root: Hash256) -> Result<(), Error> {
/// Sets `payload_received` to true.
pub fn on_valid_payload_envelope_received(&mut self, block_root: Hash256) -> Result<(), Error> {
let index = *self
.indices
.get(&block_root)
@@ -814,6 +805,8 @@ impl ProtoArray {
/// Updates the `verified_node_index` and all ancestors to have validated execution payloads.
///
/// This function is a no-op if called for a Gloas block.
///
/// Returns an error if:
///
/// - The `verified_node_index` is unknown.
@@ -857,18 +850,10 @@ impl ProtoArray {
});
}
},
// Gloas nodes don't carry `ExecutionStatus`. Mark the validated
// block as payload-received so that `is_payload_timely` /
// `is_payload_data_available` and `index == 1` attestations work.
ProtoNode::V29(node) => {
if index == verified_node_index {
node.payload_received = true;
}
if let Some(parent_index) = node.parent {
parent_index
} else {
return Ok(());
}
// Gloas nodes should not be marked valid by this function, which exists only
// for pre-Gloas fork choice.
ProtoNode::V29(_) => {
return Ok(());
}
};
@@ -879,6 +864,7 @@ impl ProtoArray {
/// Invalidate zero or more blocks, as specified by the `InvalidationOperation`.
///
/// See the documentation of `InvalidationOperation` for usage.
// TODO(gloas): this needs some tests for the mixed Gloas/pre-Gloas case.
pub fn propagate_execution_payload_invalidation<E: EthSpec>(
&mut self,
op: &InvalidationOperation,
@@ -978,7 +964,7 @@ impl ProtoArray {
// This block is pre-merge, therefore it has no execution status. Nor do its
// ancestors.
Ok(ExecutionStatus::Irrelevant(_)) => break,
Err(_) => (),
Err(_) => break,
}
}
@@ -1087,9 +1073,6 @@ impl ProtoArray {
});
}
// In the post-Gloas world, always use a virtual tree walk.
//
// Best child/best descendant is dead.
let best_fc_node = self.find_head_walk::<E>(
justified_index,
current_slot,
@@ -1125,26 +1108,6 @@ impl ProtoArray {
Ok((best_fc_node.root, best_fc_node.payload_status))
}
/// Build a parent->children index. Invalid nodes are excluded
/// (they aren't in store.blocks in the spec).
fn build_children_index(&self) -> Vec<Vec<usize>> {
let mut children = vec![vec![]; self.nodes.len()];
for (i, node) in self.nodes.iter().enumerate() {
if node
.execution_status()
.is_ok_and(|status| status.is_invalid())
{
continue;
}
if let Some(parent) = node.parent()
&& parent < children.len()
{
children[parent].push(i);
}
}
children
}
/// Spec: `get_filtered_block_tree`.
///
/// Returns the set of node indices on viable branches — those with at least
@@ -1155,7 +1118,6 @@ impl ProtoArray {
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
children_index: &[Vec<usize>],
) -> HashSet<usize> {
let mut viable = HashSet::new();
self.filter_block_tree::<E>(
@@ -1163,7 +1125,6 @@ impl ProtoArray {
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
children_index,
&mut viable,
);
viable
@@ -1176,17 +1137,25 @@ impl ProtoArray {
current_slot: Slot,
best_justified_checkpoint: Checkpoint,
best_finalized_checkpoint: Checkpoint,
children_index: &[Vec<usize>],
viable: &mut HashSet<usize>,
) -> bool {
let Some(node) = self.nodes.get(node_index) else {
return false;
};
let children = children_index
.get(node_index)
.map(|c| c.as_slice())
.unwrap_or(&[]);
// Skip invalid children — they aren't in store.blocks in the spec.
let children: Vec<usize> = self
.nodes
.iter()
.enumerate()
.filter(|(_, child)| {
child.parent() == Some(node_index)
&& !child
.execution_status()
.is_ok_and(|status| status.is_invalid())
})
.map(|(i, _)| i)
.collect();
if !children.is_empty() {
// Evaluate ALL children (no short-circuit) to mark all viable branches.
@@ -1198,7 +1167,6 @@ impl ProtoArray {
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
children_index,
viable,
)
})
@@ -1243,16 +1211,12 @@ impl ProtoArray {
payload_status: PayloadStatus::Pending,
};
// Build parent->children index once for O(1) lookups.
let children_index = self.build_children_index();
// Spec: `get_filtered_block_tree`.
let viable_nodes = self.get_filtered_block_tree::<E>(
start_index,
current_slot,
best_justified_checkpoint,
best_finalized_checkpoint,
&children_index,
);
// Compute once rather than per-child per-level.
@@ -1261,7 +1225,7 @@ impl ProtoArray {
loop {
let children: Vec<_> = self
.get_node_children(&head, &children_index)?
.get_node_children(&head)?
.into_iter()
.filter(|(fc_node, _)| viable_nodes.contains(&fc_node.proto_node_index))
.collect();
@@ -1272,11 +1236,7 @@ impl ProtoArray {
head = children
.into_iter()
.map(|(child, _)| -> Result<_, Error> {
let proto_node = self
.nodes
.get(child.proto_node_index)
.ok_or(Error::InvalidNodeIndex(child.proto_node_index))?;
.map(|(child, ref proto_node)| -> Result<_, Error> {
let weight = self.get_weight::<E>(
&child,
proto_node,
@@ -1424,7 +1384,6 @@ impl ProtoArray {
fn get_node_children(
&self,
node: &IndexedForkChoiceNode,
children_index: &[Vec<usize>],
) -> Result<Vec<(IndexedForkChoiceNode, ProtoNode)>, Error> {
if node.payload_status == PayloadStatus::Pending {
let proto_node = self
@@ -1438,25 +1397,23 @@ impl ProtoArray {
}
Ok(children)
} else {
let child_indices = children_index
.get(node.proto_node_index)
.map(|c| c.as_slice())
.unwrap_or(&[]);
Ok(child_indices
Ok(self
.nodes
.iter()
.filter_map(|&child_index| {
let child_node = self.nodes.get(child_index)?;
if child_node.get_parent_payload_status() != node.payload_status {
return None;
}
Some((
.enumerate()
.filter(|(_, child_node)| {
child_node.parent() == Some(node.proto_node_index)
&& child_node.get_parent_payload_status() == node.payload_status
})
.map(|(child_index, child_node)| {
(
IndexedForkChoiceNode {
root: child_node.root(),
proto_node_index: child_index,
payload_status: PayloadStatus::Pending,
},
child_node.clone(),
))
)
})
.collect())
}

View File

@@ -2,8 +2,7 @@ use crate::{
JustifiedBalances,
error::Error,
proto_array::{
InvalidationOperation, Iter, NodeDelta, ProposerBoost, ProtoArray, ProtoNode,
calculate_committee_fraction,
InvalidationOperation, Iter, NodeDelta, ProtoArray, ProtoNode, calculate_committee_fraction,
},
ssz_container::SszContainer,
};
@@ -74,6 +73,7 @@ impl From<VoteTracker> for VoteTrackerV28 {
}
}
/// Spec's `LatestMessage` type. Only used in tests.
pub struct LatestMessage {
pub slot: Slot,
pub root: Hash256,
@@ -527,7 +527,6 @@ impl ProtoArrayForkChoice {
prune_threshold: DEFAULT_PRUNE_THRESHOLD,
nodes: Vec::with_capacity(1),
indices: HashMap::with_capacity(1),
previous_proposer_boost: ProposerBoost::default(),
};
let block = Block {
@@ -569,11 +568,18 @@ impl ProtoArrayForkChoice {
})
}
pub fn on_execution_payload(&mut self, block_root: Hash256) -> Result<(), String> {
/// Mark a Gloas payload envelope as valid and received.
///
/// This must only be called for valid Gloas payloads.
pub fn on_valid_payload_envelope_received(
&mut self,
block_root: Hash256,
) -> Result<(), String> {
self.proto_array
.on_valid_execution_payload(block_root)
.on_valid_payload_envelope_received(block_root)
.map_err(|e| format!("Failed to process execution payload: {:?}", e))
}
/// See `ProtoArray::propagate_execution_payload_validation` for documentation.
pub fn process_execution_payload_validation(
&mut self,
@@ -880,10 +886,7 @@ impl ProtoArrayForkChoice {
/// status to be optimistic.
///
/// In practice this means forgetting any `VALID` or `INVALID` statuses.
pub fn set_all_blocks_to_optimistic<E: EthSpec>(
&mut self,
spec: &ChainSpec,
) -> Result<(), String> {
pub fn set_all_blocks_to_optimistic<E: EthSpec>(&mut self) -> Result<(), String> {
// Iterate backwards through all nodes in the `proto_array`. Whilst it's not strictly
// required to do this process in reverse, it seems natural when we consider how LMD votes
// are counted.
@@ -906,7 +909,7 @@ impl ProtoArrayForkChoice {
// Restore the weight of the node, it would have been set to `0` in
// `apply_score_changes` when it was invalidated.
let mut restored_weight: u64 = self
let restored_weight: u64 = self
.votes
.0
.iter()
@@ -922,26 +925,6 @@ impl ProtoArrayForkChoice {
})
.sum();
// If the invalid root was boosted, apply the weight to it and
// ancestors.
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.
if restored_weight > 0 {
let mut node_or_ancestor = node;
@@ -1082,10 +1065,9 @@ impl ProtoArrayForkChoice {
.is_finalized_checkpoint_or_descendant::<E>(descendant_root, best_finalized_checkpoint)
}
/// NOTE: only used in tests.
pub fn latest_message(&self, validator_index: usize) -> Option<LatestMessage> {
if validator_index < self.votes.0.len() {
let vote = &self.votes.0[validator_index];
if let Some(vote) = self.votes.0.get(validator_index) {
if *vote == VoteTracker::default() {
None
} else {

View File

@@ -38,6 +38,7 @@ pub struct SszContainer {
#[superstruct(only(V29))]
pub nodes: Vec<ProtoNode>,
pub indices: Vec<(Hash256, usize)>,
#[superstruct(only(V28))]
pub previous_proposer_boost: ProposerBoost,
}
@@ -50,7 +51,6 @@ impl SszContainerV29 {
prune_threshold: proto_array.prune_threshold,
nodes: proto_array.nodes.clone(),
indices: proto_array.indices.iter().map(|(k, v)| (*k, *v)).collect(),
previous_proposer_boost: proto_array.previous_proposer_boost,
}
}
}
@@ -63,7 +63,6 @@ impl TryFrom<(SszContainerV29, JustifiedBalances)> for ProtoArrayForkChoice {
prune_threshold: from.prune_threshold,
nodes: from.nodes,
indices: from.indices.into_iter().collect::<HashMap<_, _>>(),
previous_proposer_boost: from.previous_proposer_boost,
};
Ok(Self {
@@ -92,7 +91,6 @@ impl From<SszContainerV28> for SszContainerV29 {
})
.collect(),
indices: v28.indices,
previous_proposer_boost: v28.previous_proposer_boost,
}
}
}
@@ -116,7 +114,8 @@ impl From<SszContainerV29> for SszContainerV28 {
})
.collect(),
indices: v29.indices,
previous_proposer_boost: v29.previous_proposer_boost,
// Proposer boost is not tracked in V29 (computed on-the-fly), so reset it.
previous_proposer_boost: ProposerBoost::default(),
}
}
}

View File

@@ -2,7 +2,6 @@ use crate::test_utils::TestRandom;
use crate::{EthSpec, ForkName, PayloadAttestationData};
use bls::AggregateSignature;
use context_deserialize::context_deserialize;
use core::slice::Iter;
use serde::{Deserialize, Serialize};
use ssz_derive::{Decode, Encode};
use ssz_types::VariableList;
@@ -21,12 +20,6 @@ pub struct IndexedPayloadAttestation<E: EthSpec> {
pub signature: AggregateSignature,
}
impl<E: EthSpec> IndexedPayloadAttestation<E> {
pub fn attesting_indices_iter(&self) -> Iter<'_, u64> {
self.attesting_indices.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -107,6 +107,8 @@ pub struct ChainSpec {
pub shard_committee_period: u64,
pub proposer_reorg_cutoff_bps: u64,
pub attestation_due_bps: u64,
pub attestation_due_bps_gloas: u64,
pub payload_attestation_due_bps: u64,
pub aggregate_due_bps: u64,
pub sync_message_due_bps: u64,
pub contribution_due_bps: u64,
@@ -115,6 +117,8 @@ pub struct ChainSpec {
* Derived time values (computed at startup via `compute_derived_values()`)
*/
pub unaggregated_attestation_due: Duration,
pub unaggregated_attestation_due_gloas: Duration,
pub payload_attestation_due: Duration,
pub aggregate_attestation_due: Duration,
pub sync_message_due: Duration,
pub contribution_and_proof_due: Duration,
@@ -877,6 +881,20 @@ impl ChainSpec {
self.unaggregated_attestation_due
}
/// Spec: `get_attestation_due_ms`. Returns the epoch-appropriate threshold.
pub fn get_attestation_due<E: EthSpec>(&self, slot: Slot) -> Duration {
if self.fork_name_at_slot::<E>(slot).gloas_enabled() {
self.unaggregated_attestation_due_gloas
} else {
self.unaggregated_attestation_due
}
}
/// Spec: `get_payload_attestation_due_ms`.
pub fn get_payload_attestation_due(&self) -> Duration {
self.payload_attestation_due
}
/// Get the duration into a slot in which an aggregated attestation is due.
/// Returns the pre-computed value from `compute_derived_values()`.
pub fn get_aggregate_attestation_due(&self) -> Duration {
@@ -949,6 +967,12 @@ impl ChainSpec {
self.unaggregated_attestation_due = self
.compute_slot_component_duration(self.attestation_due_bps)
.expect("invalid chain spec: cannot compute unaggregated_attestation_due");
self.unaggregated_attestation_due_gloas = self
.compute_slot_component_duration(self.attestation_due_bps_gloas)
.expect("invalid chain spec: cannot compute unaggregated_attestation_due_gloas");
self.payload_attestation_due = self
.compute_slot_component_duration(self.payload_attestation_due_bps)
.expect("invalid chain spec: cannot compute payload_attestation_due");
self.aggregate_attestation_due = self
.compute_slot_component_duration(self.aggregate_due_bps)
.expect("invalid chain spec: cannot compute aggregate_attestation_due");
@@ -1079,6 +1103,8 @@ impl ChainSpec {
shard_committee_period: 256,
proposer_reorg_cutoff_bps: 1667,
attestation_due_bps: 3333,
attestation_due_bps_gloas: 2500,
payload_attestation_due_bps: 7500,
aggregate_due_bps: 6667,
sync_message_due_bps: 3333,
contribution_due_bps: 6667,
@@ -1087,6 +1113,8 @@ impl ChainSpec {
* Derived time values (set by `compute_derived_values()`)
*/
unaggregated_attestation_due: Duration::from_millis(3999),
unaggregated_attestation_due_gloas: Duration::from_millis(3000),
payload_attestation_due: Duration::from_millis(9000),
aggregate_attestation_due: Duration::from_millis(8000),
sync_message_due: Duration::from_millis(3999),
contribution_and_proof_due: Duration::from_millis(8000),
@@ -1390,6 +1418,8 @@ impl ChainSpec {
* Precomputed for 6000ms slot: 3333 bps = 1999ms, 6667 bps = 4000ms
*/
unaggregated_attestation_due: Duration::from_millis(1999),
unaggregated_attestation_due_gloas: Duration::from_millis(1500),
payload_attestation_due: Duration::from_millis(4500),
aggregate_attestation_due: Duration::from_millis(4000),
sync_message_due: Duration::from_millis(1999),
contribution_and_proof_due: Duration::from_millis(4000),
@@ -1479,6 +1509,8 @@ impl ChainSpec {
shard_committee_period: 256,
proposer_reorg_cutoff_bps: 1667,
attestation_due_bps: 3333,
attestation_due_bps_gloas: 2500,
payload_attestation_due_bps: 7500,
aggregate_due_bps: 6667,
/*
@@ -1486,6 +1518,8 @@ impl ChainSpec {
* Precomputed for 5000ms slot: 3333 bps = 1666ms, 6667 bps = 3333ms
*/
unaggregated_attestation_due: Duration::from_millis(1666),
unaggregated_attestation_due_gloas: Duration::from_millis(1250),
payload_attestation_due: Duration::from_millis(3750),
aggregate_attestation_due: Duration::from_millis(3333),
sync_message_due: Duration::from_millis(1666),
contribution_and_proof_due: Duration::from_millis(3333),
@@ -2062,6 +2096,12 @@ pub struct Config {
#[serde(default = "default_attestation_due_bps")]
#[serde(with = "serde_utils::quoted_u64")]
attestation_due_bps: u64,
#[serde(default = "default_attestation_due_bps_gloas")]
#[serde(with = "serde_utils::quoted_u64")]
attestation_due_bps_gloas: u64,
#[serde(default = "default_payload_attestation_due_bps")]
#[serde(with = "serde_utils::quoted_u64")]
payload_attestation_due_bps: u64,
#[serde(default = "default_aggregate_due_bps")]
#[serde(with = "serde_utils::quoted_u64")]
aggregate_due_bps: u64,
@@ -2288,6 +2328,14 @@ const fn default_attestation_due_bps() -> u64 {
3333
}
const fn default_attestation_due_bps_gloas() -> u64 {
2500
}
const fn default_payload_attestation_due_bps() -> u64 {
7500
}
const fn default_aggregate_due_bps() -> u64 {
6667
}
@@ -2539,6 +2587,8 @@ impl Config {
proposer_reorg_cutoff_bps: spec.proposer_reorg_cutoff_bps,
attestation_due_bps: spec.attestation_due_bps,
attestation_due_bps_gloas: spec.attestation_due_bps_gloas,
payload_attestation_due_bps: spec.payload_attestation_due_bps,
aggregate_due_bps: spec.aggregate_due_bps,
sync_message_due_bps: spec.sync_message_due_bps,
contribution_due_bps: spec.contribution_due_bps,
@@ -2632,6 +2682,8 @@ impl Config {
min_epochs_for_data_column_sidecars_requests,
proposer_reorg_cutoff_bps,
attestation_due_bps,
attestation_due_bps_gloas,
payload_attestation_due_bps,
aggregate_due_bps,
sync_message_due_bps,
contribution_due_bps,
@@ -2731,6 +2783,8 @@ impl Config {
proposer_reorg_cutoff_bps,
attestation_due_bps,
attestation_due_bps_gloas,
payload_attestation_due_bps,
aggregate_due_bps,
sync_message_due_bps,
contribution_due_bps,
@@ -3634,11 +3688,9 @@ mod yaml_tests {
"EIP7928_FORK_VERSION",
"EIP7928_FORK_EPOCH",
// Gloas params not yet in Config
"ATTESTATION_DUE_BPS_GLOAS",
"AGGREGATE_DUE_BPS_GLOAS",
"SYNC_MESSAGE_DUE_BPS_GLOAS",
"CONTRIBUTION_DUE_BPS_GLOAS",
"PAYLOAD_ATTESTATION_DUE_BPS",
"MAX_REQUEST_PAYLOADS",
// Gloas fork choice params not yet in Config
"REORG_HEAD_WEIGHT_THRESHOLD",

View File

@@ -448,6 +448,11 @@ pub trait EthSpec: 'static + Default + Sync + Send + Clone + Debug + PartialEq +
fn payload_timely_threshold() -> usize {
Self::PTCSize::to_usize() / 2
}
/// Returns the `DATA_AVAILABILITY_TIMELY_THRESHOLD` constant (PTC_SIZE / 2).
fn data_availability_timely_threshold() -> usize {
Self::PTCSize::to_usize() / 2
}
}
/// Macro to inherit some type values from another EthSpec.