Merge remote-tracking branch 'origin/unstable' into tree-states

This commit is contained in:
Michael Sproul
2022-01-25 15:54:02 +11:00
350 changed files with 16324 additions and 4500 deletions

View File

@@ -1,14 +1,14 @@
use std::marker::PhantomData;
use proto_array::{Block as ProtoBlock, ProtoArrayForkChoice};
use ssz_derive::{Decode, Encode};
use types::{
AttestationShufflingId, BeaconBlock, BeaconState, BeaconStateError, ChainSpec, Checkpoint,
Epoch, EthSpec, Hash256, IndexedAttestation, RelativeEpoch, SignedBeaconBlock, Slot,
};
use crate::ForkChoiceStore;
use proto_array::{Block as ProtoBlock, ExecutionStatus, ProtoArrayForkChoice};
use ssz_derive::{Decode, Encode};
use std::cmp::Ordering;
use std::marker::PhantomData;
use std::time::Duration;
use types::{
consts::merge::INTERVALS_PER_SLOT, AttestationShufflingId, BeaconBlock, BeaconState,
BeaconStateError, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, IndexedAttestation,
RelativeEpoch, SignedBeaconBlock, Slot,
};
#[derive(Debug)]
pub enum Error<T> {
@@ -16,6 +16,7 @@ pub enum Error<T> {
InvalidBlock(InvalidBlock),
ProtoArrayError(String),
InvalidProtoArrayBytes(String),
InvalidLegacyProtoArrayBytes(String),
MissingProtoArrayBlock(Hash256),
UnknownAncestor {
ancestor_slot: Slot,
@@ -37,6 +38,11 @@ pub enum Error<T> {
block_slot: Slot,
state_slot: Slot,
},
InvalidPayloadStatus {
block_slot: Slot,
block_root: Hash256,
payload_verification_status: PayloadVerificationStatus,
},
}
impl<T> From<InvalidAttestation> for Error<T> {
@@ -100,6 +106,19 @@ impl<T> From<String> for Error<T> {
}
}
/// Indicates if a block has been verified by an execution payload.
///
/// There is no variant for "invalid", since such a block should never be added to fork choice.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PayloadVerificationStatus {
/// An EL has declared the execution payload to be valid.
Verified,
/// An EL has not yet made a determination about the execution payload.
NotVerified,
/// The block is either pre-merge-fork, or prior to the terminal PoW block.
Irrelevant,
}
/// Calculate how far `slot` lies from the start of its epoch.
///
/// ## Specification
@@ -149,6 +168,13 @@ where
store.set_current_slot(time);
let current_slot = store.get_current_slot();
// Reset proposer boost if this is a new slot.
if current_slot > previous_slot {
store.set_proposer_boost_root(Hash256::zero());
}
// Not a new epoch, return.
if !(current_slot > previous_slot && compute_slots_since_epoch_start::<E>(current_slot) == 0) {
return Ok(());
}
@@ -199,6 +225,15 @@ fn dequeue_attestations(
std::mem::replace(queued_attestations, remaining)
}
/// Denotes whether an attestation we are processing was received from a block or from gossip.
/// Equivalent to the `is_from_block` `bool` in:
///
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#validate_on_attestation
pub enum AttestationFromBlock {
True,
False,
}
/// Provides an implementation of "Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice":
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#ethereum-20-phase-0----beacon-chain-fork-choice
@@ -260,14 +295,24 @@ where
AttestationShufflingId::new(anchor_block_root, anchor_state, RelativeEpoch::Next)
.map_err(Error::BeaconStateError)?;
// Default any non-merge execution block hashes to 0x000..000.
let execution_status = anchor_block.message_merge().map_or_else(
|()| ExecutionStatus::irrelevant(),
|message| {
// Assume that this payload is valid, since the anchor should be a trusted block and
// state.
ExecutionStatus::Valid(message.body.execution_payload.block_hash)
},
);
let proto_array = ProtoArrayForkChoice::new(
finalized_block_slot,
finalized_block_state_root,
fc_store.justified_checkpoint().epoch,
fc_store.finalized_checkpoint().epoch,
fc_store.finalized_checkpoint().root,
*fc_store.justified_checkpoint(),
*fc_store.finalized_checkpoint(),
current_epoch_shuffling_id,
next_epoch_shuffling_id,
execution_status,
)?;
Ok(Self {
@@ -347,7 +392,11 @@ where
/// Is equivalent to:
///
/// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#get_head
pub fn get_head(&mut self, current_slot: Slot) -> Result<Hash256, Error<T::Error>> {
pub fn get_head(
&mut self,
current_slot: Slot,
spec: &ChainSpec,
) -> Result<Hash256, Error<T::Error>> {
self.update_time(current_slot)?;
let store = &mut self.fc_store;
@@ -356,11 +405,12 @@ where
let justified_balances = store.justified_balances().to_vec();
self.proto_array
.find_head(
store.justified_checkpoint().epoch,
store.justified_checkpoint().root,
store.finalized_checkpoint().epoch,
&justified_balances,
.find_head::<E>(
*store.justified_checkpoint(),
*store.finalized_checkpoint(),
store.justified_balances(),
store.proposer_boost_root(),
spec,
)
.map_err(Into::into)
}
@@ -435,12 +485,15 @@ where
///
/// The supplied block **must** pass the `state_transition` function as it will not be run
/// here.
#[allow(clippy::too_many_arguments)]
pub fn on_block(
&mut self,
current_slot: Slot,
block: &BeaconBlock<E>,
block_root: Hash256,
block_delay: Duration,
state: &BeaconState<E>,
payload_verification_status: PayloadVerificationStatus,
spec: &ChainSpec,
) -> Result<(), Error<T::Error>> {
let current_slot = self.update_time(current_slot)?;
@@ -492,6 +545,13 @@ 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);
if current_slot == block.slot() && is_before_attesting_interval {
self.fc_store.set_proposer_boost_root(block_root);
}
// Update justified checkpoint.
if state.current_justified_checkpoint().epoch > self.fc_store.justified_checkpoint().epoch {
if state.current_justified_checkpoint().epoch
@@ -511,25 +571,9 @@ where
if state.finalized_checkpoint().epoch > self.fc_store.finalized_checkpoint().epoch {
self.fc_store
.set_finalized_checkpoint(state.finalized_checkpoint());
let finalized_slot =
compute_start_slot_at_epoch::<E>(self.fc_store.finalized_checkpoint().epoch);
// Note: the `if` statement here is not part of the specification, but I claim that it
// is an optimization and equivalent to the specification. See this PR for more
// information:
//
// https://github.com/ethereum/eth2.0-specs/pull/1880
if *self.fc_store.justified_checkpoint() != state.current_justified_checkpoint()
&& (state.current_justified_checkpoint().epoch
> self.fc_store.justified_checkpoint().epoch
|| self
.get_ancestor(self.fc_store.justified_checkpoint().root, finalized_slot)?
!= Some(self.fc_store.finalized_checkpoint().root))
{
self.fc_store
.set_justified_checkpoint(state.current_justified_checkpoint())
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
}
self.fc_store
.set_justified_checkpoint(state.current_justified_checkpoint())
.map_err(Error::UnableToSetJustifiedCheckpoint)?;
}
let target_slot = block
@@ -548,6 +592,33 @@ where
.on_verified_block(block, block_root, state)
.map_err(Error::AfterBlockFailed)?;
let execution_status = if let Ok(execution_payload) = block.body().execution_payload() {
let block_hash = execution_payload.block_hash;
if block_hash == Hash256::zero() {
// The block is post-merge-fork, but pre-terminal-PoW block. We don't need to verify
// the payload.
ExecutionStatus::irrelevant()
} else {
match payload_verification_status {
PayloadVerificationStatus::Verified => ExecutionStatus::Valid(block_hash),
PayloadVerificationStatus::NotVerified => ExecutionStatus::Unknown(block_hash),
// It would be a logic error to declare a block irrelevant if it has an
// execution payload with a non-zero block hash.
PayloadVerificationStatus::Irrelevant => {
return Err(Error::InvalidPayloadStatus {
block_slot: block.slot(),
block_root,
payload_verification_status,
})
}
}
}
} else {
// There is no payload to verify.
ExecutionStatus::irrelevant()
};
// 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(ProtoBlock {
@@ -568,13 +639,43 @@ where
)
.map_err(Error::BeaconStateError)?,
state_root: block.state_root(),
justified_epoch: state.current_justified_checkpoint().epoch,
finalized_epoch: state.finalized_checkpoint().epoch,
justified_checkpoint: state.current_justified_checkpoint(),
finalized_checkpoint: state.finalized_checkpoint(),
execution_status,
})?;
Ok(())
}
/// Validates the `epoch` against the current time according to the fork choice store.
///
/// ## Specification
///
/// Equivalent to:
///
/// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#validate_target_epoch_against_current_time
fn validate_target_epoch_against_current_time(
&self,
target_epoch: Epoch,
) -> Result<(), InvalidAttestation> {
let slot_now = self.fc_store.get_current_slot();
let epoch_now = slot_now.epoch(E::slots_per_epoch());
// Attestation must be from the current or previous epoch.
if target_epoch > epoch_now {
return Err(InvalidAttestation::FutureEpoch {
attestation_epoch: target_epoch,
current_epoch: epoch_now,
});
} else if target_epoch + 1 < epoch_now {
return Err(InvalidAttestation::PastEpoch {
attestation_epoch: target_epoch,
current_epoch: epoch_now,
});
}
Ok(())
}
/// Validates the `indexed_attestation` for application to fork choice.
///
/// ## Specification
@@ -585,6 +686,7 @@ where
fn validate_on_attestation(
&self,
indexed_attestation: &IndexedAttestation<E>,
is_from_block: AttestationFromBlock,
) -> Result<(), InvalidAttestation> {
// There is no point in processing an attestation with an empty bitfield. Reject
// it immediately.
@@ -595,21 +697,10 @@ where
return Err(InvalidAttestation::EmptyAggregationBitfield);
}
let slot_now = self.fc_store.get_current_slot();
let epoch_now = slot_now.epoch(E::slots_per_epoch());
let target = indexed_attestation.data.target;
// Attestation must be from the current or previous epoch.
if target.epoch > epoch_now {
return Err(InvalidAttestation::FutureEpoch {
attestation_epoch: target.epoch,
current_epoch: epoch_now,
});
} else if target.epoch + 1 < epoch_now {
return Err(InvalidAttestation::PastEpoch {
attestation_epoch: target.epoch,
current_epoch: epoch_now,
});
if matches!(is_from_block, AttestationFromBlock::False) {
self.validate_target_epoch_against_current_time(target.epoch)?;
}
if target.epoch != indexed_attestation.data.slot.epoch(E::slots_per_epoch()) {
@@ -692,6 +783,7 @@ where
&mut self,
current_slot: Slot,
attestation: &IndexedAttestation<E>,
is_from_block: AttestationFromBlock,
) -> Result<(), Error<T::Error>> {
// Ensure the store is up-to-date.
self.update_time(current_slot)?;
@@ -713,7 +805,7 @@ where
return Ok(());
}
self.validate_on_attestation(attestation)?;
self.validate_on_attestation(attestation, is_from_block)?;
if attestation.data.slot < self.fc_store.get_current_slot() {
for validator_index in attestation.attesting_indices.iter() {
@@ -839,6 +931,11 @@ where
&self.queued_attestations
}
/// Returns the store's `proposer_boost_root`.
pub fn proposer_boost_root(&self) -> Hash256 {
self.fc_store.proposer_boost_root()
}
/// Prunes the underlying fork choice DAG.
pub fn prune(&mut self) -> Result<(), Error<T::Error>> {
let finalized_root = self.fc_store.finalized_checkpoint().root;
@@ -880,7 +977,7 @@ where
/// This is used when persisting the state of the fork choice to disk.
#[derive(Encode, Decode, Clone)]
pub struct PersistedForkChoice {
proto_array_bytes: Vec<u8>,
pub proto_array_bytes: Vec<u8>,
queued_attestations: Vec<QueuedAttestation>,
}

View File

@@ -19,7 +19,7 @@ use types::{BeaconBlock, BeaconState, Checkpoint, EthSpec, Hash256, Slot};
pub trait ForkChoiceStore<T: EthSpec>: Sized {
type Error;
/// Returns the last value passed to `Self::update_time`.
/// Returns the last value passed to `Self::set_current_slot`.
fn get_current_slot(&self) -> Slot;
/// Set the value to be returned by `Self::get_current_slot`.
@@ -50,6 +50,9 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// Returns the `finalized_checkpoint`.
fn finalized_checkpoint(&self) -> &Checkpoint;
/// Returns the `proposer_boost_root`.
fn proposer_boost_root(&self) -> Hash256;
/// Sets `finalized_checkpoint`.
fn set_finalized_checkpoint(&mut self, checkpoint: Checkpoint);
@@ -58,4 +61,7 @@ pub trait ForkChoiceStore<T: EthSpec>: Sized {
/// Sets the `best_justified_checkpoint`.
fn set_best_justified_checkpoint(&mut self, checkpoint: Checkpoint);
/// Sets the proposer boost root.
fn set_proposer_boost_root(&mut self, proposer_boost_root: Hash256);
}

View File

@@ -2,7 +2,8 @@ mod fork_choice;
mod fork_choice_store;
pub use crate::fork_choice::{
Error, ForkChoice, InvalidAttestation, InvalidBlock, PersistedForkChoice, QueuedAttestation,
AttestationFromBlock, Error, ForkChoice, InvalidAttestation, InvalidBlock,
PayloadVerificationStatus, PersistedForkChoice, QueuedAttestation,
};
pub use fork_choice_store::ForkChoiceStore;
pub use proto_array::Block as ProtoBlock;