resolve merge conflicts

This commit is contained in:
Eitan Seri-Levi
2026-04-30 01:51:26 +02:00
544 changed files with 48684 additions and 18351 deletions

View File

@@ -19,5 +19,6 @@ types = { workspace = true }
[dev-dependencies]
beacon_chain = { workspace = true }
bls = { workspace = true }
store = { workspace = true }
tokio = { workspace = true }

View File

@@ -3,10 +3,9 @@ use crate::{ForkChoiceStore, InvalidationOperation};
use fixed_bytes::FixedBytesExtended;
use logging::crit;
use proto_array::{
Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, JustifiedBalances,
ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold,
Block as ProtoBlock, DisallowedReOrgOffsets, ExecutionStatus, JustifiedBalances, LatestMessage,
PayloadStatus, ProposerHeadError, ProposerHeadInfo, ProtoArrayForkChoice, ReOrgThreshold,
};
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
use state_processing::{
per_block_processing::errors::AttesterSlashingValidationError, per_epoch_processing,
@@ -20,13 +19,14 @@ use tracing::{debug, instrument, warn};
use types::{
AbstractExecPayload, AttestationShufflingId, AttesterSlashingRef, BeaconBlockRef, BeaconState,
BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, ExecPayload, ExecutionBlockHash,
Hash256, IndexedAttestationRef, RelativeEpoch, SignedBeaconBlock, Slot,
consts::bellatrix::INTERVALS_PER_SLOT,
Hash256, IndexedAttestationRef, IndexedPayloadAttestation, RelativeEpoch, SignedBeaconBlock,
Slot,
};
#[derive(Debug)]
pub enum Error<T> {
InvalidAttestation(InvalidAttestation),
InvalidPayloadAttestation(InvalidPayloadAttestation),
InvalidAttesterSlashing(AttesterSlashingValidationError),
InvalidBlock(InvalidBlock),
ProtoArrayStringError(String),
@@ -77,6 +77,8 @@ pub enum Error<T> {
},
UnrealizedVoteProcessing(state_processing::EpochProcessingError),
ValidatorStatuses(BeaconStateError),
ChainSpecError(String),
DoesNotDescendFromFinalizedCheckpoint,
}
impl<T> From<InvalidAttestation> for Error<T> {
@@ -85,6 +87,12 @@ impl<T> From<InvalidAttestation> for Error<T> {
}
}
impl<T> From<InvalidPayloadAttestation> for Error<T> {
fn from(e: InvalidPayloadAttestation) -> Self {
Error::InvalidPayloadAttestation(e)
}
}
impl<T> From<AttesterSlashingValidationError> for Error<T> {
fn from(e: AttesterSlashingValidationError) -> Self {
Error::InvalidAttesterSlashing(e)
@@ -170,6 +178,33 @@ pub enum InvalidAttestation {
/// The attestation is attesting to a state that is later than itself. (Viz., attesting to the
/// future).
AttestsToFutureBlock { block: Slot, attestation: Slot },
/// Post-Gloas: attestation index must be 0 or 1.
InvalidAttestationIndex { index: u64 },
/// A same-slot attestation has a non-zero index, which is invalid post-Gloas.
InvalidSameSlotAttestationIndex { slot: Slot },
/// Post-Gloas: attestation with index == 1 (payload_present) requires the block's
/// payload to have been received (`root in store.payload_states`).
PayloadNotReceived { beacon_block_root: Hash256 },
}
#[derive(Debug, Clone, PartialEq)]
pub enum InvalidPayloadAttestation {
/// The payload attestation's attesting indices were empty.
EmptyAggregationBitfield,
/// The `payload_attestation.data.beacon_block_root` block is unknown.
UnknownHeadBlock { beacon_block_root: Hash256 },
/// The payload attestation is attesting to a block that is later than itself.
AttestsToFutureBlock { block: Slot, attestation: Slot },
/// A gossip payload attestation must be for the current slot.
PayloadAttestationNotCurrentSlot {
attestation_slot: Slot,
current_slot: Slot,
},
/// One or more payload attesters are not part of the PTC.
PayloadAttestationAttestersNotInPtc {
attesting_indices_len: usize,
attesting_indices_in_ptc: usize,
},
}
impl<T> From<String> for Error<T> {
@@ -241,6 +276,17 @@ pub struct QueuedAttestation {
attesting_indices: Vec<u64>,
block_root: Hash256,
target_epoch: Epoch,
/// Per Gloas spec: `payload_present = attestation.data.index == 1`.
payload_present: bool,
}
/// Legacy queued attestation without payload_present (pre-Gloas, schema V28).
#[derive(Clone, PartialEq, Encode, Decode)]
pub struct QueuedAttestationV28 {
slot: Slot,
attesting_indices: Vec<u64>,
block_root: Hash256,
target_epoch: Epoch,
}
impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
@@ -250,6 +296,7 @@ impl<'a, E: EthSpec> From<IndexedAttestationRef<'a, E>> for QueuedAttestation {
attesting_indices: a.attesting_indices_to_vec(),
block_root: a.data().beacon_block_root,
target_epoch: a.data().target.epoch,
payload_present: a.data().index == 1,
}
}
}
@@ -367,21 +414,32 @@ where
AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Next)
.map_err(Error::BeaconStateError)?;
let execution_status = anchor_block.message().execution_payload().map_or_else(
// If the block doesn't have an execution payload then it can't have
// execution enabled.
|_| ExecutionStatus::irrelevant(),
|execution_payload| {
let (execution_status, execution_payload_parent_hash, execution_payload_block_hash) =
if let Ok(signed_bid) = anchor_block.message().body().signed_execution_payload_bid() {
// Gloas: execution status is irrelevant post-Gloas; payload validation
// is decoupled from beacon blocks.
(
ExecutionStatus::irrelevant(),
Some(signed_bid.message.parent_block_hash),
Some(signed_bid.message.block_hash),
)
} else if let Ok(execution_payload) = anchor_block.message().execution_payload() {
// Pre-Gloas forks: do not set payload hashes, they are only used post-Gloas.
if execution_payload.is_default_with_empty_roots() {
// A default payload does not have execution enabled.
ExecutionStatus::irrelevant()
(ExecutionStatus::irrelevant(), None, None)
} else {
// Assume that this payload is valid, since the anchor should be a trusted block and
// state.
ExecutionStatus::Valid(execution_payload.block_hash())
// Assume that this payload is valid, since the anchor should be a
// trusted block and state.
(
ExecutionStatus::Valid(execution_payload.block_hash()),
None,
None,
)
}
},
);
} else {
// Pre-merge: no execution payload at all.
(ExecutionStatus::irrelevant(), None, None)
};
// If the current slot is not provided, use the value that was last provided to the store.
let current_slot = current_slot.unwrap_or_else(|| fc_store.get_current_slot());
@@ -396,6 +454,10 @@ where
next_epoch_shuffling_id,
fc_store.unsatisfied_inclusion_list_blocks().clone(),
execution_status,
execution_payload_parent_hash,
execution_payload_block_hash,
anchor_block.message().proposer_index(),
spec,
)?;
let mut fork_choice = Self {
@@ -481,7 +543,7 @@ where
&mut self,
system_time_current_slot: Slot,
spec: &ChainSpec,
) -> Result<Hash256, Error<T::Error>> {
) -> Result<(Hash256, PayloadStatus), Error<T::Error>> {
// Provide the slot (as per the system clock) to the `fc_store` and then return its view of
// the current slot. The `fc_store` will ensure that the `current_slot` is never
// decreasing, a property which we must maintain.
@@ -489,7 +551,7 @@ where
let store = &mut self.fc_store;
let head_root = self.proto_array.find_head::<E>(
let (head_root, head_payload_status) = self.proto_array.find_head::<E>(
*store.justified_checkpoint(),
*store.finalized_checkpoint(),
store.justified_balances(),
@@ -500,9 +562,22 @@ where
)?;
// Cache some values for the next forkchoiceUpdate call to the execution layer.
let head_hash = self
.get_block(&head_root)
.and_then(|b| b.execution_status.block_hash());
// For Gloas blocks, `execution_status` is Irrelevant (no embedded payload).
// If the payload envelope was received (Full), use the bid's block_hash as the
// execution chain head. Otherwise fall back to the parent hash (Pending) or None.
// TODO(gloas): this is a bit messy, and we probably need a similar treatment for
// justified/finalized
// Can fix as part of: https://github.com/sigp/lighthouse/issues/8957
let head_hash = self.get_block(&head_root).and_then(|b| {
b.execution_status
.block_hash()
.or(match head_payload_status {
PayloadStatus::Full => b.execution_payload_block_hash,
PayloadStatus::Pending | PayloadStatus::Empty => {
b.execution_payload_parent_hash
}
})
});
let justified_root = self.justified_checkpoint().root;
let finalized_root = self.finalized_checkpoint().root;
let justified_hash = self
@@ -518,7 +593,7 @@ where
finalized_hash,
};
Ok(head_root)
Ok((head_root, head_payload_status))
}
/// Get the block to build on as proposer, taking into account proposer re-orgs.
@@ -613,6 +688,20 @@ where
}
}
/// Mark a Gloas payload envelope as valid and received.
///
/// This must only be called for valid Gloas payloads.
pub fn on_valid_payload_envelope_received(
&mut self,
block_root: Hash256,
) -> Result<(), Error<T::Error>> {
self.proto_array
.on_valid_payload_envelope_received(block_root)
.map_err(Error::FailedToProcessValidExecutionPayload)
}
/// Pre-Gloas only.
///
/// See `ProtoArrayForkChoice::process_execution_payload_validation` for documentation.
pub fn on_valid_execution_payload(
&mut self,
@@ -623,6 +712,8 @@ where
.map_err(Error::FailedToProcessValidExecutionPayload)
}
/// Pre-Gloas only.
///
/// See `ProtoArrayForkChoice::process_execution_payload_invalidation` for documentation.
pub fn on_invalid_execution_payload(
&mut self,
@@ -672,6 +763,7 @@ where
block_delay: Duration,
state: &BeaconState<E>,
payload_verification_status: PayloadVerificationStatus,
canonical_head_proposer_index: u64,
spec: &ChainSpec,
) -> Result<(), Error<T::Error>> {
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_BLOCK_TIMES);
@@ -734,12 +826,20 @@ where
}));
}
// Add proposer score boost if the block is timely.
let is_before_attesting_interval =
block_delay < Duration::from_secs(spec.seconds_per_slot / INTERVALS_PER_SLOT);
let attestation_threshold = spec.get_attestation_due::<E>(block.slot());
// Add proposer score boost if the block is the first timely block for this slot and its
// proposer matches the expected proposer on the canonical chain (per spec
// `update_proposer_boost_root`, introduced in v1.7.0-alpha.5).
let is_before_attesting_interval = block_delay < attestation_threshold;
let is_first_block = self.fc_store.proposer_boost_root().is_zero();
if current_slot == block.slot() && is_before_attesting_interval && is_first_block {
let is_canonical_proposer = block.proposer_index() == canonical_head_proposer_index;
if current_slot == block.slot()
&& is_before_attesting_interval
&& is_first_block
&& is_canonical_proposer
{
self.fc_store.set_proposer_boost_root(block_root);
}
@@ -888,6 +988,16 @@ where
ExecutionStatus::irrelevant()
};
let (execution_payload_parent_hash, execution_payload_block_hash) =
if let Ok(signed_bid) = block.body().signed_execution_payload_bid() {
(
Some(signed_bid.message.parent_block_hash),
Some(signed_bid.message.block_hash),
)
} else {
(None, None)
};
// This does not apply a vote to the block, it just makes fork choice aware of the block so
// it can still be identified as the head even if it doesn't have any votes.
self.proto_array.process_block::<E>(
@@ -914,10 +1024,13 @@ where
execution_status,
unrealized_justified_checkpoint: Some(unrealized_justified_checkpoint),
unrealized_finalized_checkpoint: Some(unrealized_finalized_checkpoint),
execution_payload_parent_hash,
execution_payload_block_hash,
proposer_index: Some(block.proposer_index()),
},
current_slot,
self.justified_checkpoint(),
self.finalized_checkpoint(),
spec,
block_delay,
)?;
Ok(())
@@ -986,6 +1099,7 @@ where
&self,
indexed_attestation: IndexedAttestationRef<E>,
is_from_block: AttestationFromBlock,
spec: &ChainSpec,
) -> Result<(), InvalidAttestation> {
// There is no point in processing an attestation with an empty bitfield. Reject
// it immediately.
@@ -1058,6 +1172,89 @@ where
});
}
if spec
.fork_name_at_slot::<E>(indexed_attestation.data().slot)
.gloas_enabled()
{
let index = indexed_attestation.data().index;
// Post-Gloas: attestation index must be 0 or 1.
if index > 1 {
return Err(InvalidAttestation::InvalidAttestationIndex { index });
}
// Same-slot attestations must have index == 0.
if indexed_attestation.data().slot == block.slot && index != 0 {
return Err(InvalidAttestation::InvalidSameSlotAttestationIndex {
slot: block.slot,
});
}
// index == 1 (payload_present) requires the block's payload to have been received.
// TODO(gloas): could optimise by adding `payload_received` to `Block`
if index == 1
&& !self
.proto_array
.is_payload_received(&indexed_attestation.data().beacon_block_root)
{
return Err(InvalidAttestation::PayloadNotReceived {
beacon_block_root: indexed_attestation.data().beacon_block_root,
});
}
}
Ok(())
}
/// Validates a payload attestation for application to fork choice.
fn validate_on_payload_attestation(
&self,
indexed_payload_attestation: &IndexedPayloadAttestation<E>,
is_from_block: AttestationFromBlock,
) -> Result<(), InvalidPayloadAttestation> {
// This check is from `is_valid_indexed_payload_attestation`, but we do it immediately to
// avoid wasting time on junk attestations.
if indexed_payload_attestation.attesting_indices.is_empty() {
return Err(InvalidPayloadAttestation::EmptyAggregationBitfield);
}
// PTC attestation must be for a known block. If block is unknown, delay consideration until
// the block is found (responsibility of caller).
let block = self
.proto_array
.get_block(&indexed_payload_attestation.data.beacon_block_root)
.ok_or(InvalidPayloadAttestation::UnknownHeadBlock {
beacon_block_root: indexed_payload_attestation.data.beacon_block_root,
})?;
// Not strictly part of the spec, but payload attestations to future slots are MORE INVALID
// than payload attestations to blocks at previous slots.
if block.slot > indexed_payload_attestation.data.slot {
return Err(InvalidPayloadAttestation::AttestsToFutureBlock {
block: block.slot,
attestation: indexed_payload_attestation.data.slot,
});
}
// PTC votes can only change the vote for their assigned beacon block, return early otherwise
if block.slot != indexed_payload_attestation.data.slot {
return Ok(());
}
// Gossip payload attestations must be for the current slot.
// NOTE: signature is assumed to have been verified by caller.
// https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md
if matches!(is_from_block, AttestationFromBlock::False)
&& indexed_payload_attestation.data.slot != self.fc_store.get_current_slot()
{
return Err(
InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot {
attestation_slot: indexed_payload_attestation.data.slot,
current_slot: self.fc_store.get_current_slot(),
},
);
}
Ok(())
}
@@ -1083,6 +1280,7 @@ where
system_time_current_slot: Slot,
attestation: IndexedAttestationRef<E>,
is_from_block: AttestationFromBlock,
spec: &ChainSpec,
) -> Result<(), Error<T::Error>> {
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_ON_ATTESTATION_TIMES);
@@ -1105,14 +1303,21 @@ where
return Ok(());
}
self.validate_on_attestation(attestation, is_from_block)?;
self.validate_on_attestation(attestation, is_from_block, spec)?;
// Per Gloas spec: `payload_present = attestation.data.index == 1`.
let payload_present = spec
.fork_name_at_slot::<E>(attestation.data().slot)
.gloas_enabled()
&& attestation.data().index == 1;
if attestation.data().slot < self.fc_store.get_current_slot() {
for validator_index in attestation.attesting_indices_iter() {
self.proto_array.process_attestation(
*validator_index as usize,
attestation.data().beacon_block_root,
attestation.data().target.epoch,
attestation.data().slot,
payload_present,
)?;
}
} else {
@@ -1129,6 +1334,59 @@ where
Ok(())
}
/// Register a payload attestation with the fork choice DAG.
///
/// `ptc` is the PTC committee for the attestation's slot: a list of validator indices
/// ordered by committee position. Each attesting validator index is resolved to its
/// position within `ptc` (its `ptc_index`) before being applied to the proto-array.
pub fn on_payload_attestation(
&mut self,
system_time_current_slot: Slot,
attestation: &IndexedPayloadAttestation<E>,
is_from_block: AttestationFromBlock,
ptc: &[usize],
) -> Result<(), Error<T::Error>> {
self.update_time(system_time_current_slot)?;
if attestation.data.beacon_block_root.is_zero() {
return Ok(());
}
// TODO(gloas): Should ignore wrong-slot payload attestations at the caller, they could
// have been processed at the correct slot when received on gossip, but then have the
// wrong-slot by the time they make it to here (TOCTOU).
self.validate_on_payload_attestation(attestation, is_from_block)?;
// Resolve validator indices to PTC committee positions.
let ptc_indices: Vec<usize> = attestation
.attesting_indices
.iter()
.filter_map(|validator_index| ptc.iter().position(|&p| p == *validator_index as usize))
.collect();
// Check that all the attesters are in the PTC
if ptc_indices.len() != attestation.attesting_indices.len() {
return Err(
InvalidPayloadAttestation::PayloadAttestationAttestersNotInPtc {
attesting_indices_len: attestation.attesting_indices.len(),
attesting_indices_in_ptc: ptc_indices.len(),
}
.into(),
);
}
for &ptc_index in &ptc_indices {
self.proto_array.process_payload_attestation(
attestation.data.beacon_block_root,
ptc_index,
attestation.data.payload_present,
attestation.data.blob_data_available,
)?;
}
Ok(())
}
/// Apply an attester slashing to fork choice.
///
/// We assume that the attester slashing provided to this function has already been verified.
@@ -1235,7 +1493,8 @@ where
self.proto_array.process_attestation(
*validator_index as usize,
attestation.block_root,
attestation.target_epoch,
attestation.slot,
attestation.payload_present,
)?;
}
}
@@ -1258,6 +1517,14 @@ where
}
}
/// Returns whether the proposer should extend the execution payload chain of the given block.
pub fn should_extend_payload(&self, block_root: &Hash256) -> Result<bool, Error<T::Error>> {
let proposer_boost_root = self.fc_store.proposer_boost_root();
self.proto_array
.should_extend_payload::<E>(block_root, proposer_boost_root)
.map_err(Error::ProtoArrayStringError)
}
/// Returns an `ExecutionStatus` if the block is known **and** a descendant of the finalized root.
pub fn get_block_execution_status(&self, block_root: &Hash256) -> Option<ExecutionStatus> {
if self.is_finalized_checkpoint_or_descendant(*block_root) {
@@ -1267,6 +1534,29 @@ where
}
}
/// Returns the canonical payload status of a block. See
/// `ProtoArrayForkChoice::get_canonical_payload_status`.
pub fn get_canonical_payload_status(
&self,
block_root: &Hash256,
spec: &ChainSpec,
) -> Result<PayloadStatus, Error<T::Error>> {
if self.is_finalized_checkpoint_or_descendant(*block_root) {
let current_slot = self.fc_store.get_current_slot();
let proposer_boost_root = self.fc_store.proposer_boost_root();
self.proto_array
.get_canonical_payload_status::<E>(
block_root,
current_slot,
proposer_boost_root,
spec,
)
.map_err(Error::ProtoArrayError)
} else {
Err(Error::DoesNotDescendFromFinalizedCheckpoint)
}
}
/// Returns the weight for the given block root.
pub fn get_block_weight(&self, block_root: &Hash256) -> Option<u64> {
self.proto_array.get_weight(block_root)
@@ -1365,13 +1655,15 @@ where
/// Returns the latest message for a given validator, if any.
///
/// Returns `(block_root, block_slot)`.
/// Returns `block_root, block_slot, payload_present`.
///
/// ## Notes
///
/// It may be prudent to call `Self::update_time` before calling this function,
/// since some attestations might be queued and awaiting processing.
pub fn latest_message(&self, validator_index: usize) -> Option<(Hash256, Epoch)> {
///
/// This function is only used in tests.
pub fn latest_message(&self, validator_index: usize) -> Option<LatestMessage> {
self.proto_array.latest_message(validator_index)
}
@@ -1416,7 +1708,6 @@ where
persisted_proto_array: proto_array::core::SszContainer,
justified_balances: JustifiedBalances,
reset_payload_statuses: ResetPayloadStatuses,
spec: &ChainSpec,
) -> Result<ProtoArrayForkChoice, Error<T::Error>> {
let mut proto_array = ProtoArrayForkChoice::from_container(
persisted_proto_array.clone(),
@@ -1441,7 +1732,7 @@ where
// Reset all blocks back to being "optimistic". This helps recover from an EL consensus
// fault where an invalid payload becomes valid.
if let Err(e) = proto_array.set_all_blocks_to_optimistic::<E>(spec) {
if let Err(e) = proto_array.set_all_blocks_to_optimistic::<E>() {
// If there is an error resetting the optimistic status then log loudly and revert
// back to a proto-array which does not have the reset applied. This indicates a
// significant error in Lighthouse and warrants detailed investigation.
@@ -1471,7 +1762,6 @@ where
persisted.proto_array,
justified_balances,
reset_payload_statuses,
spec,
)?;
let current_slot = fc_store.get_current_slot();
@@ -1479,7 +1769,7 @@ where
let mut fork_choice = Self {
fc_store,
proto_array,
queued_attestations: persisted.queued_attestations,
queued_attestations: vec![],
// Will be updated in the following call to `Self::get_head`.
forkchoice_update_parameters: ForkchoiceUpdateParameters {
head_hash: None,
@@ -1505,7 +1795,7 @@ where
// get a different result.
fork_choice
.proto_array
.set_all_blocks_to_optimistic::<E>(spec)?;
.set_all_blocks_to_optimistic::<E>()?;
// If the second attempt at finding a head fails, return an error since we do not
// expect this scenario.
fork_choice.get_head(current_slot, spec)?;
@@ -1518,10 +1808,7 @@ where
/// be instantiated again later.
pub fn to_persisted(&self) -> PersistedForkChoice {
PersistedForkChoice {
proto_array: self
.proto_array()
.as_ssz_container(self.justified_checkpoint(), self.finalized_checkpoint()),
queued_attestations: self.queued_attestations().to_vec(),
proto_array: self.proto_array().as_ssz_container(),
}
}
@@ -1535,43 +1822,34 @@ where
///
/// This is used when persisting the state of the fork choice to disk.
#[superstruct(
variants(V17, V28),
variants(V28, V29),
variant_attributes(derive(Encode, Decode, Clone)),
no_enum
)]
pub struct PersistedForkChoice {
#[superstruct(only(V17))]
pub proto_array_bytes: Vec<u8>,
#[superstruct(only(V28))]
pub proto_array: proto_array::core::SszContainerV28,
pub queued_attestations: Vec<QueuedAttestation>,
pub proto_array_v28: proto_array::core::SszContainerV28,
#[superstruct(only(V29))]
pub proto_array: proto_array::core::SszContainerV29,
#[superstruct(only(V28))]
pub queued_attestations_v28: Vec<QueuedAttestationV28>,
}
pub type PersistedForkChoice = PersistedForkChoiceV28;
pub type PersistedForkChoice = PersistedForkChoiceV29;
impl TryFrom<PersistedForkChoiceV17> for PersistedForkChoiceV28 {
type Error = ssz::DecodeError;
fn try_from(v17: PersistedForkChoiceV17) -> Result<Self, Self::Error> {
let container_v17 =
proto_array::core::SszContainerV17::from_ssz_bytes(&v17.proto_array_bytes)?;
let container_v28 = container_v17.into();
Ok(Self {
proto_array: container_v28,
queued_attestations: v17.queued_attestations,
})
impl From<PersistedForkChoiceV28> for PersistedForkChoiceV29 {
fn from(v28: PersistedForkChoiceV28) -> Self {
Self {
proto_array: v28.proto_array_v28.into(),
}
}
}
impl From<(PersistedForkChoiceV28, JustifiedBalances)> for PersistedForkChoiceV17 {
fn from((v28, balances): (PersistedForkChoiceV28, JustifiedBalances)) -> Self {
let container_v17 = proto_array::core::SszContainerV17::from((v28.proto_array, balances));
let proto_array_bytes = container_v17.as_ssz_bytes();
impl From<PersistedForkChoiceV29> for PersistedForkChoiceV28 {
fn from(v29: PersistedForkChoiceV29) -> Self {
Self {
proto_array_bytes,
queued_attestations: v28.queued_attestations,
proto_array_v28: v29.proto_array.into(),
queued_attestations_v28: vec![],
}
}
}
@@ -1611,6 +1889,7 @@ mod tests {
attesting_indices: vec![],
block_root: Hash256::zero(),
target_epoch: Epoch::new(0),
payload_present: false,
})
.collect()
}

View File

@@ -4,10 +4,11 @@ mod metrics;
pub use crate::fork_choice::{
AttestationFromBlock, Error, ForkChoice, ForkChoiceView, ForkchoiceUpdateParameters,
InvalidAttestation, InvalidBlock, PayloadVerificationStatus, PersistedForkChoice,
PersistedForkChoiceV17, PersistedForkChoiceV28, QueuedAttestation, ResetPayloadStatuses,
InvalidAttestation, InvalidBlock, InvalidPayloadAttestation, PayloadVerificationStatus,
PersistedForkChoice, PersistedForkChoiceV28, PersistedForkChoiceV29, QueuedAttestation,
ResetPayloadStatuses,
};
pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::{
Block as ProtoBlock, ExecutionStatus, InvalidationOperation, ProposerHeadError,
Block as ProtoBlock, ExecutionStatus, InvalidationOperation, PayloadStatus, ProposerHeadError,
};

View File

@@ -7,9 +7,11 @@ use beacon_chain::{
BeaconChain, BeaconChainError, BeaconForkChoiceStore, ChainConfig, ForkChoiceError,
StateSkipConfig, WhenSlotSkipped,
};
use bls::AggregateSignature;
use fixed_bytes::FixedBytesExtended;
use fork_choice::{
ForkChoiceStore, InvalidAttestation, InvalidBlock, PayloadVerificationStatus, QueuedAttestation,
AttestationFromBlock, ForkChoiceStore, InvalidAttestation, InvalidBlock,
InvalidPayloadAttestation, PayloadVerificationStatus, QueuedAttestation,
};
use state_processing::state_advance::complete_state_advance;
use std::fmt;
@@ -19,8 +21,8 @@ use store::MemoryStore;
use types::SingleAttestation;
use types::{
BeaconBlockRef, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, ForkName, Hash256,
IndexedAttestation, MainnetEthSpec, RelativeEpoch, SignedBeaconBlock, Slot, SubnetId,
test_utils::generate_deterministic_keypair,
IndexedAttestation, IndexedPayloadAttestation, MainnetEthSpec, PayloadAttestationData,
RelativeEpoch, SignedBeaconBlock, Slot, SubnetId, test_utils::generate_deterministic_keypair,
};
pub type E = MainnetEthSpec;
@@ -71,6 +73,9 @@ impl ForkChoiceTest {
Self { harness }
}
/// Creates a new tester with the Gloas fork active at epoch 1.
/// Genesis is a standard Fulu block (epoch 0), so block production works normally.
/// Tests that need Gloas semantics should advance the chain into epoch 1 first.
/// Get a value from the `ForkChoice` instantiation.
fn get<T, U>(&self, func: T) -> U
where
@@ -311,6 +316,7 @@ impl ForkChoiceTest {
Duration::from_secs(0),
&state,
PayloadVerificationStatus::Verified,
block.message().proposer_index(),
&self.harness.chain.spec,
)
.unwrap();
@@ -354,6 +360,7 @@ impl ForkChoiceTest {
Duration::from_secs(0),
&state,
PayloadVerificationStatus::Verified,
block.message().proposer_index(),
&self.harness.chain.spec,
)
.expect_err("on_block did not return an error");
@@ -923,6 +930,56 @@ async fn invalid_attestation_future_block() {
.await;
}
/// Gossip payload attestations must be for the current slot. A payload attestation for slot S
/// received at slot S+1 should be rejected per the spec.
#[tokio::test]
async fn non_block_payload_attestation_for_previous_slot_is_rejected() {
let test = ForkChoiceTest::new()
.apply_blocks_without_new_attestations(1)
.await;
let chain = &test.harness.chain;
let block_a = chain
.block_at_slot(Slot::new(1), WhenSlotSkipped::Prev)
.expect("lookup should succeed")
.expect("block A should exist");
let block_a_root = block_a.canonical_root();
let s_plus_1 = block_a.slot().saturating_add(1_u64);
let payload_attestation = IndexedPayloadAttestation::<E> {
attesting_indices: vec![0_u64].try_into().expect("valid attesting indices"),
data: PayloadAttestationData {
beacon_block_root: block_a_root,
slot: Slot::new(1),
payload_present: true,
blob_data_available: true,
},
signature: AggregateSignature::empty(),
};
let ptc = &[0_usize];
let result = chain
.canonical_head
.fork_choice_write_lock()
.on_payload_attestation(
s_plus_1,
&payload_attestation,
AttestationFromBlock::False,
ptc,
);
assert!(
matches!(
result,
Err(ForkChoiceError::InvalidPayloadAttestation(
InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { .. }
))
),
"gossip payload attestation for previous slot should be rejected, got: {:?}",
result
);
}
/// Specification v0.12.1:
///
/// assert target.root == get_ancestor(store, attestation.data.beacon_block_root, target_slot)