Files
lighthouse/beacon_node/beacon_chain/src/payload_envelope_verification/mod.rs
2026-02-24 12:08:07 -08:00

336 lines
11 KiB
Rust

//! The incremental processing steps (e.g., signatures verified but not the state transition) is
//! represented as a sequence of wrapper-types around the block. There is a linear progression of
//! types, starting at a `SignedExecutionPayloadEnvelope` and finishing with an `AvailableExecutedEnvelope` (see
//! diagram below).
//!
//! // TODO(gloas) we might want to update this diagram to include `AvailabelExecutedEnvelope`
//! ```ignore
//! START
//! |
//! ▼
//! SignedExecutionPayloadEnvelope
//! |
//! |---------------
//! | |
//! | ▼
//! | GossipVerifiedEnvelope
//! | |
//! |---------------
//! |
//! ▼
//! ExecutionPendingEnvelope
//! |
//! await
//! |
//! ▼
//! END
//!
//! ```
use std::sync::Arc;
use store::Error as DBError;
use state_processing::{BlockProcessingError, envelope_processing::EnvelopeProcessingError};
use tracing::instrument;
use types::{
BeaconState, BeaconStateError, ChainSpec, DataColumnSidecarList, EthSpec, ExecutionBlockHash,
ExecutionPayloadEnvelope, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot,
};
use crate::{
BeaconChainError, BeaconChainTypes, BeaconStore, BlockError, ExecutionPayloadError,
PayloadVerificationOutcome, canonical_head::CanonicalHead,
};
pub mod execution_pending_envelope;
pub mod gossip_verified_envelope;
pub mod import;
mod payload_notifier;
pub use execution_pending_envelope::{ExecutionPendingEnvelope, IntoExecutionPendingEnvelope};
#[derive(PartialEq)]
pub struct EnvelopeImportData<E: EthSpec> {
pub block_root: Hash256,
pub block: Arc<SignedBeaconBlock<E>>,
pub post_state: Box<BeaconState<E>>,
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct AvailableEnvelope<E: EthSpec> {
execution_block_hash: ExecutionBlockHash,
envelope: Arc<SignedExecutionPayloadEnvelope<E>>,
columns: DataColumnSidecarList<E>,
/// Timestamp at which this block first became available (UNIX timestamp, time since 1970).
columns_available_timestamp: Option<std::time::Duration>,
pub spec: Arc<ChainSpec>,
}
impl<E: EthSpec> AvailableEnvelope<E> {
pub fn message(&self) -> &ExecutionPayloadEnvelope<E> {
&self.envelope.message
}
#[allow(clippy::type_complexity)]
pub fn deconstruct(
self,
) -> (
Arc<SignedExecutionPayloadEnvelope<E>>,
DataColumnSidecarList<E>,
) {
let AvailableEnvelope {
envelope, columns, ..
} = self;
(envelope, columns)
}
}
pub enum MaybeAvailableEnvelope<E: EthSpec> {
Available(AvailableEnvelope<E>),
AvailabilityPending {
block_hash: ExecutionBlockHash,
envelope: Arc<SignedExecutionPayloadEnvelope<E>>,
},
}
/// This snapshot is to be used for verifying a envelope of the block.
#[derive(Debug, Clone)]
pub struct EnvelopeProcessingSnapshot<E: EthSpec> {
/// This state is equivalent to the `self.beacon_block.state_root()` before applying the envelope.
pub pre_state: BeaconState<E>,
pub state_root: Hash256,
pub beacon_block_root: Hash256,
}
/// A payload envelope that has gone through processing checks and execution by an EL client.
/// This envelope hasn't necessarily completed data availability checks.
///
///
/// It contains 2 variants:
/// 1. `Available`: This enelope has been executed and also contains all data to consider it
/// fully available.
/// 2. `AvailabilityPending`: This envelope hasn't received all required blobs to consider it
/// fully available.
pub enum ExecutedEnvelope<E: EthSpec> {
Available(AvailableExecutedEnvelope<E>),
// TODO(gloas) implement availability pending
AvailabilityPending(),
}
impl<E: EthSpec> ExecutedEnvelope<E> {
pub fn new(
envelope: MaybeAvailableEnvelope<E>,
import_data: EnvelopeImportData<E>,
payload_verification_outcome: PayloadVerificationOutcome,
) -> Self {
match envelope {
MaybeAvailableEnvelope::Available(available_envelope) => {
Self::Available(AvailableExecutedEnvelope::new(
available_envelope,
import_data,
payload_verification_outcome,
))
}
// TODO(gloas) implement availability pending
MaybeAvailableEnvelope::AvailabilityPending {
block_hash: _,
envelope: _,
} => Self::AvailabilityPending(),
}
}
}
/// A payload envelope that has completed all payload processing checks including verification
/// by an EL client **and** has all requisite blob data to be imported into fork choice.
pub struct AvailableExecutedEnvelope<E: EthSpec> {
pub envelope: AvailableEnvelope<E>,
pub import_data: EnvelopeImportData<E>,
pub payload_verification_outcome: PayloadVerificationOutcome,
}
impl<E: EthSpec> AvailableExecutedEnvelope<E> {
pub fn new(
envelope: AvailableEnvelope<E>,
import_data: EnvelopeImportData<E>,
payload_verification_outcome: PayloadVerificationOutcome,
) -> Self {
Self {
envelope,
import_data,
payload_verification_outcome,
}
}
}
#[derive(Debug)]
pub enum EnvelopeError {
/// The envelope's block root is unknown.
BlockRootUnknown {
block_root: Hash256,
},
/// The signature is invalid.
BadSignature,
/// The builder index doesn't match the committed bid
BuilderIndexMismatch {
committed_bid: u64,
envelope: u64,
},
// The envelope slot doesn't match the block
SlotMismatch {
block: Slot,
envelope: Slot,
},
// The validator index is unknown
UnknownValidator {
builder_index: u64,
},
// The block hash doesn't match the committed bid
BlockHashMismatch {
committed_bid: ExecutionBlockHash,
envelope: ExecutionBlockHash,
},
// The block's proposer_index does not match the locally computed proposer
IncorrectBlockProposer {
block: u64,
local_shuffling: u64,
},
// The slot belongs to a block that is from a slot prior than
// the most recently finalized slot
PriorToFinalization {
payload_slot: Slot,
latest_finalized_slot: Slot,
},
// Some Beacon Chain Error
BeaconChainError(Arc<BeaconChainError>),
// Some Beacon State error
BeaconStateError(BeaconStateError),
// Some BlockProcessingError (for electra operations)
BlockProcessingError(BlockProcessingError),
// Some EnvelopeProcessingError
EnvelopeProcessingError(EnvelopeProcessingError),
// Error verifying the execution payload
ExecutionPayloadError(ExecutionPayloadError),
// An error from block-level checks reused during envelope import
BlockError(BlockError),
// Internal error
InternalError(String),
}
impl std::fmt::Display for EnvelopeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<BeaconChainError> for EnvelopeError {
fn from(e: BeaconChainError) -> Self {
EnvelopeError::BeaconChainError(Arc::new(e))
}
}
impl From<ExecutionPayloadError> for EnvelopeError {
fn from(e: ExecutionPayloadError) -> Self {
EnvelopeError::ExecutionPayloadError(e)
}
}
impl From<BeaconStateError> for EnvelopeError {
fn from(e: BeaconStateError) -> Self {
EnvelopeError::BeaconStateError(e)
}
}
impl From<DBError> for EnvelopeError {
fn from(e: DBError) -> Self {
EnvelopeError::BeaconChainError(Arc::new(BeaconChainError::DBError(e)))
}
}
impl From<BlockError> for EnvelopeError {
fn from(e: BlockError) -> Self {
EnvelopeError::BlockError(e)
}
}
/// Pull errors up from EnvelopeProcessingError to EnvelopeError
impl From<EnvelopeProcessingError> for EnvelopeError {
fn from(e: EnvelopeProcessingError) -> Self {
match e {
EnvelopeProcessingError::BadSignature => EnvelopeError::BadSignature,
EnvelopeProcessingError::BeaconStateError(e) => EnvelopeError::BeaconStateError(e),
EnvelopeProcessingError::BlockHashMismatch {
committed_bid,
envelope,
} => EnvelopeError::BlockHashMismatch {
committed_bid,
envelope,
},
EnvelopeProcessingError::BlockProcessingError(e) => {
EnvelopeError::BlockProcessingError(e)
}
e => EnvelopeError::EnvelopeProcessingError(e),
}
}
}
#[allow(clippy::type_complexity)]
#[instrument(skip_all, level = "debug", fields(beacon_block_root = %beacon_block_root))]
/// Load state from store given a known state root and block root.
/// Use this when the proto block has already been looked up from fork choice.
pub(crate) fn load_snapshot_from_state_root<T: BeaconChainTypes>(
beacon_block_root: Hash256,
block_state_root: Hash256,
store: &BeaconStore<T>,
) -> Result<EnvelopeProcessingSnapshot<T::EthSpec>, EnvelopeError> {
// TODO(EIP-7732): add metrics here
// We can use `get_hot_state` here rather than `get_advanced_hot_state` because the envelope
// must be from the same slot as its block (so no advance is required).
let cache_state = true;
let state = store
.get_hot_state(&block_state_root, cache_state)
.map_err(EnvelopeError::from)?
.ok_or_else(|| {
BeaconChainError::DBInconsistent(format!(
"Missing state for envelope block {block_state_root:?}",
))
})?;
Ok(EnvelopeProcessingSnapshot {
pre_state: state,
state_root: block_state_root,
beacon_block_root,
})
}
#[instrument(skip_all, level = "debug", fields(beacon_block_root = %envelope.beacon_block_root()))]
pub(crate) fn load_snapshot<T: BeaconChainTypes>(
envelope: &SignedExecutionPayloadEnvelope<T::EthSpec>,
canonical_head: &CanonicalHead<T>,
store: &BeaconStore<T>,
) -> Result<EnvelopeProcessingSnapshot<T::EthSpec>, EnvelopeError> {
// Reject any envelope if its block is not known to fork choice.
//
// A block that is not in fork choice is either:
//
// - Not yet imported: we should reject this envelope because we should only import it after
// its parent block has been fully imported.
// - Pre-finalized: if the parent block is _prior_ to finalization, we should ignore the
// envelope because it will revert finalization. Note that the finalized block is stored in
// fork choice, so we will not reject any child of the finalized block (this is relevant
// during genesis).
let fork_choice_read_lock = canonical_head.fork_choice_read_lock();
let beacon_block_root = envelope.beacon_block_root();
let Some(proto_beacon_block) = fork_choice_read_lock.get_block(&beacon_block_root) else {
return Err(EnvelopeError::BlockRootUnknown {
block_root: beacon_block_root,
});
};
drop(fork_choice_read_lock);
load_snapshot_from_state_root::<T>(beacon_block_root, proto_beacon_block.state_root, store)
}