This commit is contained in:
hopinheimer
2026-03-16 07:00:51 -04:00
parent 0df749f0a2
commit 916d9fb018
5 changed files with 50 additions and 23 deletions

View File

@@ -1961,12 +1961,13 @@ fn load_parent<T: BeaconChainTypes, B: AsBlock<T::EthSpec>>(
{ {
if block.as_block().is_parent_block_full(parent_bid_block_hash) { if block.as_block().is_parent_block_full(parent_bid_block_hash) {
// TODO(gloas): loading the envelope here is not very efficient // TODO(gloas): loading the envelope here is not very efficient
let envelope = chain.store.get_payload_envelope(&root)?.ok_or_else(|| { if let Some(envelope) = chain.store.get_payload_envelope(&root)? {
BeaconChainError::DBInconsistent(format!( (StatePayloadStatus::Full, envelope.message.state_root)
"Missing envelope for parent block {root:?}", } else {
)) // The envelope hasn't been stored yet (e.g. genesis block, or payload
})?; // not yet delivered). Fall back to the pending/empty state.
(StatePayloadStatus::Full, envelope.message.state_root) (StatePayloadStatus::Pending, parent_block.state_root())
}
} else { } else {
(StatePayloadStatus::Pending, parent_block.state_root()) (StatePayloadStatus::Pending, parent_block.state_root())
} }

View File

@@ -91,6 +91,7 @@ pub enum Operation {
AssertHeadPayloadStatus { AssertHeadPayloadStatus {
head_root: Hash256, head_root: Hash256,
expected_status: PayloadStatus, expected_status: PayloadStatus,
current_slot: Slot,
}, },
SetPayloadTiebreak { SetPayloadTiebreak {
block_root: Hash256, block_root: Hash256,
@@ -456,9 +457,13 @@ impl ForkChoiceTestDefinition {
Operation::AssertHeadPayloadStatus { Operation::AssertHeadPayloadStatus {
head_root, head_root,
expected_status, expected_status,
current_slot,
} => { } => {
let actual = fork_choice let actual = fork_choice
.head_payload_status::<MainnetEthSpec>(&head_root) .head_payload_status::<MainnetEthSpec>(
&head_root,
current_slot,
)
.unwrap_or_else(|| { .unwrap_or_else(|| {
panic!( panic!(
"AssertHeadPayloadStatus: head root not found at op index {}", "AssertHeadPayloadStatus: head root not found at op index {}",

View File

@@ -145,6 +145,7 @@ pub fn get_gloas_payload_probe_test_definition() -> ForkChoiceTestDefinition {
ops.push(Operation::AssertHeadPayloadStatus { ops.push(Operation::AssertHeadPayloadStatus {
head_root: get_root(1), head_root: get_root(1),
expected_status: PayloadStatus::Empty, expected_status: PayloadStatus::Empty,
current_slot: Slot::new(0),
}); });
// Flip validator 0 to Empty; both bits now clear. // Flip validator 0 to Empty; both bits now clear.
@@ -170,6 +171,7 @@ pub fn get_gloas_payload_probe_test_definition() -> ForkChoiceTestDefinition {
ops.push(Operation::AssertHeadPayloadStatus { ops.push(Operation::AssertHeadPayloadStatus {
head_root: get_root(1), head_root: get_root(1),
expected_status: PayloadStatus::Empty, expected_status: PayloadStatus::Empty,
current_slot: Slot::new(0),
}); });
// Same-slot attestation to a new head candidate should be Pending (no payload bucket change). // Same-slot attestation to a new head candidate should be Pending (no payload bucket change).
@@ -204,6 +206,7 @@ pub fn get_gloas_payload_probe_test_definition() -> ForkChoiceTestDefinition {
ops.push(Operation::AssertHeadPayloadStatus { ops.push(Operation::AssertHeadPayloadStatus {
head_root: get_root(5), head_root: get_root(5),
expected_status: PayloadStatus::Empty, expected_status: PayloadStatus::Empty,
current_slot: Slot::new(0),
}); });
ForkChoiceTestDefinition { ForkChoiceTestDefinition {

View File

@@ -1448,8 +1448,8 @@ fn child_matches_parent_payload_preference(
&& parent_v29.empty_payload_weight > parent_v29.full_payload_weight && parent_v29.empty_payload_weight > parent_v29.full_payload_weight
{ {
false false
} else { } else if use_tiebreaker_only {
// Equal weights (or current-slot parent): tiebreaker per spec. // Previous slot: should_extend_payload = is_payload_timely && is_payload_data_available.
is_payload_timely( is_payload_timely(
&parent_v29.payload_timeliness_votes, &parent_v29.payload_timeliness_votes,
ptc_size, ptc_size,
@@ -1459,6 +1459,10 @@ fn child_matches_parent_payload_preference(
ptc_size, ptc_size,
parent_v29.payload_received, parent_v29.payload_received,
) )
} else {
// Not previous slot: should_extend_payload = true.
// Full wins the tiebreaker (1 > 0) when the payload has been received.
parent_v29.payload_received
}; };
if prefers_full { if prefers_full {
child_v29.parent_payload_status == PayloadStatus::Full child_v29.parent_payload_status == PayloadStatus::Full

View File

@@ -991,29 +991,43 @@ impl ProtoArrayForkChoice {
.map(|node| node.weight()) .map(|node| node.weight())
} }
/// Returns the payload status of the head node based on accumulated weights. /// Returns the payload status of the head node based on accumulated weights and tiebreaker.
/// ///
/// Returns `Full` if `full_payload_weight > empty_payload_weight`. /// Returns `Full` if `full_payload_weight > empty_payload_weight`.
/// Returns `Empty` if `empty_payload_weight > full_payload_weight`. /// Returns `Empty` if `empty_payload_weight > full_payload_weight`.
/// On ties, consult the node's runtime `payload_tiebreak`: prefer `Full` only when timely and /// On ties:
/// data is available, otherwise `Empty`. /// - Previous slot (`slot + 1 == current_slot`): prefer Full only when timely and
/// Returns `Empty` otherwise. Returns `None` for V17 nodes. /// data available (per `should_extend_payload`).
pub fn head_payload_status<E: EthSpec>(&self, head_root: &Hash256) -> Option<PayloadStatus> { /// - Otherwise: prefer Full when payload has been received.
/// Returns `None` for V17 nodes.
pub fn head_payload_status<E: EthSpec>(
&self,
head_root: &Hash256,
current_slot: Slot,
) -> Option<PayloadStatus> {
let node = self.get_proto_node(head_root)?; let node = self.get_proto_node(head_root)?;
let v29 = node.as_v29().ok()?; let v29 = node.as_v29().ok()?;
if v29.full_payload_weight > v29.empty_payload_weight { if v29.full_payload_weight > v29.empty_payload_weight {
Some(PayloadStatus::Full) Some(PayloadStatus::Full)
} else if v29.empty_payload_weight > v29.full_payload_weight { } else if v29.empty_payload_weight > v29.full_payload_weight {
Some(PayloadStatus::Empty) Some(PayloadStatus::Empty)
} else if is_payload_timely( } else if node.slot() + 1 == current_slot {
&v29.payload_timeliness_votes, // Previous slot: should_extend_payload = is_payload_timely && is_payload_data_available
E::ptc_size(), if is_payload_timely(
v29.payload_received, &v29.payload_timeliness_votes,
) && is_payload_data_available( E::ptc_size(),
&v29.payload_data_availability_votes, v29.payload_received,
E::ptc_size(), ) && is_payload_data_available(
v29.payload_received, &v29.payload_data_availability_votes,
) { E::ptc_size(),
v29.payload_received,
) {
Some(PayloadStatus::Full)
} else {
Some(PayloadStatus::Empty)
}
} else if v29.payload_received {
// Not previous slot: Full wins tiebreaker (1 > 0) when payload received.
Some(PayloadStatus::Full) Some(PayloadStatus::Full)
} else { } else {
Some(PayloadStatus::Empty) Some(PayloadStatus::Empty)