This commit is contained in:
Eitan Seri- Levi
2026-02-11 14:53:24 -08:00
parent 22f3fd4ccf
commit 9f972d1743
9 changed files with 211 additions and 228 deletions

View File

@@ -56,6 +56,7 @@ use crate::observed_block_producers::ObservedBlockProducers;
use crate::observed_data_sidecars::ObservedDataSidecars;
use crate::observed_operations::{ObservationOutcome, ObservedOperations};
use crate::observed_slashable::ObservedSlashable;
use crate::payload_envelope_verification::{ExecutedEnvelope, ExecutionPendingEnvelope};
use crate::persisted_beacon_chain::PersistedBeaconChain;
use crate::persisted_custody::persist_custody_context;
use crate::persisted_fork_choice::PersistedForkChoice;
@@ -3547,6 +3548,33 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
))
}
/// Accepts a fully-verified payload envelope and awaits on its payload verification handle to
/// get a fully `ExecutedEnvelope`.
///
/// An error is returned if the verification handle couldn't be awaited.
#[instrument(skip_all, level = "debug")]
pub async fn into_executed_payload_envelope(
self: Arc<Self>,
pending_envelope: ExecutionPendingEnvelope<T>,
) -> Result<ExecutedEnvelope<T::EthSpec>, BlockError> {
let ExecutionPendingEnvelope {
signed_envelope,
import_data,
payload_verification_handle,
} = pending_envelope;
let payload_verification_outcome = payload_verification_handle
.await
.map_err(BeaconChainError::TokioJoin)?
.ok_or(BeaconChainError::RuntimeShutdown)??;
Ok(ExecutedEnvelope::new(
signed_envelope,
import_data,
payload_verification_outcome,
))
}
/* Import methods */
/// Checks if the block is available, and imports immediately if so, otherwise caches the block

View File

@@ -7,7 +7,6 @@
//! 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

@@ -4,7 +4,10 @@ use educe::Educe;
use slot_clock::SlotClock;
use state_processing::{VerifySignatures, envelope_processing::process_execution_payload_envelope};
use tracing::debug;
use types::{EthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope, consts::gloas::BUILDER_INDEX_SELF_BUILD};
use types::{
EthSpec, SignedBeaconBlock, SignedExecutionPayloadEnvelope,
consts::gloas::BUILDER_INDEX_SELF_BUILD,
};
use crate::{
BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer,
@@ -70,7 +73,7 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
.signed_execution_payload_bid()?
.message;
// check that the envelopes slot isnt from a slot prior
// check that the envelopes slot isnt from a slot prior
// to the latest finalized slot.
if envelope.slot < latest_finalized_slot {
return Err(EnvelopeError::PriorToFinalization {
@@ -103,54 +106,58 @@ impl<T: BeaconChainTypes> GossipVerifiedEnvelope<T> {
});
}
// 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.
// Verify the envelope signature.
//
// For self-build envelopes, we can use the proposer cache for the fork and the
// validator pubkey cache for the proposer's pubkey, avoiding a state load from disk.
// For external builder envelopes, we must load the state to access the builder registry.
let builder_index = envelope.builder_index;
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;
let builder_index = envelope.builder_index;
let index = if builder_index == BUILDER_INDEX_SELF_BUILD {
block.message().proposer_index()
} else {
builder_index
};
let (signature_is_valid, opt_snapshot) = if builder_index == BUILDER_INDEX_SELF_BUILD {
// Fast path: self-build envelopes can be verified without loading the state.
let envelope_ref = signed_envelope.as_ref();
let mut opt_snapshot = None;
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,
"Proposer shuffling cache miss for envelope verification"
);
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;
let signature_is_valid = {
// TODO(gloas) the builder pubkey wont be in the validator pubkey cache
// this will currently only work for local block building.
let pubkey_cache = chain.validator_pubkey_cache.read();
let pubkey = pubkey_cache
.get(index as usize)
.ok_or_else(|| EnvelopeError::UnknownValidator { builder_index: index })?;
signed_envelope.verify_signature(
.get(block.message().proposer_index() as usize)
.ok_or_else(|| EnvelopeError::UnknownValidator {
builder_index: block.message().proposer_index(),
})?;
let is_valid = signed_envelope.verify_signature(
pubkey,
&fork,
chain.genesis_validators_root,
&chain.spec,
)
);
(is_valid, opt_snapshot)
} else {
// TODO(gloas) we should probably introduce a builder cache or some type of
// global cache.
// External builder: must load the state to get the builder pubkey.
let snapshot = load_snapshot(signed_envelope.as_ref(), chain)?;
let is_valid =
signed_envelope.verify_signature_with_state(&snapshot.pre_state, &chain.spec)?;
(is_valid, Some(Box::new(snapshot)))
};
if !signature_is_valid {
@@ -179,20 +186,13 @@ impl<T: BeaconChainTypes> IntoExecutionPendingEnvelope<T> for GossipVerifiedEnve
let envelope = &signed_envelope.message;
let payload = &envelope.payload;
// TODO(gloas) unwrap
let bid = chain
.get_full_block(&envelope.beacon_block_root)
.unwrap()
.unwrap()
.message()
.body()
.signed_execution_payload_bid()
.unwrap()
.message;
// Verify the execution payload is valid
let payload_notifier =
PayloadNotifier::new(chain.clone(), envelope, notify_execution_layer)?;
let payload_notifier = PayloadNotifier::new(
chain.clone(),
signed_envelope.clone(),
self.block.clone(),
notify_execution_layer,
)?;
let block_root = envelope.beacon_block_root;
let slot = self.block.slot();

View File

@@ -17,9 +17,6 @@
//! |---------------
//! |
//! ▼
//! SignatureVerifiedEnvelope
//! |
//! ▼
//! ExecutionPendingEnvelope
//! |
//! await
@@ -41,15 +38,16 @@ use types::{
};
use crate::{
BeaconChain, BeaconChainError, BeaconChainTypes, NotifyExecutionLayer,
block_verification::PayloadVerificationHandle,
AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes,
ExecutionPayloadError, NotifyExecutionLayer, PayloadVerificationOutcome,
block_verification::PayloadVerificationHandle, block_verification_types::BlockImportData,
payload_envelope_verification::gossip_verified_envelope::GossipVerifiedEnvelope,
};
pub mod execution_pending_envelope;
pub mod gossip_verified_envelope;
mod payload_notifier;
mod signature_verified_envelope;
mod tests;
pub trait IntoExecutionPendingEnvelope<T: BeaconChainTypes>: Sized {
fn into_execution_pending_envelope(
@@ -101,7 +99,67 @@ pub struct EnvelopeProcessingSnapshot<E: EthSpec> {
pub beacon_block_root: Hash256,
}
#[derive(Debug, Clone)]
/// 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 {
@@ -142,6 +200,8 @@ pub enum EnvelopeError {
BlockProcessingError(BlockProcessingError),
// Some EnvelopeProcessingError
EnvelopeProcessingError(EnvelopeProcessingError),
// Error verifying the execution payload
ExecutionPayloadError(ExecutionPayloadError),
}
impl From<BeaconChainError> for EnvelopeError {
@@ -150,6 +210,12 @@ impl From<BeaconChainError> for EnvelopeError {
}
}
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)

View File

@@ -1,15 +1,10 @@
use std::sync::Arc;
use execution_layer::NewPayloadRequest;
use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas};
use fork_choice::PayloadVerificationStatus;
use state_processing::{
envelope_processing::partially_verify_payload_envelope,
per_block_processing::is_execution_enabled,
};
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use tracing::warn;
use types::{
BeaconState, ExecutionPayloadBid, Hash256, SignedBeaconBlock, SignedExecutionPayloadEnvelope,
};
use types::{SignedBeaconBlock, SignedExecutionPayloadEnvelope};
use crate::{
BeaconChain, BeaconChainTypes, BlockError, ExecutionPayloadError, NotifyExecutionLayer,
@@ -19,33 +14,26 @@ use crate::{
/// Used to await the result of executing payload with a remote EE.
pub struct PayloadNotifier<T: BeaconChainTypes> {
pub chain: Arc<BeaconChain<T>>,
pub envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
block: Arc<SignedBeaconBlock<T::EthSpec>>,
payload_verification_status: Option<PayloadVerificationStatus>,
}
impl<T: BeaconChainTypes> PayloadNotifier<T> {
pub fn new(
chain: Arc<BeaconChain<T>>,
bid: &ExecutionPayloadBid<T::EthSpec>,
envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
state: &BeaconState<T::EthSpec>,
block: Arc<SignedBeaconBlock<T::EthSpec>>,
notify_execution_layer: NotifyExecutionLayer,
) -> Result<Self, ExecutionPayloadError> {
let payload_verification_status = {
// Perform the initial stages of payload verification.
//
// We will duplicate these checks again during `per_block_processing`, however these
// checks are cheap and doing them here ensures we have verified them before marking
// the block as optimistically imported. This is particularly relevant in the case
// where we do not send the block to the EL at all.
let payload_message = &envelope.message;
partially_verify_payload_envelope(state, &envelope, &chain.spec).unwrap(); // TODO(gloas) unwrap
match notify_execution_layer {
NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => {
// Create a NewPayloadRequest (no clones required) and check optimistic sync verifications
let new_payload_request: NewPayloadRequest<T::EthSpec> =
payload_message.try_into()?;
// TODO(gloas) unwrap
let new_payload_request =
Self::build_new_payload_request(&envelope, &block).unwrap();
if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() {
warn!(
block_number = ?payload_message.payload.block_number,
@@ -65,6 +53,7 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
Ok(Self {
chain,
envelope,
block,
payload_verification_status,
})
}
@@ -73,13 +62,34 @@ impl<T: BeaconChainTypes> PayloadNotifier<T> {
if let Some(precomputed_status) = self.payload_verification_status {
Ok(precomputed_status)
} else {
// tODO(gloas) fix zero
notify_new_payload(
&self.chain,
Hash256::ZERO,
self.envelope.message.try_into()?,
)
.await
let block_root = self.envelope.message.beacon_block_root;
let request = Self::build_new_payload_request(&self.envelope, &self.block)?;
notify_new_payload(&self.chain, block_root, request).await
}
}
fn build_new_payload_request<'a>(
envelope: &'a SignedExecutionPayloadEnvelope<T::EthSpec>,
block: &'a SignedBeaconBlock<T::EthSpec>,
) -> Result<NewPayloadRequest<'a, T::EthSpec>, BlockError> {
let bid = &block
.message()
.body()
.signed_execution_payload_bid()
.map_err(|e| BlockError::BeaconChainError(Box::new(e.into())))?
.message;
let versioned_hashes = bid
.blob_kzg_commitments
.iter()
.map(kzg_commitment_to_versioned_hash)
.collect();
Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas {
execution_payload: &envelope.message.payload,
versioned_hashes,
parent_beacon_block_root: block.message().parent_root(),
execution_requests: &envelope.message.execution_requests,
}))
}
}

View File

@@ -1,40 +0,0 @@
use std::sync::Arc;
use state_processing::ConsensusContext;
use types::{BeaconState, Hash256, SignedExecutionPayloadEnvelope};
use crate::{BeaconChain, BeaconChainTypes, payload_envelope_verification::{EnvelopeError, MaybeAvailableEnvelope}};
/// A wrapper around a `SignedExecutionPayloadEnvelope` that indicates that all signatures (except the deposit
/// signatures) have been verified.
pub struct SignatureVerifiedEnvelope<T: BeaconChainTypes> {
envelope: SignedExecutionPayloadEnvelope<T::EthSpec>,
block_root: Hash256,
state: Option<BeaconState<T::EthSpec>>,
consensus_context: ConsensusContext<T::EthSpec>,
}
impl<T: BeaconChainTypes> SignatureVerifiedEnvelope<T> {
pub fn new(
envelope: Arc<SignedExecutionPayloadEnvelope<T::EthSpec>>,
state: &mut BeaconState<T::EthSpec>,
block_root: Hash256,
chain: &BeaconChain<T>,
) -> Result<Self, EnvelopeError> {
let is_signature_valid = envelope.verify_signature_with_state(state, &chain.spec)?;
if !is_signature_valid {
return Err(EnvelopeError::BadSignature)
}
Self {
envelope,
block_root,
state
}
todo!()
}
}

View File

@@ -0,0 +1,20 @@
use std::sync::Arc;
use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, payload_envelope_verification::{ExecutedEnvelope, ExecutionPendingEnvelope}};
async fn import_execution_pending_envelope<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
execution_pending_envelope: ExecutionPendingEnvelope<T>,
) -> Result<AvailabilityProcessingStatus, String> {
match chain
.clone()
.into_executed_payload_envelope(execution_pending_envelope)
.await
.unwrap()
{
ExecutedEnvelope::Available(envelope) => todo!(),
ExecutedEnvelope::AvailabilityPending() => {
Err("AvailabilityPending not expected in this test. Block not imported.".to_string())
}
}
}

View File

@@ -5,7 +5,7 @@ use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_h
use superstruct::superstruct;
use types::{
BeaconBlockRef, BeaconStateError, EthSpec, ExecutionBlockHash, ExecutionPayload,
ExecutionPayloadEnvelope, ExecutionPayloadRef, Hash256, VersionedHash,
ExecutionPayloadRef, Hash256, VersionedHash,
};
use types::{
ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb,

View File

@@ -276,103 +276,3 @@ pub fn process_execution_payload_envelope<E: EthSpec>(
Ok(())
}
/// Performs *partial* verification of the `payload envelope`.
pub fn partially_verify_payload_envelope<E: EthSpec>(
state: &BeaconState<E>,
signed_envelope: &SignedExecutionPayloadEnvelope<E>,
spec: &ChainSpec,
) -> Result<(), EnvelopeProcessingError> {
let envelope = &signed_envelope.message;
let payload = &signed_envelope.message.payload;
// Verify consistency with the beacon block
let latest_block_header_root = state.latest_block_header().tree_hash_root();
envelope_verify!(
envelope.beacon_block_root == latest_block_header_root,
EnvelopeProcessingError::LatestBlockHeaderMismatch {
envelope_root: envelope.beacon_block_root,
block_header_root: latest_block_header_root,
}
);
envelope_verify!(
envelope.slot == state.slot(),
EnvelopeProcessingError::SlotMismatch {
envelope_slot: envelope.slot,
parent_state_slot: state.slot(),
}
);
// Verify consistency with the committed bid
let committed_bid = state.latest_execution_payload_bid()?;
envelope_verify!(
envelope.builder_index == committed_bid.builder_index,
EnvelopeProcessingError::BuilderIndexMismatch {
committed_bid: committed_bid.builder_index,
envelope: envelope.builder_index,
}
);
envelope_verify!(
committed_bid.prev_randao == payload.prev_randao,
EnvelopeProcessingError::PrevRandaoMismatch {
committed_bid: committed_bid.prev_randao,
envelope: payload.prev_randao,
}
);
// Verify consistency with expected withdrawals
// NOTE: we don't bother hashing here except in case of error, because we can just compare for
// equality directly. This equality check could be more straight-forward if the types were
// changed to match (currently we are comparing VariableList to List). This could happen
// coincidentally when we adopt ProgressiveList.
envelope_verify!(
payload.withdrawals.len() == state.payload_expected_withdrawals()?.len()
&& payload
.withdrawals
.iter()
.eq(state.payload_expected_withdrawals()?.iter()),
EnvelopeProcessingError::WithdrawalsRootMismatch {
state: state.payload_expected_withdrawals()?.tree_hash_root(),
payload: payload.withdrawals.tree_hash_root(),
}
);
// Verify the gas limit
envelope_verify!(
committed_bid.gas_limit == payload.gas_limit,
EnvelopeProcessingError::GasLimitMismatch {
committed_bid: committed_bid.gas_limit,
envelope: payload.gas_limit,
}
);
// Verify the block hash
envelope_verify!(
committed_bid.block_hash == payload.block_hash,
EnvelopeProcessingError::BlockHashMismatch {
committed_bid: committed_bid.block_hash,
envelope: payload.block_hash,
}
);
// Verify consistency of the parent hash with respect to the previous execution payload
envelope_verify!(
payload.parent_hash == *state.latest_block_hash()?,
EnvelopeProcessingError::ParentHashMismatch {
state: *state.latest_block_hash()?,
envelope: payload.parent_hash,
}
);
// Verify timestamp
let state_timestamp = compute_timestamp_at_slot(state, state.slot(), spec)?;
envelope_verify!(
payload.timestamp == state_timestamp,
EnvelopeProcessingError::TimestampMismatch {
state: state_timestamp,
envelope: payload.timestamp,
}
);
Ok(())
}