diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ec79153785..a59508a37a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1147,6 +1147,24 @@ impl BeaconChain { } } + + /// 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>, 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 diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 9bb6757341..9d1b8e0d3d 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -718,7 +718,7 @@ pub struct SignatureVerifiedBlock { } /// Used to await the result of executing payload with an EE. -type PayloadVerificationHandle = JoinHandle>>; +pub type PayloadVerificationHandle = JoinHandle>>; /// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and /// ready to import into the `BeaconChain`. The validation includes: diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index bdf3ab9594..370f044c7a 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -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, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index e77739e2d5..7aa64deb1c 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -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; diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification.rs b/beacon_node/beacon_chain/src/payload_envelope_verification.rs new file mode 100644 index 0000000000..b22acd69fb --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification.rs @@ -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), + // Some Beacon State error + BeaconStateError(BeaconStateError), + // Some BlockProcessingError (for electra operations) + BlockProcessingError(BlockProcessingError), + // Some EnvelopeProcessingError + EnvelopeProcessingError(EnvelopeProcessingError), +} + +impl From for EnvelopeError { + fn from(e: BeaconChainError) -> Self { + EnvelopeError::BeaconChainError(Arc::new(e)) + } +} + +impl From for EnvelopeError { + fn from(e: BeaconStateError) -> Self { + EnvelopeError::BeaconStateError(e) + } +} + +/// Pull errors up from EnvelopeProcessingError to EnvelopeError +impl From 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 { + /// This state is equivalent to the `self.beacon_block.state_root()` before applying the envelope. + pub pre_state: BeaconState, + 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( + envelope: &SignedExecutionPayloadEnvelope, + chain: &BeaconChain, +) -> Result, 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 { + pub signed_envelope: Arc>, + pub block: Arc>, + pub snapshot: Option>>, +} + +impl GossipVerifiedEnvelope { + pub fn new( + signed_envelope: Arc>, + chain: &BeaconChain, + ) -> Result { + 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::(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> { + self.signed_envelope.clone() + } +} + +pub trait IntoExecutionPendingEnvelope: Sized { + fn into_execution_pending_envelope( + self, + chain: &Arc>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result, EnvelopeError>; +} + +pub struct ExecutionPendingEnvelope { + pub signed_envelope: MaybeAvailableEnvelope, + pub import_data: EnvelopeImportData, + pub payload_verification_handle: PayloadVerificationHandle, +} + +impl IntoExecutionPendingEnvelope for GossipVerifiedEnvelope { + fn into_execution_pending_envelope( + self, + chain: &Arc>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result, 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 IntoExecutionPendingEnvelope + for Arc> +{ + fn into_execution_pending_envelope( + self, + chain: &Arc>, + notify_execution_layer: NotifyExecutionLayer, + ) -> Result, EnvelopeError> { + // TODO(EIP-7732): figure out how this should be refactored.. + GossipVerifiedEnvelope::new(self, chain)? + .into_execution_pending_envelope(chain, notify_execution_layer) + } +} \ No newline at end of file diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification_types.rs b/beacon_node/beacon_chain/src/payload_envelope_verification_types.rs new file mode 100644 index 0000000000..bd2eb5a34f --- /dev/null +++ b/beacon_node/beacon_chain/src/payload_envelope_verification_types.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; +use types::{ + BeaconState, ChainSpec, DataColumnSidecarList, EthSpec, ExecutionBlockHash, Hash256, + SignedBeaconBlock, SignedExecutionPayloadEnvelope, +}; + +#[derive(PartialEq)] +pub struct EnvelopeImportData { + pub block_root: Hash256, + pub block: Arc>, + pub post_state: Box>, +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct AvailableEnvelope { + // TODO(EIP-7732): rename to execution_block_hash + block_hash: ExecutionBlockHash, + envelope: Arc>, + columns: DataColumnSidecarList, + /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). + columns_available_timestamp: Option, + pub spec: Arc, +} +pub enum MaybeAvailableEnvelope { + Available(AvailableEnvelope), + AvailabilityPending { + block_hash: ExecutionBlockHash, + envelope: Arc>, + }, +} \ No newline at end of file