diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index fe66b2f8d6..a452d528a1 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1961,12 +1961,13 @@ fn load_parent>( { if block.as_block().is_parent_block_full(parent_bid_block_hash) { // TODO(gloas): loading the envelope here is not very efficient - let envelope = chain.store.get_payload_envelope(&root)?.ok_or_else(|| { - BeaconChainError::DBInconsistent(format!( - "Missing envelope for parent block {root:?}", - )) - })?; - (StatePayloadStatus::Full, envelope.message.state_root) + if let Some(envelope) = chain.store.get_payload_envelope(&root)? { + (StatePayloadStatus::Full, envelope.message.state_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::Pending, parent_block.state_root()) + } } else { (StatePayloadStatus::Pending, parent_block.state_root()) } diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 16c7df4ca2..b36e9c2117 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -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::(&head_root) + .head_payload_status::( + &head_root, + current_slot, + ) .unwrap_or_else(|| { panic!( "AssertHeadPayloadStatus: head root not found at op index {}", diff --git a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs index 84e2878d32..e19fb196f2 100644 --- a/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs +++ b/consensus/proto_array/src/fork_choice_test_definition/gloas_payload.rs @@ -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 { diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 908d391401..5a0f49e64d 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -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 diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index e1b8c43ff1..b50db01561 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -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(&self, head_root: &Hash256) -> Option { + /// 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( + &self, + head_root: &Hash256, + current_slot: Slot, + ) -> Option { 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)