diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index 1b53f229dc..10a9fbe7c5 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -14,17 +14,18 @@ use state_processing::{ }; use state_processing::{VerifyOperation, state_advance::complete_state_advance}; use tracing::{Span, debug, debug_span, error, trace, warn}; +use tree_hash::TreeHash; use types::{ Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, - BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, Deposit, Eth1Data, EthSpec, FullPayload, - Graffiti, Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, - SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedVoluntaryExit, Slot, - SyncAggregate, + BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, Deposit, Eth1Data, EthSpec, + ExecutionPayloadEnvelope, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, + RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedVoluntaryExit, Slot, SyncAggregate, }; use crate::{ BeaconChain, BeaconChainTypes, BlockProductionError, ProduceBlockVerification, - graffiti_calculator::GraffitiSettings, metrics, + execution_payload_bid::ExecutionPayloadData, graffiti_calculator::GraffitiSettings, metrics, }; pub struct PartialBeaconBlock { @@ -147,7 +148,7 @@ impl BeaconChain { // Produce the execution payload bid. // TODO(gloas) this is strictly for building local bids // We'll need to build out trustless/trusted bid paths. - let (execution_payload_bid, state) = self + let (execution_payload_bid, state, payload_data) = self .clone() .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, 0, u64::MAX) .await?; @@ -165,6 +166,7 @@ impl BeaconChain { chain.complete_partial_beacon_block_gloas( partial_beacon_block, execution_payload_bid, + payload_data, state, verification, ) @@ -417,6 +419,7 @@ impl BeaconChain { &self, partial_beacon_block: PartialBeaconBlock, signed_execution_payload_bid: SignedExecutionPayloadBid, + payload_data: Option>, mut state: BeaconState, verification: ProduceBlockVerification, ) -> Result< @@ -572,6 +575,32 @@ impl BeaconChain { let (mut block, _) = signed_beacon_block.deconstruct(); *block.state_root_mut() = state_root; + // Construct and cache the ExecutionPayloadEnvelope if we have payload data. + // For local building, we always have payload data. + // For trustless building, the builder will provide the envelope separately. + if let Some(payload_data) = payload_data { + let beacon_block_root = block.tree_hash_root(); + let execution_payload_envelope = ExecutionPayloadEnvelope { + payload: payload_data.payload, + execution_requests: payload_data.execution_requests, + builder_index: payload_data.builder_index, + beacon_block_root, + slot: payload_data.slot, + state_root: payload_data.state_root, + }; + + // Cache the envelope for later retrieval for signing and publishing. + self.pending_payload_envelopes + .write() + .insert(beacon_block_root, execution_payload_envelope); + + debug!( + %beacon_block_root, + slot = %block.slot(), + "Cached pending execution payload envelope" + ); + } + // TODO(gloas) // metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a12600141f..4f9e3b950e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -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::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; @@ -419,6 +420,9 @@ pub struct BeaconChain { RwLock, T::EthSpec>>, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock>, + /// Cache of pending execution payload envelopes for local block building. + /// Envelopes are stored here during block production and eventually published. + pub pending_payload_envelopes: RwLock>, /// Maintains a record of which validators have submitted voluntary exits. pub observed_voluntary_exits: Mutex>, /// Maintains a record of which validators we've seen proposer slashings for. diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5dbe662b9b..0cbcc28819 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1009,6 +1009,7 @@ where observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), + pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs index 752337a7e8..213fb71d4e 100644 --- a/beacon_node/beacon_chain/src/execution_payload_bid.rs +++ b/beacon_node/beacon_chain/src/execution_payload_bid.rs @@ -4,8 +4,8 @@ use bls::Signature; use execution_layer::{BlockProposalContentsType, BuilderParams}; use tracing::instrument; use types::{ - Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, Hash256, - SignedExecutionPayloadBid, Slot, + Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, + ExecutionPayloadGloas, ExecutionRequests, Hash256, SignedExecutionPayloadBid, Slot, }; use crate::{ @@ -13,11 +13,27 @@ use crate::{ execution_payload::get_execution_payload, }; +/// Data needed to construct an ExecutionPayloadEnvelope. +/// The envelope requires the beacon_block_root which can only be computed after the block exists. +pub struct ExecutionPayloadData { + pub payload: ExecutionPayloadGloas, + pub execution_requests: ExecutionRequests, + pub builder_index: BuilderIndex, + pub slot: Slot, + pub state_root: Hash256, +} + impl BeaconChain { // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless // bid building. Right now this only works for local building. /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. /// This function assumes we've already done the state advance. + /// + /// Returns the signed bid, the state, and optionally the payload data needed to construct + /// the `ExecutionPayloadEnvelope` after the beacon block is created. + /// + /// For local building, payload data is always returned (`Some`). + /// For trustless building, the builder provides the envelope separately, so `None` is returned. #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, @@ -30,6 +46,7 @@ impl BeaconChain { ( SignedExecutionPayloadBid, BeaconState, + Option>, ), BlockProductionError, > { @@ -82,33 +99,41 @@ impl BeaconChain { .map_err(BlockProductionError::TokioJoin)? .ok_or(BlockProductionError::ShuttingDown)??; - let (execution_payload, blob_kzg_commitments) = match block_contents_type { - BlockProposalContentsType::Full(block_proposal_contents) => { - let blob_kzg_commitments = - block_proposal_contents.blob_kzg_commitments().cloned(); + let (execution_payload, blob_kzg_commitments, execution_requests) = + match block_contents_type { + BlockProposalContentsType::Full(block_proposal_contents) => { + let (payload, blob_kzg_commitments, _, execution_requests, _) = + block_proposal_contents.deconstruct(); - if let Some(blob_kzg_commitments) = blob_kzg_commitments { - ( - block_proposal_contents.to_payload().execution_payload(), - blob_kzg_commitments, - ) - } else { - return Err(BlockProductionError::MissingKzgCommitment( - "No KZG commitments from the payload".to_owned(), - )); + if let Some(blob_kzg_commitments) = blob_kzg_commitments + && let Some(execution_requests) = execution_requests + { + ( + payload.execution_payload(), + blob_kzg_commitments, + execution_requests, + ) + } else { + return Err(BlockProductionError::MissingKzgCommitment( + "No KZG commitments from the payload".to_owned(), + )); + } } - } - // TODO(gloas) we should never receive a blinded response. - // Should return some type of `Unexpected` error variant as this should never happen - // in the V4 block production flow - BlockProposalContentsType::Blinded(_) => { - return Err(BlockProductionError::GloasNotImplemented); - } - }; + // TODO(gloas) we should never receive a blinded response. + // Should return some type of `Unexpected` error variant as this should never happen + // in the V4 block production flow + BlockProposalContentsType::Blinded(_) => { + return Err(BlockProductionError::GloasNotImplemented); + } + }; - let state_root = state_root_opt.ok_or_else(|| { - BlockProductionError::MissingStateRoot - })?; + let state_root = state_root_opt.ok_or_else(|| BlockProductionError::MissingStateRoot)?; + + // TODO(gloas) this is just a dummy error variant for now + let execution_payload_gloas = execution_payload + .as_gloas() + .map_err(|_| BlockProductionError::GloasNotImplemented)? + .to_owned(); let bid = ExecutionPayloadBid:: { parent_block_hash: state.latest_block_hash()?.to_owned(), @@ -124,6 +149,15 @@ impl BeaconChain { blob_kzg_commitments, }; + // Store payload data for envelope construction after block is created + let payload_data = ExecutionPayloadData { + payload: execution_payload_gloas, + execution_requests, + builder_index, + slot: produce_at_slot, + state_root, + }; + // TODO(gloas) this is only local building // we'll need to implement builder signature for the trustless path Ok(( @@ -134,6 +168,9 @@ impl BeaconChain { .map_err(|_| BlockProductionError::GloasNotImplemented)?, }, state, + // Local building always returns payload data. + // Trustless building would return None here. + Some(payload_data), )) } } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4ddd075bb5..78385cd226 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -44,6 +44,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs new file mode 100644 index 0000000000..353830f175 --- /dev/null +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -0,0 +1,150 @@ +//! Provides the `PendingPayloadEnvelopes` cache for storing execution payload envelopes +//! that have been produced during local block production but not yet imported to fork choice. +//! +//! For local building, the envelope is created during block production. +//! This cache holds the envelopes temporarily until the proposer can sign and publish the payload. + +use std::collections::HashMap; +use types::{EthSpec, ExecutionPayloadEnvelope, Hash256, Slot}; + +/// Cache for pending execution payload envelopes awaiting publishing. +/// +/// Envelopes are keyed by beacon block root and pruned based on slot age. +pub struct PendingPayloadEnvelopes { + /// Maximum number of slots to keep envelopes before pruning. + max_slot_age: u64, + /// The envelopes, keyed by beacon block root. + envelopes: HashMap>, +} + +impl Default for PendingPayloadEnvelopes { + fn default() -> Self { + Self::new(Self::DEFAULT_MAX_SLOT_AGE) + } +} + +impl PendingPayloadEnvelopes { + /// Default maximum slot age before pruning (2 slots). + pub const DEFAULT_MAX_SLOT_AGE: u64 = 2; + + /// Create a new cache with the specified maximum slot age. + pub fn new(max_slot_age: u64) -> Self { + Self { + max_slot_age, + envelopes: HashMap::new(), + } + } + + /// Insert a pending envelope into the cache. + pub fn insert(&mut self, block_root: Hash256, envelope: ExecutionPayloadEnvelope) { + self.envelopes.insert(block_root, envelope); + } + + /// Get a pending envelope by block root. + pub fn get(&self, block_root: &Hash256) -> Option<&ExecutionPayloadEnvelope> { + self.envelopes.get(block_root) + } + + /// Remove and return a pending envelope by block root. + pub fn remove(&mut self, block_root: &Hash256) -> Option> { + self.envelopes.remove(block_root) + } + + /// Check if an envelope exists for the given block root. + pub fn contains(&self, block_root: &Hash256) -> bool { + self.envelopes.contains_key(block_root) + } + + /// Prune envelopes older than `current_slot - max_slot_age`. + /// + /// This removes stale envelopes from blocks that were never imported. + pub fn prune(&mut self, current_slot: Slot) { + let min_slot = current_slot.saturating_sub(self.max_slot_age); + self.envelopes + .retain(|_, envelope| envelope.slot >= min_slot); + } + + /// Returns the number of pending envelopes in the cache. + pub fn len(&self) -> usize { + self.envelopes.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.envelopes.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::{ExecutionPayloadGloas, ExecutionRequests, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn make_envelope(slot: Slot, block_root: Hash256) -> ExecutionPayloadEnvelope { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: block_root, + slot, + state_root: Hash256::ZERO, + } + } + + #[test] + fn insert_and_get() { + let mut cache = PendingPayloadEnvelopes::::default(); + let block_root = Hash256::repeat_byte(1); + let envelope = make_envelope(Slot::new(1), block_root); + + assert!(!cache.contains(&block_root)); + assert_eq!(cache.len(), 0); + + cache.insert(block_root, envelope.clone()); + + assert!(cache.contains(&block_root)); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(&block_root), Some(&envelope)); + } + + #[test] + fn remove() { + let mut cache = PendingPayloadEnvelopes::::default(); + let block_root = Hash256::repeat_byte(1); + let envelope = make_envelope(Slot::new(1), block_root); + + cache.insert(block_root, envelope.clone()); + assert!(cache.contains(&block_root)); + + let removed = cache.remove(&block_root); + assert_eq!(removed, Some(envelope)); + assert!(!cache.contains(&block_root)); + assert_eq!(cache.len(), 0); + } + + #[test] + fn prune_old_envelopes() { + let mut cache = PendingPayloadEnvelopes::::new(2); + + // Insert envelope at slot 5 + let block_root_1 = Hash256::repeat_byte(1); + let envelope_1 = make_envelope(Slot::new(5), block_root_1); + cache.insert(block_root_1, envelope_1); + + // Insert envelope at slot 10 + let block_root_2 = Hash256::repeat_byte(2); + let envelope_2 = make_envelope(Slot::new(10), block_root_2); + cache.insert(block_root_2, envelope_2); + + assert_eq!(cache.len(), 2); + + // Prune at slot 10 with max_slot_age=2, should keep slots >= 8 + cache.prune(Slot::new(10)); + + assert_eq!(cache.len(), 1); + assert!(!cache.contains(&block_root_1)); // slot 5 < 8, pruned + assert!(cache.contains(&block_root_2)); // slot 10 >= 8, kept + } +} diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 8fdf57fe1b..c520a608a9 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -140,7 +140,8 @@ pub fn build_response_v4( .to_ref() .fork_name(&chain.spec) .map_err(inconsistent_fork_rejection)?; - let consensus_block_value_wei = Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); + let consensus_block_value_wei = + Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); match accept_header { Some(api_types::Accept::Ssz) => Response::builder() diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 126a9e472f..b82905be52 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,4 +1,6 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4}; +use crate::produce_block::{ + produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4, +}; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 825d8ed8ba..2e5ce12323 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3799,6 +3799,19 @@ impl ApiTester { ); assert!(!metadata.consensus_block_value.is_zero()); + // Verify that the execution payload envelope is cached for local building. + // The envelope is stored in the pending cache until publishing. + let block_root = block.tree_hash_root(); + let envelope = self + .chain + .pending_payload_envelopes + .read() + .get(&block_root) + .cloned() + .expect("envelope should exist in pending cache for local building"); + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + // Sign and publish the block let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); let signed_block_request = diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 267173b527..c9672f2221 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2452,7 +2452,13 @@ impl BeaconNodeHttpClient { graffiti: Option<&Graffiti>, builder_booster_factor: Option, graffiti_policy: Option, - ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + ) -> Result< + ( + ForkVersionedResponse, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { self.get_validator_blocks_v4_modular( slot, randao_reveal, @@ -2473,7 +2479,13 @@ impl BeaconNodeHttpClient { skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, graffiti_policy: Option, - ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + ) -> Result< + ( + ForkVersionedResponse, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { let path = self .get_validator_blocks_v4_path( slot, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index fa211b9d77..149b342a8c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1742,7 +1742,6 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } - /// Metadata about a `ProduceBlockV3Response` which is returned in the body & headers. #[derive(Debug, Deserialize, Serialize)] pub struct ProduceBlockV4Metadata {