Fix fork transition case

This commit is contained in:
Michael Sproul
2026-04-01 13:29:48 +11:00
parent afb1f0ae2d
commit edae39cc29
2 changed files with 48 additions and 44 deletions

View File

@@ -757,40 +757,31 @@ mod tests {
// First Gloas block (V29 node). // First Gloas block (V29 node).
let gloas_slot = if skip_first_gloas_slot { 33 } else { 32 }; 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 first Gloas block should always have the pre-Gloas block as its execution parent,
// The V17 parent's EL hash = ExecutionBlockHash::from_root(get_root(1)) = get_hash(1). // although this is currently not checked anywhere (the spec doesn't mention this).
// For Empty: use a non-matching hash.
let parent_hash = if first_gloas_block_full {
get_hash(1)
} else {
get_hash(99)
};
ops.push(Operation::ProcessBlock { ops.push(Operation::ProcessBlock {
slot: Slot::new(gloas_slot), slot: Slot::new(gloas_slot),
root: get_root(2), root: get_root(2),
parent_root: get_root(1), parent_root: get_root(1),
justified_checkpoint: get_checkpoint(0), justified_checkpoint: get_checkpoint(0),
finalized_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)), execution_payload_block_hash: Some(get_hash(2)),
}); });
// Verify the parent_payload_status is correctly set. // Parent payload status of fork boundary block should always be Empty.
let expected_parent_status = if first_gloas_block_full { let expected_parent_status = PayloadStatus::Empty;
PayloadStatus::Full
} else {
PayloadStatus::Empty
};
ops.push(Operation::AssertParentPayloadStatus { ops.push(Operation::AssertParentPayloadStatus {
block_root: get_root(2), block_root: get_root(2),
expected_status: expected_parent_status, expected_status: expected_parent_status,
}); });
// Mark root 2's execution payload as received so the Full virtual child exists. // 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::ProcessExecutionPayload {
block_root: get_root(2), block_root: get_root(2),
}); });
}
// Extend the chain with another V29 block (Full child of root 2). // Extend the chain with another V29 block (Full child of root 2).
ops.push(Operation::ProcessBlock { ops.push(Operation::ProcessBlock {
@@ -799,7 +790,11 @@ mod tests {
parent_root: get_root(2), parent_root: get_root(2),
justified_checkpoint: get_checkpoint(0), justified_checkpoint: get_checkpoint(0),
finalized_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)), execution_payload_block_hash: Some(get_hash(3)),
}); });
@@ -813,6 +808,15 @@ mod tests {
expected_payload_status: None, 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 { ForkChoiceTestDefinition {
finalized_block_slot: Slot::new(0), finalized_block_slot: Slot::new(0),
justified_checkpoint: get_checkpoint(0), justified_checkpoint: get_checkpoint(0),

View File

@@ -570,29 +570,29 @@ impl ProtoArray {
block_root: block.root, block_root: block.root,
})?; })?;
let parent_payload_status: PayloadStatus = if let Some(parent_node) = let parent_payload_status: PayloadStatus =
parent_index.and_then(|idx| self.nodes.get(idx)) if let Some(parent_node) = parent_index.and_then(|idx| self.nodes.get(idx)) {
{ match parent_node {
// Get the parent's execution block hash, handling both V17 and V29 nodes. ProtoNode::V29(v29) => {
// V17 parents occur during the Gloas fork transition. // Both parent and child are Gloas blocks. The parent is full if the
// TODO(gloas): the spec's `get_parent_payload_status` assumes all blocks are // block hash in the parent node matches the parent block hash in the
// post-Gloas with bids. Revisit once the spec clarifies fork-transition behavior. // child bid.
let parent_el_block_hash = match parent_node { if execution_payload_parent_hash == v29.execution_payload_block_hash {
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 PayloadStatus::Full
} else { } else {
PayloadStatus::Empty 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 { } else {
// TODO(gloas): re-assess this assumption
// Parent is missing (genesis or pruned due to finalization). Default to Full // 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 // since this path should only be hit at Gloas genesis.
// chain is the safe default.
PayloadStatus::Full PayloadStatus::Full
}; };