Gloas alpha spec 9 (#9393)

Changes implemented

Ensure bids are for a higher slot than their parent (https://github.com/ethereum/consensus-specs/pull/5302)
Ignore PTC attestations for empty assigned slots (https://github.com/ethereum/consensus-specs/pull/5281)
Limit should_build_on_full checks to the previous slot (https://github.com/ethereum/consensus-specs/pull/5309)
Apply proposer boost if dependent roots match (https://github.com/ethereum/consensus-specs/pull/5306)
Exclude slashed validators from proposing (EIP-8045) (https://github.com/ethereum/consensus-specs/pull/5115)
Force the proposer to reorg late payloads (https://github.com/ethereum/consensus-specs/pull/5210)
Remove support for old deposit mechanism in Fulu (https://github.com/ethereum/consensus-specs/pull/4704)


  


Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@gmail.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
Eitan Seri-Levi
2026-06-15 16:56:09 -07:00
committed by GitHub
parent d8e406b6ac
commit 58e35bc96f
33 changed files with 785 additions and 247 deletions

View File

@@ -124,6 +124,15 @@ 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 {
block_root: Hash256,
parent_payload_status: PayloadStatus,
proposal_slot: Slot,
expected: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -606,6 +615,30 @@ impl ForkChoiceTestDefinition {
op_index
);
}
Operation::AssertShouldBuildOnFull {
block_root,
parent_payload_status,
proposal_slot,
expected,
} => {
let actual = fork_choice
.should_build_on_full::<MainnetEthSpec>(
&block_root,
parent_payload_status,
proposal_slot,
)
.unwrap_or_else(|e| {
panic!(
"should_build_on_full op at index {} returned error: {}",
op_index, e
)
});
assert_eq!(
actual, expected,
"should_build_on_full mismatch at op index {}",
op_index
);
}
}
}
}

View File

@@ -971,6 +971,90 @@ pub fn get_gloas_proposer_boost_flips_ancestor_test_definition() -> ForkChoiceTe
}
}
/// Tests the slot check in `should_build_on_full`. When the parent is from an earlier slot the
/// function returns `true` and ignores PTC data-availability votes. It only checks those votes
/// when the parent is from the immediately preceding slot.
pub fn get_gloas_should_build_on_full_test_definition() -> ForkChoiceTestDefinition {
let mut ops = vec![];
// Block 1 at slot 1, child of genesis.
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)),
});
// PTC has voted the payload data unavailable. `is_timely` sets `payload_received` so the votes
// are consulted, and clearing the data-availability bits gives the "false" votes a majority.
ops.push(Operation::SetPayloadTiebreak {
block_root: get_root(1),
is_timely: true,
is_data_available: false,
});
// 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.
ops.push(Operation::SetPayloadTiebreak {
block_root: get_root(1),
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),
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()),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1160,6 +1244,12 @@ mod tests {
test.run();
}
#[test]
fn should_build_on_full_slot_check() {
let test = get_gloas_should_build_on_full_test_definition();
test.run();
}
/// Test that execution payload invalidation propagates across the V17→V29 fork
/// boundary: after invalidating a V17 parent, head must not select any descendant.
///

View File

@@ -1569,10 +1569,13 @@ impl ProtoArray {
/// Called by the proposer to decide whether to build on the full or empty
/// parent pending node. Returns false if the PTC has voted the data as unavailable.
/// For a parent from an earlier slot the `Empty` or `Full` node has already been resolved
/// by attestation weight in `get_head`.
pub fn should_build_on_full<E: EthSpec>(
&self,
fc_node: &IndexedForkChoiceNode,
proto_node: &ProtoNode,
current_slot: Slot,
) -> Result<bool, Error> {
if fc_node.payload_status == PayloadStatus::Pending {
return Err(Error::InvalidPayloadStatus {
@@ -1584,10 +1587,23 @@ impl ProtoArray {
if fc_node.payload_status == PayloadStatus::Empty {
return Ok(false);
}
if proto_node.slot().saturating_add(1u64) != current_slot {
return Ok(true);
}
// Check that false votes have not achieved an absolute majority. This allows the payload to be
// considered available when either a majority have voted true or not enough votes have
// been cast either way.
Ok(!proto_node.payload_data_availability::<E>(false)?)
if proto_node.payload_data_availability::<E>(false)? {
return Ok(false);
}
if proto_node.payload_timeliness::<E>(false)? {
return Ok(false);
}
Ok(true)
}
pub fn should_extend_payload<E: EthSpec>(

View File

@@ -968,6 +968,7 @@ impl ProtoArrayForkChoice {
&self,
block_root: &Hash256,
parent_payload_status: PayloadStatus,
current_slot: Slot,
) -> Result<bool, String> {
let block_index = self
.proto_array
@@ -985,7 +986,7 @@ impl ProtoArrayForkChoice {
payload_status: parent_payload_status,
};
self.proto_array
.should_build_on_full::<E>(&fc_node, proto_node)
.should_build_on_full::<E>(&fc_node, proto_node, current_slot)
.map_err(|e| format!("{e:?}"))
}