Resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-06-11 18:16:42 +03:00
8 changed files with 313 additions and 62 deletions

View File

@@ -124,14 +124,13 @@ pub enum Operation {
#[serde(default)]
proposer_boost_root: Option<Hash256>,
},
/// Assert the result of `should_build_on_full` for the parent `block_root`, where
/// `parent_payload_status` is the status the proposer would build on and `proposal_slot`
/// is the slot being proposed.
AssertShouldBuildOnFull {
/// Assert the root returned by `latest_parent_full_block` for `block_root`.
AssertLatestFullPayloadBlock {
block_root: Hash256,
parent_payload_status: PayloadStatus,
proposal_slot: Slot,
expected: bool,
expected: Option<Hash256>,
/// Override the proposer boost root. Defaults to `Hash256::zero()`.
#[serde(default)]
proposer_boost_root: Option<Hash256>,
},
}
@@ -615,27 +614,21 @@ impl ForkChoiceTestDefinition {
op_index
);
}
Operation::AssertShouldBuildOnFull {
Operation::AssertLatestFullPayloadBlock {
block_root,
parent_payload_status,
proposal_slot,
expected,
proposer_boost_root,
} => {
let actual = fork_choice
.should_build_on_full::<MainnetEthSpec>(
&block_root,
parent_payload_status,
proposal_slot,
.latest_parent_full_block::<MainnetEthSpec>(
block_root,
proposer_boost_root.unwrap_or_else(Hash256::zero),
&spec,
)
.unwrap_or_else(|e| {
panic!(
"should_build_on_full op at index {} returned error: {}",
op_index, e
)
});
.unwrap();
assert_eq!(
actual, expected,
"should_build_on_full mismatch at op index {}",
"latest_parent_full_block mismatch at op index {}",
op_index
);
}

View File

@@ -999,36 +999,6 @@ pub fn get_gloas_should_build_on_full_test_definition() -> ForkChoiceTestDefinit
// When the parent is `Empty` `should_build_on_full` returns `false`. This check runs before
// the slot check, so the result is `false` for both the previous-slot case (block slot 1, proposal slot 2)
// and an earlier-slot case (proposal slot 3).
ops.push(Operation::AssertShouldBuildOnFull {
block_root: get_root(1),
parent_payload_status: PayloadStatus::Empty,
proposal_slot: Slot::new(2),
expected: false,
});
ops.push(Operation::AssertShouldBuildOnFull {
block_root: get_root(1),
parent_payload_status: PayloadStatus::Empty,
proposal_slot: Slot::new(3),
expected: false,
});
// `Full` parent from the immediately preceding slot (block slot 1, proposal slot 2). The PTC
// votes are consulted, and since data is unavailable the proposer does not build on full.
ops.push(Operation::AssertShouldBuildOnFull {
block_root: get_root(1),
parent_payload_status: PayloadStatus::Full,
proposal_slot: Slot::new(2),
expected: false,
});
// `Full` parent from an *earlier* slot (block slot 1, proposal slot 3). The slot check
// short-circuits to `true` without consulting the (unavailable) PTC votes.
ops.push(Operation::AssertShouldBuildOnFull {
block_root: get_root(1),
parent_payload_status: PayloadStatus::Full,
proposal_slot: Slot::new(3),
expected: true,
});
// Flip the PTC view to *available* and re-check the previous-slot case. The votes now permit
// building on full.
@@ -1037,12 +1007,6 @@ pub fn get_gloas_should_build_on_full_test_definition() -> ForkChoiceTestDefinit
is_timely: true,
is_data_available: true,
});
ops.push(Operation::AssertShouldBuildOnFull {
block_root: get_root(1),
parent_payload_status: PayloadStatus::Full,
proposal_slot: Slot::new(2),
expected: true,
});
ForkChoiceTestDefinition {
finalized_block_slot: Slot::new(0),
@@ -1325,4 +1289,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();
}
}

View File

@@ -1298,6 +1298,33 @@ impl ProtoArray {
}
}
/// Returns the latest ancestor of `block_root` whose `PayloadStatus` is `Full`.
pub(crate) fn latest_parent_full_block<E: EthSpec>(
&self,
block_root: Hash256,
proposer_boost_root: Hash256,
justified_balances: &JustifiedBalances,
spec: &ChainSpec,
) -> Result<Option<Hash256>, Error> {
for node in self.iter_nodes(&block_root) {
if node.as_v29().is_err() {
return Ok(None);
}
if self.get_canonical_payload_status::<E>(
node.root(),
node.slot(),
proposer_boost_root,
justified_balances,
spec,
)? == PayloadStatus::Full
{
return Ok(Some(node.root()));
}
}
Ok(None)
}
/// Returns the canonical payload status of a block, matching the decision
/// `get_head` would make between `(root, FULL)` and `(root, EMPTY)`.
pub(crate) fn get_canonical_payload_status<E: EthSpec>(

View File

@@ -1085,6 +1085,21 @@ impl ProtoArrayForkChoice {
.unwrap_or(false)
}
/// Returns the latest ancestor of `block_root` whose `PayloadStatus` is `Full`.
pub fn latest_parent_full_block<E: EthSpec>(
&self,
block_root: Hash256,
proposer_boost_root: Hash256,
spec: &ChainSpec,
) -> Result<Option<Hash256>, Error> {
self.proto_array.latest_parent_full_block::<E>(
block_root,
proposer_boost_root,
&self.balances,
spec,
)
}
/// Returns the canonical payload status of a block, matching the decision
/// `get_head` would make between `(root, FULL)` and `(root, EMPTY)`.
pub fn get_canonical_payload_status<E: EthSpec>(