init payload processing

This commit is contained in:
Eitan Seri- Levi
2026-02-10 09:15:50 -08:00
parent a1176e77be
commit 43c24d3ee2
6 changed files with 472 additions and 1 deletions

View File

@@ -1147,6 +1147,24 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
}
}
/// Returns the full block at the given root, if it's available in the database.
///
/// Should always return a full block for pre-merge and post-gloas blocks.
pub fn get_full_block(
&self,
block_root: &Hash256,
) -> Result<Option<SignedBeaconBlock<T::EthSpec>>, Error> {
match self.store.try_get_full_block(block_root)? {
Some(DatabaseBlock::Full(block)) => Ok(Some(block)),
Some(DatabaseBlock::Blinded(_)) => {
// TODO(gloas) this should error out
todo!()
}
None => Ok(None),
}
}
/// Returns the block at the given root, if any.
///
/// ## Errors

View File

@@ -718,7 +718,7 @@ pub struct SignatureVerifiedBlock<T: BeaconChainTypes> {
}
/// Used to await the result of executing payload with an EE.
type PayloadVerificationHandle = JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>;
pub type PayloadVerificationHandle = JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>;
/// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and
/// ready to import into the `BeaconChain`. The validation includes:

View File

@@ -7,6 +7,7 @@
//! So, this module contains functions that one might expect to find in other crates, but they live
//! here for good reason.
use crate::payload_envelope_verification::EnvelopeError;
use crate::{
BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, BlockProductionError,
ExecutionPayloadError,

View File

@@ -42,6 +42,8 @@ pub mod observed_block_producers;
pub mod observed_data_sidecars;
pub mod observed_operations;
mod observed_slashable;
pub mod payload_envelope_verification;
pub mod payload_envelope_verification_types;
pub mod persisted_beacon_chain;
pub mod persisted_custody;
mod persisted_fork_choice;

View File

@@ -0,0 +1,419 @@
//! 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 `SignedBeaconBlock` and finishing with a `Fully VerifiedBlock` (see
//! diagram below).
//!
//! ```ignore
//! START
//! |
//! ▼
//! SignedExecutionPayloadEnvelope
//! |
//! |---------------
//! | |
//! | ▼
//! | GossipVerifiedEnvelope
//! | |
//! |---------------
//! |
//! ▼
//! ExecutionPendingEnvelope
//! |
//! await
//! |
//! ▼
//! END
//!
//! ```
use crate::NotifyExecutionLayer;
use crate::block_verification::{PayloadVerificationHandle, PayloadVerificationOutcome};
use crate::payload_envelope_verification_types::{EnvelopeImportData, MaybeAvailableEnvelope};
use crate::execution_payload::PayloadNotifier;
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes};
use educe::Educe;
use slot_clock::SlotClock;
use state_processing::envelope_processing::{EnvelopeProcessingError, process_execution_payload_envelope};
use state_processing::{BlockProcessingError, VerifySignatures};
use std::sync::Arc;
use tracing::{debug, instrument};
use types::{
BeaconState, BeaconStateError, EthSpec, ExecutionBlockHash, Hash256, SignedBeaconBlock,
SignedExecutionPayloadEnvelope, Slot,
};
#[derive(Debug, Clone)]
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,
},
// Some Beacon Chain Error
BeaconChainError(Arc<BeaconChainError>),
// Some Beacon State error
BeaconStateError(BeaconStateError),
// Some BlockProcessingError (for electra operations)
BlockProcessingError(BlockProcessingError),
// Some EnvelopeProcessingError
EnvelopeProcessingError(EnvelopeProcessingError),
}
impl From<BeaconChainError> for EnvelopeError {
fn from(e: BeaconChainError) -> Self {
EnvelopeError::BeaconChainError(Arc::new(e))
}
}
impl From<BeaconStateError> for EnvelopeError {
fn from(e: BeaconStateError) -> Self {
EnvelopeError::BeaconStateError(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),
}
}
}
/// 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,
}
#[allow(clippy::type_complexity)]
#[instrument(skip_all, level = "debug", fields(beacon_block_root = %envelope.beacon_block_root()))]
fn load_snapshot<T: BeaconChainTypes>(
envelope: &SignedExecutionPayloadEnvelope<T::EthSpec>,
chain: &BeaconChain<T>,
) -> Result<EnvelopeProcessingSnapshot<T::EthSpec>, EnvelopeError> {
// Reject any block 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 block because we should only import a child
// envelope after its parent has been fully imported.
// - Pre-finalized: if the 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 = chain.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);
// TODO(EIP-7732): add metrics here
let block_state_root = proto_beacon_block.state_root;
// 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 = chain
.store
.get_hot_state(&block_state_root, cache_state)
.map_err(|e| EnvelopeError::BeaconChainError(Arc::new(e.into())))?
.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,
})
}
/// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates it has been approved for re-gossiping on
/// the p2p network.
#[derive(Educe)]
#[educe(Debug(bound = "T: BeaconChainTypes"))]
pub struct GossipVerifiedEnvelope<T: BeaconChainTypes> {
pub signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
pub block: Arc<SignedBeaconBlock<T::EthSpec>>,
pub snapshot: Option<Box<EnvelopeProcessingSnapshot<T::EthSpec>>>,
}
impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
pub fn new(
signed_envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
chain: &BeaconChain<T>,
) -> Result<Self, EnvelopeError> {
let envelope = &signed_envelope.message;
let payload = &envelope.payload;
let beacon_block_root = envelope.beacon_block_root;
// Check that we've seen the beacon block for this envelope and that it passes validation.
// TODO(EIP-7732): We need a block status table in order to differentiate between:
//
// 1. Blocks we haven't seen (IGNORE), and
// 2. Blocks we've seen that are invalid (REJECT).
//
// Presently these two cases are conflated.
let fork_choice_read_lock = chain.canonical_head.fork_choice_read_lock();
let Some(proto_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);
// TODO(EIP-7732): check that we haven't seen another valid `SignedExecutionPayloadEnvelope`
// for this block root from this builder - envelope status table check
// TODO(EIP-7732): this could be obtained from the ProtoBlock instead of the DB
// but this means the ProtoBlock needs to include something like the ExecutionBid
// will need to answer this question later.
let block = chain
.get_full_block(&beacon_block_root)?
.ok_or_else(|| {
EnvelopeError::from(BeaconChainError::MissingBeaconBlock(beacon_block_root))
})
.map(Arc::new)?;
let execution_bid = &block
.message()
.body()
.signed_execution_payload_bid()?
.message;
// TODO(EIP-7732): Gossip rules for the beacon block contain the following:
// https://github.com/ethereum/consensus-specs/blob/master/specs/phase0/p2p-interface.md#beacon_block
// [IGNORE] The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
// [IGNORE] The block is from a slot greater than the latest finalized slot
// should these kinds of checks be included for envelopes as well?
// check that the slot of the envelope matches the slot of the parent block
if envelope.slot != block.slot() {
return Err(EnvelopeError::SlotMismatch {
block: block.slot(),
envelope: envelope.slot,
});
}
// builder index matches committed bid
if envelope.builder_index != execution_bid.builder_index {
return Err(EnvelopeError::BuilderIndexMismatch {
committed_bid: execution_bid.builder_index,
envelope: envelope.builder_index,
});
}
// the block hash should match the block hash of the execution bid
if payload.block_hash != execution_bid.block_hash {
return Err(EnvelopeError::BlockHashMismatch {
committed_bid: execution_bid.block_hash,
envelope: payload.block_hash,
});
}
// Get the fork from the proposer cache so we can verify the signature.
// This is currently the most efficient way to implement envelope signature verification
// because the `fork` might depend on advancing the parent state.
let block_slot = envelope.slot;
let block_epoch = block_slot.epoch(T::EthSpec::slots_per_epoch());
let proposer_shuffling_decision_block =
proto_block.proposer_shuffling_root_for_child_block(block_epoch, &chain.spec);
let mut opt_snapshot = None;
let envelope_ref = signed_envelope.as_ref();
let proposer = chain.with_proposer_cache::<_, EnvelopeError>(
proposer_shuffling_decision_block,
block_epoch,
|proposers| proposers.get_slot::<T::EthSpec>(block_slot),
|| {
debug!(
%beacon_block_root,
block_hash = %envelope_ref.block_hash(),
"Proposer shuffling cache miss for envelope verification"
);
// The proposer index was *not* cached and we must load the parent in order to
// determine the proposer index.
let snapshot = load_snapshot(envelope_ref, chain)?;
opt_snapshot = Some(Box::new(snapshot.clone()));
Ok((snapshot.state_root, snapshot.pre_state))
},
)?;
let fork = proposer.fork;
// True builder index accounting for self-building.
let proposer_index = block.message().proposer_index();
let builder_index = envelope.builder_index;
let signature_is_valid = {
let pubkey_cache = chain.validator_pubkey_cache.read();
let builder_pubkey = pubkey_cache
.get(builder_index as usize)
.ok_or_else(|| EnvelopeError::UnknownValidator { builder_index })?;
signed_envelope.verify_signature(
builder_pubkey,
&fork,
chain.genesis_validators_root,
&chain.spec,
)
};
if !signature_is_valid {
return Err(EnvelopeError::BadSignature);
}
Ok(Self {
signed_envelope,
block,
snapshot: opt_snapshot,
})
}
pub fn envelope_cloned(&self) -> Arc<SignedExecutionPayloadEnvelope<T::EthSpec>> {
self.signed_envelope.clone()
}
}
pub trait IntoExecutionPendingEnvelope<T: BeaconChainTypes>: Sized {
fn into_execution_pending_envelope(
self,
chain: &Arc<BeaconChain<T>>,
notify_execution_layer: NotifyExecutionLayer,
) -> Result<ExecutionPendingEnvelope<T>, EnvelopeError>;
}
pub struct ExecutionPendingEnvelope<T: BeaconChainTypes> {
pub signed_envelope: MaybeAvailableEnvelope<T::EthSpec>,
pub import_data: EnvelopeImportData<T::EthSpec>,
pub payload_verification_handle: PayloadVerificationHandle,
}
impl<T: BeaconChainTypes> IntoExecutionPendingEnvelope<T> for GossipVerifiedEnvelope<T> {
fn into_execution_pending_envelope(
self,
chain: &Arc<BeaconChain<T>>,
notify_execution_layer: NotifyExecutionLayer,
) -> Result<ExecutionPendingEnvelope<T>, EnvelopeError> {
let signed_envelope = self.signed_envelope;
let envelope = &signed_envelope.message;
let payload = &envelope.payload;
// Verify the execution payload is valid
let payload_notifier =
PayloadNotifier::from_envelope(chain.clone(), envelope, notify_execution_layer)?;
let block_root = envelope.beacon_block_root;
let slot = self.block.slot();
let payload_verification_future = async move {
let chain = payload_notifier.chain.clone();
// TODO:(gloas): timing metrics
if let Some(started_execution) = chain.slot_clock.now_duration() {
chain.block_times_cache.write().set_time_started_execution(
block_root,
slot,
started_execution,
);
}
let payload_verification_status = payload_notifier.notify_new_payload().await?;
Ok(PayloadVerificationOutcome {
payload_verification_status,
// This fork is after the merge so it'll never be the merge transition block
is_valid_merge_transition_block: false,
})
};
// Spawn the payload verification future as a new task, but don't wait for it to complete.
// The `payload_verification_future` will be awaited later to ensure verification completed
// successfully.
let payload_verification_handle = chain
.task_executor
.spawn_handle(
payload_verification_future,
"execution_payload_verification",
)
.ok_or(BeaconChainError::RuntimeShutdown)?;
let snapshot = if let Some(snapshot) = self.snapshot {
*snapshot
} else {
load_snapshot(signed_envelope.as_ref(), chain)?
};
let mut state = snapshot.pre_state;
// All the state modifications are done in envelope_processing
process_execution_payload_envelope(
&mut state,
Some(snapshot.state_root),
&signed_envelope,
// verify signature already done for GossipVerifiedEnvelope
VerifySignatures::False,
&chain.spec,
)?;
Ok(ExecutionPendingEnvelope {
signed_envelope: MaybeAvailableEnvelope::AvailabilityPending {
block_hash: payload.block_hash,
envelope: signed_envelope,
},
import_data: EnvelopeImportData {
block_root,
block: self.block,
post_state: Box::new(state),
},
payload_verification_handle,
})
}
}
impl<T: BeaconChainTypes> IntoExecutionPendingEnvelope<T>
for Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>
{
fn into_execution_pending_envelope(
self,
chain: &Arc<BeaconChain<T>>,
notify_execution_layer: NotifyExecutionLayer,
) -> Result<ExecutionPendingEnvelope<T>, EnvelopeError> {
// TODO(EIP-7732): figure out how this should be refactored..
GossipVerifiedEnvelope::new(self, chain)?
.into_execution_pending_envelope(chain, notify_execution_layer)
}
}

View File

@@ -0,0 +1,31 @@
use std::sync::Arc;
use types::{
BeaconState, ChainSpec, DataColumnSidecarList, EthSpec, ExecutionBlockHash, Hash256,
SignedBeaconBlock, SignedExecutionPayloadEnvelope,
};
#[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> {
// TODO(EIP-7732): rename to execution_block_hash
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>,
}
pub enum MaybeAvailableEnvelope<E: EthSpec> {
Available(AvailableEnvelope<E>),
AvailabilityPending {
block_hash: ExecutionBlockHash,
envelope: Arc<SignedExecutionPayloadEnvelope<E>>,
},
}