From 7cf4eb039618ae154ba45a103cab94242a3495d9 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 16:13:07 -0800 Subject: [PATCH] Add new block production endpoint --- beacon_node/beacon_chain/src/beacon_block.rs | 205 +++++++++++------- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/errors.rs | 1 + .../beacon_chain/src/execution_payload_bid.rs | 159 ++++++-------- beacon_node/http_api/src/produce_block.rs | 75 +++++++ beacon_node/http_api/src/validator/mod.rs | 51 ++++- beacon_node/http_api/src/version.rs | 1 + beacon_node/http_api/tests/tests.rs | 177 +++++++++++++++ common/eth2/src/lib.rs | 170 +++++++++++++++ common/eth2/src/types.rs | 36 +++ .../validator_services/src/block_service.rs | 198 +++++++++++------ 11 files changed, 844 insertions(+), 231 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index c9c9d15f57..1b53f229dc 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; +use std::u64; use bls::Signature; -use execution_layer::BuilderParams; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::get_attesting_indices_from_state; @@ -22,14 +22,12 @@ use types::{ SyncAggregate, }; -use crate::BeaconBlockResponse; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, - ProduceBlockVerification, graffiti_calculator::GraffitiSettings, + BeaconChain, BeaconChainTypes, BlockProductionError, ProduceBlockVerification, + graffiti_calculator::GraffitiSettings, metrics, }; pub struct PartialBeaconBlock { - state: BeaconState, slot: Slot, proposer_index: u64, parent_root: Hash256, @@ -46,66 +44,136 @@ pub struct PartialBeaconBlock { bls_to_execution_changes: Vec, } +// We'll need to add that once we include trusted/trustless bids impl BeaconChain { - pub async fn produce_block_on_bid( + pub async fn produce_block_with_verification_gloas( + self: &Arc, + randao_reveal: Signature, + slot: Slot, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + _builder_boost_factor: Option, + ) -> Result< + ( + BeaconBlock>, + BeaconState, + u64, + ), + BlockProductionError, + > { + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); + let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); + // Part 1/2 (blocking) + // + // Load the parent state from disk. + let chain = self.clone(); + let span = Span::current(); + let (state, state_root_opt) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "load_state_for_block_production").entered(); + chain.load_state_for_block_production(slot) + }, + "load_state_for_block_production", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/2 (async, with some blocking components) + // + // Produce the block upon the state + self.produce_block_on_state_gloas( + state, + state_root_opt, + slot, + randao_reveal, + graffiti_settings, + verification, + ) + .await + } + + // TODO(gloas) need to implement builder boost factor logic + pub async fn produce_block_on_state_gloas( self: &Arc, state: BeaconState, - execution_payload_bid: SignedExecutionPayloadBid, state_root_opt: Option, produce_at_slot: Slot, randao_reveal: Signature, graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, - builder_boost_factor: Option, - ) -> Result>, BlockProductionError> { + ) -> Result< + ( + BeaconBlock>, + BeaconState, + u64, + ), + BlockProductionError, + > { + // Part 1/3 (blocking) + // + // Perform the state advance and block-packing functions. let chain = self.clone(); let graffiti = self .graffiti_calculator .get_graffiti(graffiti_settings) .await; let span = Span::current(); - let mut partial_beacon_block = self + let (partial_beacon_block, state) = self .task_executor .spawn_blocking_handle( move || { let _guard = - debug_span!(parent: span, "produce_partial_beacon_block").entered(); + debug_span!(parent: span, "produce_partial_beacon_block_gloas").entered(); chain.produce_partial_beacon_block_gloas( state, state_root_opt, produce_at_slot, randao_reveal, graffiti, - builder_boost_factor, ) }, - "produce_partial_beacon_block", + "produce_partial_beacon_block_gloas", ) .ok_or(BlockProductionError::ShuttingDown)? .await .map_err(BlockProductionError::TokioJoin)??; + // Part 2/3 (async) + // + // 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 + .clone() + .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, 0, u64::MAX) + .await?; + + // Part 3/3 (blocking) + // + // Complete the block with the execution payload bid. let chain = self.clone(); let span = Span::current(); - let beacon_block_response = self - .task_executor + self.task_executor .spawn_blocking_handle( move || { let _guard = - debug_span!(parent: span, "complete_partial_beacon_block").entered(); + debug_span!(parent: span, "complete_partial_beacon_block_gloas").entered(); chain.complete_partial_beacon_block_gloas( partial_beacon_block, execution_payload_bid, + state, verification, ) }, - "complete_partial_beacon_block", + "complete_partial_beacon_block_gloas", ) .ok_or(BlockProductionError::ShuttingDown)? .await - .map_err(BlockProductionError::TokioJoin)??; - - todo!() + .map_err(BlockProductionError::TokioJoin)? } #[allow(clippy::too_many_arguments)] @@ -116,8 +184,8 @@ impl BeaconChain { produce_at_slot: Slot, randao_reveal: Signature, graffiti: Graffiti, - builder_boost_factor: Option, - ) -> Result, BlockProductionError> { + ) -> Result<(PartialBeaconBlock, BeaconState), BlockProductionError> + { // It is invalid to try to produce a block using a state from a future slot. if state.slot() > produce_at_slot { return Err(BlockProductionError::StateSlotTooHigh { @@ -148,22 +216,6 @@ impl BeaconChain { let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; - let pubkey = state - .validators() - .get(proposer_index as usize) - .map(|v| v.pubkey) - .ok_or(BlockProductionError::BeaconChain(Box::new( - BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), - )))?; - - let builder_params = BuilderParams { - pubkey, - slot: state.slot(), - chain_health: self - .is_healthy(&parent_root) - .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, - }; - let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = self.op_pool.get_slashings_and_exits(&state, &self.spec); @@ -339,30 +391,33 @@ impl BeaconChain { Some(sync_aggregate) }; - Ok(PartialBeaconBlock { + Ok(( + PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + // TODO(gloas) need to implement payload attestations + payload_attestations: vec![], + bls_to_execution_changes, + }, state, - slot, - proposer_index, - parent_root, - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - // TODO(gloas) need to implement payload attestations - payload_attestations: vec![], - bls_to_execution_changes, - }) + )) } fn complete_partial_beacon_block_gloas( &self, partial_beacon_block: PartialBeaconBlock, signed_execution_payload_bid: SignedExecutionPayloadBid, + mut state: BeaconState, verification: ProduceBlockVerification, ) -> Result< ( @@ -373,7 +428,6 @@ impl BeaconChain { BlockProductionError, > { let PartialBeaconBlock { - mut state, slot, proposer_index, parent_root, @@ -392,36 +446,39 @@ impl BeaconChain { let beacon_block = match &state { BeaconState::Base(_) => { - ( - // TODO(gloas) this should be an error - todo!() - ) + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Altair(_) => { - ( - // TODO(gloas) this should be an error - todo!() - ) + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Bellatrix(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Capella(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Deneb(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Electra(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Fulu(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { slot, diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ec79153785..a12600141f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4601,7 +4601,7 @@ impl BeaconChain { /// Load a beacon state from the database for block production. This is a long-running process /// that should not be performed in an `async` context. - fn load_state_for_block_production( + pub fn load_state_for_block_production( self: &Arc, slot: Slot, ) -> Result<(BeaconState, Option), BlockProductionError> { diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 816e75fd24..bd6c13e364 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -310,6 +310,7 @@ pub enum BlockProductionError { MissingSyncAggregate, MissingExecutionPayload, MissingKzgCommitment(String), + MissingStateRoot, TokioJoin(JoinError), BeaconChain(Box), InvalidPayloadFork, diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs index bffbdcd16d..752337a7e8 100644 --- a/beacon_node/beacon_chain/src/execution_payload_bid.rs +++ b/beacon_node/beacon_chain/src/execution_payload_bid.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; +use std::{sync::Arc, u64}; +use bls::Signature; use execution_layer::{BlockProposalContentsType, BuilderParams}; -use ssz_types::VariableList; -use state_processing::state_advance::complete_state_advance; use tracing::instrument; use types::{ - Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayload, - ExecutionPayloadBid, Hash256, ProposerPreferences, Slot, + Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, Hash256, + SignedExecutionPayloadBid, Slot, }; use crate::{ @@ -15,34 +14,30 @@ use crate::{ }; 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. #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( - self: &Arc, - mut state: BeaconState, - state_root: Hash256, - execution_payload: ExecutionPayload, + self: Arc, + state: BeaconState, + state_root_opt: Option, produce_at_slot: Slot, - proposer_preferences: Option, + bid_value: u64, builder_index: BuilderIndex, - value: u64, - ) -> Result, BlockProductionError> { - // It is invalid to try to produce a block using a state from a future slot. - if state.slot() > produce_at_slot { - return Err(BlockProductionError::StateSlotTooHigh { - produce_at_slot, - state_slot: state.slot(), - }); - } - - // TODO(gloas) add sanity check on value + ) -> Result< + ( + SignedExecutionPayloadBid, + BeaconState, + ), + BlockProductionError, + > { + // TODO(gloas) For non local building, add sanity check on value // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. // TODO(gloas) add metrics for execution payload bid production - // Ensure the state has performed a complete transition into the required slot. - complete_state_advance(&mut state, Some(state_root), produce_at_slot, &self.spec)?; - let parent_root = if state.slot() > 0 { *state .get_block_root(state.slot() - 1) @@ -82,79 +77,63 @@ impl BeaconChain { BlockProductionVersion::V3, )?; - let block_contents_type_option = Some( - prepare_payload_handle - .await - .map_err(BlockProductionError::TokioJoin)? - .ok_or(BlockProductionError::ShuttingDown)??, - ); + let block_contents_type = prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??; - let blob_kzg_commitments = if let Some(block_contents_type) = block_contents_type_option { - 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) = match block_contents_type { + BlockProposalContentsType::Full(block_proposal_contents) => { + let blob_kzg_commitments = + block_proposal_contents.blob_kzg_commitments().cloned(); - if let Some(blob_kzg_commitments) = blob_kzg_commitments { - blob_kzg_commitments - } 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); + 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(), + )); } } - } else { - todo!() + // 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 bid = if let Some(proposer_preferences) = proposer_preferences - && proposer_preferences.proposal_slot == produce_at_slot - { - // Trustless bid - ExecutionPayloadBid:: { - parent_block_hash: state.latest_block_hash()?.to_owned(), - parent_block_root: state.get_latest_block_root(state_root), - block_hash: execution_payload.block_hash(), - prev_randao: execution_payload.prev_randao(), - fee_recipient: proposer_preferences.fee_recipient, - // TODO(gloas) payload construction should factor in the proposers gas limit preferences - gas_limit: execution_payload.gas_limit(), - builder_index, - slot: produce_at_slot, - value, - execution_payment: 0, - blob_kzg_commitments, - } - } else if builder_index == u64::MAX { - // Local bid - ExecutionPayloadBid:: { - parent_block_hash: state.latest_block_hash()?.to_owned(), - parent_block_root: state.get_latest_block_root(state_root), - block_hash: execution_payload.block_hash(), - prev_randao: execution_payload.prev_randao(), - fee_recipient: Address::ZERO, - gas_limit: execution_payload.gas_limit(), - builder_index, - slot: produce_at_slot, - value, - execution_payment: 0, - blob_kzg_commitments, - } - } else { - // No proposer preferences and this isn't local building - // TODO(gloas) this should return a specific error type - // i.e if proposer prefs are missing and its a trustless bid - // return an error that communicates that. - return Err(BlockProductionError::GloasNotImplemented); + let state_root = state_root_opt.ok_or_else(|| { + BlockProductionError::MissingStateRoot + })?; + + let bid = ExecutionPayloadBid:: { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: execution_payload.block_hash(), + prev_randao: execution_payload.prev_randao(), + fee_recipient: Address::ZERO, + gas_limit: execution_payload.gas_limit(), + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: 0, + blob_kzg_commitments, }; - Ok(bid) + // TODO(gloas) this is only local building + // we'll need to implement builder signature for the trustless path + Ok(( + SignedExecutionPayloadBid { + message: bid, + // TODO(gloas) return better error variant here + signature: Signature::infinity() + .map_err(|_| BlockProductionError::GloasNotImplemented)?, + }, + state, + )) } } diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 6a549c91ef..8fdf57fe1b 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -43,6 +43,49 @@ pub fn get_randao_verification( Ok(randao_verification) } +#[instrument( + name = "lh_produce_block_v4", + skip_all, + fields(%slot) +)] +pub async fn produce_block_v4( + accept_header: Option, + chain: Arc>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let builder_boost_factor = if query.builder_boost_factor == Some(DEFAULT_BOOST_FACTOR) { + None + } else { + query.builder_boost_factor + }; + + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + + let (block, _state, consensus_block_value) = chain + .produce_block_with_verification_gloas( + randao_reveal, + slot, + graffiti_settings, + randao_verification, + builder_boost_factor, + ) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) + })?; + + build_response_v4(chain, block, consensus_block_value, accept_header) +} + #[instrument( name = "lh_produce_block_v3", skip_all, @@ -87,6 +130,38 @@ pub async fn produce_block_v3( build_response_v3(chain, block_response_type, accept_header) } +pub fn build_response_v4( + chain: Arc>, + block: BeaconBlock>, + consensus_block_value: u64, + accept_header: Option, +) -> Result, warp::Rejection> { + let fork_name = block + .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); + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(block.as_ssz_bytes().into()) + .map(|res: Response| add_ssz_content_type_header(res)) + .map(|res: Response| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)) + .map_err(|e| -> warp::Rejection { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => Ok(warp::reply::json(&beacon_response( + ResponseIncludesVersion::Yes(fork_name), + block, + )) + .into_response()) + .map(|res| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)), + } +} + pub fn build_response_v3( chain: Arc>, block_response: BeaconBlockResponseWrapper, diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index b1ab4c648a..126a9e472f 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,10 +1,10 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +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, ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message, }; -use crate::version::V3; +use crate::version::{V3, V4}; use crate::{StateId, attester_duties, proposer_duties, sync_committees}; use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; @@ -316,7 +316,11 @@ pub fn get_validator_blocks( not_synced_filter?; - if endpoint_version == V3 { + // Use V4 block production for Gloas fork + let fork_name = chain.spec.fork_name_at_slot::(slot); + if fork_name.gloas_enabled() { + produce_block_v4(accept_header, chain, slot, query).await + } else if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { produce_block_v2(accept_header, chain, slot, query).await @@ -327,6 +331,47 @@ pub fn get_validator_blocks( .boxed() } +// GET validator/execution_payload_bid/ +pub fn get_validator_execution_payload_bid( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + accept_header: Option, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!( + ?slot, + "Execution paylaod bid production request from HTTP API" + ); + + not_synced_filter?; + + todo!() + }) + }, + ) + .boxed() +} + // POST validator/liveness/{epoch} pub fn post_validator_liveness_epoch( eth_v1: EthV1Filter, diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs index 371064c886..e1ba628032 100644 --- a/beacon_node/http_api/src/version.rs +++ b/beacon_node/http_api/src/version.rs @@ -14,6 +14,7 @@ use warp::reply::{self, Reply, Response}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); +pub const V4: EndpointVersion = EndpointVersion(4); #[derive(Debug, PartialEq, Clone, Serialize)] pub enum ResponseIncludesVersion { diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bef9fe6acd..825d8ed8ba 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3736,6 +3736,167 @@ impl ApiTester { self } + /// Test V4 block production (JSON). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + // Skip if not in Gloas fork yet + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = self.chain.spec.get_domain( + epoch, + Domain::Randao, + &fork, + genesis_validators_root, + ); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + let (response, metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + let block = response.data; + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + // Sign and publish the block + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + + /// Test V4 block production (SSZ). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4_ssz(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return self; + } + + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + // Skip if not in Gloas fork yet + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = self.chain.spec.get_domain( + epoch, + Domain::Randao, + &fork, + genesis_validators_root, + ); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + let (block, metadata) = self + .client + .get_validator_blocks_v4_ssz::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + // Sign and publish the block + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + + self.client + .post_beacon_blocks_v2_ssz(&signed_block_request, None) + .await + .unwrap(); + + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -7469,6 +7630,22 @@ async fn block_production_v3_ssz_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4_ssz() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new().await.test_blinded_block_production().await; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 8746e3c063..267173b527 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -50,6 +50,7 @@ use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); +pub const V4: EndpointVersion = EndpointVersion(4); pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; pub const EXECUTION_PAYLOAD_BLINDED_HEADER: &str = "Eth-Execution-Payload-Blinded"; @@ -2399,6 +2400,175 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } + /// returns `GET v4/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_v4_path( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result { + let mut path = self.eth_path(V4)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("blocks") + .push(&slot.to_string()); + + path.query_pairs_mut() + .append_pair("randao_reveal", &randao_reveal.to_string()); + + if let Some(graffiti) = graffiti { + path.query_pairs_mut() + .append_pair("graffiti", &graffiti.to_string()); + } + + if skip_randao_verification == SkipRandaoVerification::Yes { + path.query_pairs_mut() + .append_pair("skip_randao_verification", ""); + } + + if let Some(builder_booster_factor) = builder_booster_factor { + path.query_pairs_mut() + .append_pair("builder_boost_factor", &builder_booster_factor.to_string()); + } + + if let Some(GraffitiPolicy::AppendClientVersions) = graffiti_policy { + path.query_pairs_mut() + .append_pair("graffiti_policy", "AppendClientVersions"); + } + + Ok(path) + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4_modular( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_result = self + .get_response_with_response_headers( + path, + Accept::Json, + self.timeouts.get_validator_block, + |response, headers| async move { + let header_metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let block_response = response + .json::, ProduceBlockV4Metadata>>() + .await?; + Ok((block_response, header_metadata)) + }, + ) + .await?; + + opt_result.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(BeaconBlock, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular_ssz::( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_modular_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(BeaconBlock, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_response = self + .get_response_with_response_headers( + path, + Accept::Ssz, + self.timeouts.get_validator_block, + |response, headers| async move { + let metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let response_bytes = response.bytes().await?; + + let block = BeaconBlock::from_ssz_bytes_for_fork( + &response_bytes, + metadata.consensus_version, + ) + .map_err(Error::InvalidSsz)?; + + Ok((block, metadata)) + }, + ) + .await?; + + opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c1572ca354..fa211b9d77 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1742,6 +1742,21 @@ 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 { + // The consensus version is serialized & deserialized by `ForkVersionedResponse`. + #[serde( + skip_serializing, + skip_deserializing, + default = "dummy_consensus_version" + )] + pub consensus_version: ForkName, + #[serde(with = "serde_utils::u256_dec")] + pub consensus_block_value: Uint256, +} + impl FullBlockContents { pub fn new(block: BeaconBlock, blob_data: Option<(KzgProofs, BlobsList)>) -> Self { match blob_data { @@ -1898,6 +1913,27 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { } } +impl TryFrom<&HeaderMap> for ProduceBlockV4Metadata { + type Error = String; + + fn try_from(headers: &HeaderMap) -> Result { + let consensus_version = parse_required_header(headers, CONSENSUS_VERSION_HEADER, |s| { + s.parse::() + .map_err(|e| format!("invalid {CONSENSUS_VERSION_HEADER}: {e:?}")) + })?; + let consensus_block_value = + parse_required_header(headers, CONSENSUS_BLOCK_VALUE_HEADER, |s| { + Uint256::from_str_radix(s, 10) + .map_err(|e| format!("invalid {CONSENSUS_BLOCK_VALUE_HEADER}: {e:?}")) + })?; + + Ok(ProduceBlockV4Metadata { + consensus_version, + consensus_block_value, + }) + } +} + /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. #[derive(Clone, Debug, PartialEq, Encode, Serialize)] #[serde(untagged)] diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 625f8db7cb..dd1b938baf 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -459,73 +459,145 @@ impl BlockService { info!(slot = slot.as_u64(), "Requesting unsigned block"); - // Request an SSZ block from all beacon nodes in order, returning on the first successful response. - // If all nodes fail, run a second pass falling back to JSON. - // - // Proposer nodes will always be tried last during each pass since it's likely that they don't have a - // great view of attestations on the network. - let ssz_block_response = proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - beacon_node - .get_validator_blocks_v3_ssz::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - }) - .await; + // Check if Gloas fork is active at this slot + let fork_name = self_ref.chain_spec.fork_name_at_slot::(slot); - let block_response = match ssz_block_response { - Ok((ssz_block_response, _metadata)) => ssz_block_response, - Err(e) => { - warn!( - slot = slot.as_u64(), - error = %e, - "SSZ block production failed, falling back to JSON" - ); + let (block_proposer, unsigned_block) = if fork_name.gloas_enabled() { + // Use V4 block production for Gloas + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v4_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; - proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - let (json_block_response, _metadata) = beacon_node - .get_validator_blocks_v3::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })?; + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ V4 block production failed, falling back to JSON" + ); - Ok(json_block_response.data) - }) - .await - .map_err(BlockError::from)? - } - }; + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v4::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; - let (block_proposer, unsigned_block) = match block_response { - eth2::types::ProduceBlockV3Response::Full(block) => { - (block.block().proposer_index(), UnsignedBlock::Full(block)) - } - eth2::types::ProduceBlockV3Response::Blinded(block) => { - (block.proposer_index(), UnsignedBlock::Blinded(block)) + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + // Gloas blocks don't have blobs (they're in the execution layer) + let block_contents = eth2::types::FullBlockContents::Block(block_response); + ( + block_contents.block().proposer_index(), + UnsignedBlock::Full(block_contents), + ) + } else { + // Use V3 block production for pre-Gloas forks + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + // + // Proposer nodes will always be tried last during each pass since it's likely that they don't have a + // great view of attestations on the network. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v3_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; + + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ block production failed, falling back to JSON" + ); + + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v3::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; + + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + match block_response { + eth2::types::ProduceBlockV3Response::Full(block) => { + (block.block().proposer_index(), UnsignedBlock::Full(block)) + } + eth2::types::ProduceBlockV3Response::Blinded(block) => { + (block.proposer_index(), UnsignedBlock::Blinded(block)) + } } };