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 9a7acbde09..5b82e1de8c 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 @@ -757,40 +757,31 @@ mod tests { // First Gloas block (V29 node). let gloas_slot = if skip_first_gloas_slot { 33 } else { 32 }; - // For Full: execution_payload_parent_hash must match the V17 parent's EL hash. - // The V17 parent's EL hash = ExecutionBlockHash::from_root(get_root(1)) = get_hash(1). - // For Empty: use a non-matching hash. - let parent_hash = if first_gloas_block_full { - get_hash(1) - } else { - get_hash(99) - }; - + // The first Gloas block should always have the pre-Gloas block as its execution parent, + // although this is currently not checked anywhere (the spec doesn't mention this). ops.push(Operation::ProcessBlock { slot: Slot::new(gloas_slot), root: get_root(2), parent_root: get_root(1), justified_checkpoint: get_checkpoint(0), finalized_checkpoint: get_checkpoint(0), - execution_payload_parent_hash: Some(parent_hash), + execution_payload_parent_hash: Some(get_hash(1)), execution_payload_block_hash: Some(get_hash(2)), }); - // Verify the parent_payload_status is correctly set. - let expected_parent_status = if first_gloas_block_full { - PayloadStatus::Full - } else { - PayloadStatus::Empty - }; + // Parent payload status of fork boundary block should always be Empty. + let expected_parent_status = PayloadStatus::Empty; ops.push(Operation::AssertParentPayloadStatus { block_root: get_root(2), expected_status: expected_parent_status, }); // Mark root 2's execution payload as received so the Full virtual child exists. - ops.push(Operation::ProcessExecutionPayload { - block_root: get_root(2), - }); + if first_gloas_block_full { + ops.push(Operation::ProcessExecutionPayload { + block_root: get_root(2), + }); + } // Extend the chain with another V29 block (Full child of root 2). ops.push(Operation::ProcessBlock { @@ -799,7 +790,11 @@ mod tests { parent_root: get_root(2), justified_checkpoint: get_checkpoint(0), finalized_checkpoint: get_checkpoint(0), - execution_payload_parent_hash: Some(get_hash(2)), + execution_payload_parent_hash: if first_gloas_block_full { + Some(get_hash(2)) + } else { + Some(get_hash(1)) + }, execution_payload_block_hash: Some(get_hash(3)), }); @@ -813,6 +808,15 @@ mod tests { expected_payload_status: None, }); + ops.push(Operation::AssertParentPayloadStatus { + block_root: get_root(3), + expected_status: if first_gloas_block_full { + PayloadStatus::Full + } else { + PayloadStatus::Empty + }, + }); + ForkChoiceTestDefinition { finalized_block_slot: Slot::new(0), justified_checkpoint: get_checkpoint(0), diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 452679d7a3..ffe60d3a50 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -570,31 +570,31 @@ impl ProtoArray { block_root: block.root, })?; - let parent_payload_status: PayloadStatus = if let Some(parent_node) = - parent_index.and_then(|idx| self.nodes.get(idx)) - { - // Get the parent's execution block hash, handling both V17 and V29 nodes. - // V17 parents occur during the Gloas fork transition. - // TODO(gloas): the spec's `get_parent_payload_status` assumes all blocks are - // post-Gloas with bids. Revisit once the spec clarifies fork-transition behavior. - let parent_el_block_hash = match parent_node { - ProtoNode::V29(v29) => Some(v29.execution_payload_block_hash), - ProtoNode::V17(v17) => v17.execution_status.block_hash(), - }; - // Per spec's `is_parent_node_full`: if the child's EL parent hash - // matches the parent's EL block hash, the child extends the parent's - // payload chain, meaning the parent was Full. - if parent_el_block_hash.is_some_and(|hash| execution_payload_parent_hash == hash) { - PayloadStatus::Full + let parent_payload_status: PayloadStatus = + if let Some(parent_node) = parent_index.and_then(|idx| self.nodes.get(idx)) { + match parent_node { + ProtoNode::V29(v29) => { + // Both parent and child are Gloas blocks. The parent is full if the + // block hash in the parent node matches the parent block hash in the + // child bid. + if execution_payload_parent_hash == v29.execution_payload_block_hash { + PayloadStatus::Full + } else { + PayloadStatus::Empty + } + } + ProtoNode::V17(_) => { + // Parent is pre-Gloas, pre-Gloas blocks are treated as having Empty + // payload status. This case is reached during the fork transition. + PayloadStatus::Empty + } + } } else { - PayloadStatus::Empty - } - } else { - // Parent is missing (genesis or pruned due to finalization). Default to Full - // since this path should only be hit at Gloas genesis, and extending the payload - // chain is the safe default. - PayloadStatus::Full - }; + // TODO(gloas): re-assess this assumption + // Parent is missing (genesis or pruned due to finalization). Default to Full + // since this path should only be hit at Gloas genesis. + PayloadStatus::Full + }; // Per spec `get_forkchoice_store`: the anchor (genesis) block has // its payload state initialized (`payload_states = {anchor_root: ...}`).