From a09d3f26a4509ca641d40670c5e04f5c8a4cbb28 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Sun, 31 May 2026 12:28:33 +0300 Subject: [PATCH] gloas-head-block-number --- .../src/fork_choice_test_definition.rs | 26 +++ .../gloas_payload.rs | 154 ++++++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index 43b76ec7cb..cc82e2ab2a 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -124,6 +124,14 @@ pub enum Operation { #[serde(default)] proposer_boost_root: Option, }, + /// Assert the root returned by `latest_parent_full_block` for `block_root`. + AssertLatestFullPayloadBlock { + block_root: Hash256, + expected: Option, + /// Override the proposer boost root. Defaults to `Hash256::zero()`. + #[serde(default)] + proposer_boost_root: Option, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -606,6 +614,24 @@ impl ForkChoiceTestDefinition { op_index ); } + Operation::AssertLatestFullPayloadBlock { + block_root, + expected, + proposer_boost_root, + } => { + let actual = fork_choice + .latest_parent_full_block::( + block_root, + proposer_boost_root.unwrap_or_else(Hash256::zero), + &spec, + ) + .unwrap(); + assert_eq!( + actual, expected, + "latest_parent_full_block mismatch at op index {}", + 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 ac4f8992c4..76348289c8 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 @@ -1235,4 +1235,158 @@ mod tests { } .run(); } + + /// `latest_parent_full_block` returns the block itself when its own payload is Full. + #[test] + fn latest_full_payload_block_returns_head_when_full() { + let mut ops = vec![]; + + // Gloas block with a received payload, so it is Full at its own slot. + ops.push(Operation::ProcessBlock { + slot: Slot::new(1), + root: get_root(1), + parent_root: get_root(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: Some(get_hash(0)), + execution_payload_block_hash: Some(get_hash(1)), + }); + ops.push(Operation::ProcessExecutionPayloadEnvelope { + block_root: get_root(1), + }); + + ops.push(Operation::AssertLatestFullPayloadBlock { + block_root: get_root(1), + expected: Some(get_root(1)), + proposer_boost_root: None, + }); + + ForkChoiceTestDefinition { + finalized_block_slot: Slot::new(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + operations: ops, + execution_payload_parent_hash: Some(get_hash(42)), + execution_payload_block_hash: Some(get_hash(0)), + spec: Some(gloas_spec()), + } + .run(); + } + + /// `latest_parent_full_block` walks back past Empty descendants to the latest Full ancestor. + /// + /// root_1 (Full) -> root_2 (Empty) -> root_3 (Empty) + #[test] + fn latest_full_payload_block_walks_back_to_full_ancestor() { + let mut ops = vec![]; + + // root_1: payload received -> Full. + ops.push(Operation::ProcessBlock { + slot: Slot::new(1), + root: get_root(1), + parent_root: get_root(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: Some(get_hash(0)), + execution_payload_block_hash: Some(get_hash(1)), + }); + ops.push(Operation::ProcessExecutionPayloadEnvelope { + block_root: get_root(1), + }); + + // root_2 and root_3: no payload received -> Empty. + ops.push(Operation::ProcessBlock { + slot: Slot::new(2), + root: get_root(2), + parent_root: get_root(1), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: Some(get_hash(1)), + execution_payload_block_hash: Some(get_hash(2)), + }); + ops.push(Operation::ProcessBlock { + slot: Slot::new(3), + root: get_root(3), + 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_block_hash: Some(get_hash(3)), + }); + + ops.push(Operation::AssertLatestFullPayloadBlock { + block_root: get_root(3), + expected: Some(get_root(1)), + proposer_boost_root: None, + }); + + ForkChoiceTestDefinition { + finalized_block_slot: Slot::new(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + operations: ops, + execution_payload_parent_hash: Some(get_hash(42)), + execution_payload_block_hash: Some(get_hash(0)), + spec: Some(gloas_spec()), + } + .run(); + } + + /// `latest_parent_full_block` returns `None` when the walk reaches the pre-Gloas boundary + /// without finding a Full payload (the documented TODO case). + /// + /// root_1 (V17, slot 31) -> root_2 (V29 Empty) -> root_3 (V29 Empty) + #[test] + fn latest_full_payload_block_none_at_pre_gloas_boundary() { + let mut ops = vec![]; + + // Pre-Gloas (V17) block at the last pre-Gloas slot. + ops.push(Operation::ProcessBlock { + slot: Slot::new(31), + root: get_root(1), + parent_root: get_root(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: None, + execution_payload_block_hash: None, + }); + + // Two Gloas (V29) blocks with no payload received -> Empty. + ops.push(Operation::ProcessBlock { + slot: Slot::new(32), + root: get_root(2), + parent_root: get_root(1), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + execution_payload_parent_hash: Some(get_hash(1)), + execution_payload_block_hash: Some(get_hash(2)), + }); + ops.push(Operation::ProcessBlock { + slot: Slot::new(33), + root: get_root(3), + 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_block_hash: Some(get_hash(3)), + }); + + // The walk hits the V17 boundary block before any Full payload, so returns `None`. + ops.push(Operation::AssertLatestFullPayloadBlock { + block_root: get_root(3), + expected: None, + proposer_boost_root: None, + }); + + ForkChoiceTestDefinition { + finalized_block_slot: Slot::new(0), + justified_checkpoint: get_checkpoint(0), + finalized_checkpoint: get_checkpoint(0), + operations: ops, + execution_payload_parent_hash: None, + execution_payload_block_hash: None, + spec: Some(gloas_fork_boundary_spec()), + } + .run(); + } }