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

@@ -91,6 +91,7 @@ pub enum Operation {
AssertHeadPayloadStatus {
head_root: Hash256,
expected_status: PayloadStatus,
current_slot: Slot,
},
SetPayloadTiebreak {
block_root: Hash256,
@@ -456,9 +457,13 @@ impl ForkChoiceTestDefinition {
Operation::AssertHeadPayloadStatus {
head_root,
expected_status,
current_slot,
} => {
let actual = fork_choice
.head_payload_status::<MainnetEthSpec>(&head_root)
.head_payload_status::<MainnetEthSpec>(
&head_root,
current_slot,
)
.unwrap_or_else(|| {
panic!(
"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 {
head_root: get_root(1),
expected_status: PayloadStatus::Empty,
current_slot: Slot::new(0),
});
// 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 {
head_root: get_root(1),
expected_status: PayloadStatus::Empty,
current_slot: Slot::new(0),
});
// 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 {
head_root: get_root(5),
expected_status: PayloadStatus::Empty,
current_slot: Slot::new(0),
});
ForkChoiceTestDefinition {

View File

@@ -1448,8 +1448,8 @@ fn child_matches_parent_payload_preference(
&& parent_v29.empty_payload_weight > parent_v29.full_payload_weight
{
false
} else {
// Equal weights (or current-slot parent): tiebreaker per spec.
} else if use_tiebreaker_only {
// Previous slot: should_extend_payload = is_payload_timely && is_payload_data_available.
is_payload_timely(
&parent_v29.payload_timeliness_votes,
ptc_size,
@@ -1459,6 +1459,10 @@ fn child_matches_parent_payload_preference(
ptc_size,
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 {
child_v29.parent_payload_status == PayloadStatus::Full

View File

@@ -991,29 +991,43 @@ impl ProtoArrayForkChoice {
.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 `Empty` if `empty_payload_weight > full_payload_weight`.
/// On ties, consult the node's runtime `payload_tiebreak`: prefer `Full` only when timely and
/// data is available, otherwise `Empty`.
/// Returns `Empty` otherwise. Returns `None` for V17 nodes.
pub fn head_payload_status<E: EthSpec>(&self, head_root: &Hash256) -> Option<PayloadStatus> {
/// On ties:
/// - Previous slot (`slot + 1 == current_slot`): prefer Full only when timely and
/// data available (per `should_extend_payload`).
/// - 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 v29 = node.as_v29().ok()?;
if v29.full_payload_weight > v29.empty_payload_weight {
Some(PayloadStatus::Full)
} else if v29.empty_payload_weight > v29.full_payload_weight {
Some(PayloadStatus::Empty)
} else if is_payload_timely(
&v29.payload_timeliness_votes,
E::ptc_size(),
v29.payload_received,
) && is_payload_data_available(
&v29.payload_data_availability_votes,
E::ptc_size(),
v29.payload_received,
) {
} else if node.slot() + 1 == current_slot {
// Previous slot: should_extend_payload = is_payload_timely && is_payload_data_available
if is_payload_timely(
&v29.payload_timeliness_votes,
E::ptc_size(),
v29.payload_received,
) && is_payload_data_available(
&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)
} else {
Some(PayloadStatus::Empty)