Resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-06-17 16:55:37 +03:00
108 changed files with 2768 additions and 1018 deletions

View File

@@ -68,6 +68,7 @@ fn build_chain(num_blocks: u64, gloas: bool) -> (ProtoArrayForkChoice, types::Ch
},
execution_payload_block_hash: if is_gloas { Some(get_hash(i)) } else { None },
proposer_index: Some(0),
payload_received: false,
};
fork_choice

View File

@@ -50,7 +50,6 @@ pub enum Error {
block_root: Hash256,
parent_root: Hash256,
},
InvalidEpochOffset(u64),
Arith(ArithError),
InvalidNodeVariant {
block_root: Hash256,

View File

@@ -132,6 +132,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)]
@@ -329,6 +338,7 @@ impl ForkChoiceTestDefinition {
execution_payload_parent_hash,
execution_payload_block_hash,
proposer_index: Some(0),
payload_received: false,
};
fork_choice
.process_block::<MainnetEthSpec>(block, slot, &spec, Duration::ZERO)
@@ -629,6 +639,30 @@ impl ForkChoiceTestDefinition {
assert_eq!(
actual, expected,
"latest_parent_full_block mismatch at op index {}",
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

@@ -8,8 +8,8 @@ mod ssz_container;
pub use crate::justified_balances::JustifiedBalances;
pub use crate::proto_array::{InvalidationOperation, calculate_committee_fraction};
pub use crate::proto_array_fork_choice::{
Block, DisallowedReOrgOffsets, DoNotReOrg, ExecutionStatus, LatestMessage, PayloadStatus,
ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold,
Block, DoNotReOrg, ExecutionStatus, LatestMessage, PayloadStatus, ProposerHeadError,
ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold,
};
pub use error::Error;

View File

@@ -1596,10 +1596,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 {
@@ -1611,10 +1614,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

@@ -242,6 +242,8 @@ pub struct Block {
pub execution_payload_parent_hash: Option<ExecutionBlockHash>,
pub execution_payload_block_hash: Option<ExecutionBlockHash>,
pub proposer_index: Option<u64>,
/// Whether the block's execution payload envelope has been received. Always `false` pre-Gloas.
pub payload_received: bool,
}
impl Block {
@@ -385,10 +387,6 @@ pub enum DoNotReOrg {
MissingHeadFinalizedCheckpoint,
ParentDistance,
HeadDistance,
ShufflingUnstable,
DisallowedOffset {
offset: u64,
},
JustificationAndFinalizationNotCompetitive,
ChainNotFinalizing {
epochs_since_finalization: u64,
@@ -413,10 +411,6 @@ impl std::fmt::Display for DoNotReOrg {
Self::MissingHeadFinalizedCheckpoint => write!(f, "finalized checkpoint missing"),
Self::ParentDistance => write!(f, "parent too far from head"),
Self::HeadDistance => write!(f, "head too far from current slot"),
Self::ShufflingUnstable => write!(f, "shuffling unstable at epoch boundary"),
Self::DisallowedOffset { offset } => {
write!(f, "re-orgs disabled at offset {offset}")
}
Self::JustificationAndFinalizationNotCompetitive => {
write!(f, "justification or finalization not competitive")
}
@@ -462,31 +456,6 @@ impl std::fmt::Display for DoNotReOrg {
#[serde(transparent)]
pub struct ReOrgThreshold(pub u64);
/// New-type for disallowed re-org slots.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DisallowedReOrgOffsets {
// Vecs are faster than hashmaps for small numbers of items.
offsets: Vec<u64>,
}
impl Default for DisallowedReOrgOffsets {
fn default() -> Self {
DisallowedReOrgOffsets { offsets: vec![0] }
}
}
impl DisallowedReOrgOffsets {
pub fn new<E: EthSpec>(offsets: Vec<u64>) -> Result<Self, Error> {
for &offset in &offsets {
if offset >= E::slots_per_epoch() {
return Err(Error::InvalidEpochOffset(offset));
}
}
Ok(Self { offsets })
}
}
#[derive(PartialEq)]
pub struct ProtoArrayForkChoice {
pub(crate) proto_array: ProtoArray,
@@ -535,6 +504,7 @@ impl ProtoArrayForkChoice {
execution_payload_parent_hash,
execution_payload_block_hash,
proposer_index: Some(proposer_index),
payload_received: false,
};
proto_array
@@ -724,7 +694,6 @@ impl ProtoArrayForkChoice {
justified_balances: &JustifiedBalances,
re_org_head_threshold: ReOrgThreshold,
re_org_parent_threshold: ReOrgThreshold,
disallowed_offsets: &DisallowedReOrgOffsets,
max_epochs_since_finalization: Epoch,
) -> Result<ProposerHeadInfo, ProposerHeadError<Error>> {
let info = self.get_proposer_head_info::<E>(
@@ -733,7 +702,6 @@ impl ProtoArrayForkChoice {
justified_balances,
re_org_head_threshold,
re_org_parent_threshold,
disallowed_offsets,
max_epochs_since_finalization,
)?;
@@ -784,7 +752,6 @@ impl ProtoArrayForkChoice {
justified_balances: &JustifiedBalances,
re_org_head_threshold: ReOrgThreshold,
re_org_parent_threshold: ReOrgThreshold,
disallowed_offsets: &DisallowedReOrgOffsets,
max_epochs_since_finalization: Epoch,
) -> Result<ProposerHeadInfo, ProposerHeadError<Error>> {
let mut nodes = self
@@ -823,18 +790,6 @@ impl ProtoArrayForkChoice {
return Err(DoNotReOrg::ParentDistance.into());
}
// Check shuffling stability.
let shuffling_stable = re_org_block_slot % E::slots_per_epoch() != 0;
if !shuffling_stable {
return Err(DoNotReOrg::ShufflingUnstable.into());
}
// Check allowed slot offsets.
let offset = (re_org_block_slot % E::slots_per_epoch()).as_u64();
if disallowed_offsets.offsets.contains(&offset) {
return Err(DoNotReOrg::DisallowedOffset { offset }.into());
}
// Check FFG.
let ffg_competitive = parent_node.unrealized_justified_checkpoint()
== head_node.unrealized_justified_checkpoint()
@@ -1007,6 +962,7 @@ impl ProtoArrayForkChoice {
execution_payload_parent_hash: block.execution_payload_parent_hash().ok(),
execution_payload_block_hash: block.execution_payload_block_hash().ok(),
proposer_index: block.proposer_index().ok(),
payload_received: block.payload_received().unwrap_or(false),
})
}
@@ -1016,6 +972,7 @@ impl ProtoArrayForkChoice {
&self,
block_root: &Hash256,
parent_payload_status: PayloadStatus,
current_slot: Slot,
) -> Result<bool, String> {
let block_index = self
.proto_array
@@ -1033,7 +990,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:?}"))
}
@@ -1445,6 +1402,7 @@ mod test_compute_deltas {
execution_payload_parent_hash: None,
execution_payload_block_hash: None,
proposer_index: Some(0),
payload_received: false,
},
genesis_slot + 1,
&spec,
@@ -1473,6 +1431,7 @@ mod test_compute_deltas {
execution_payload_parent_hash: None,
execution_payload_block_hash: None,
proposer_index: Some(0),
payload_received: false,
},
genesis_slot + 1,
&spec,
@@ -1609,6 +1568,7 @@ mod test_compute_deltas {
execution_payload_parent_hash: None,
execution_payload_block_hash: None,
proposer_index: Some(0),
payload_received: false,
},
Slot::from(block.slot),
&spec,