diff --git a/Cargo.lock b/Cargo.lock index c59a3b56..68be97df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -994,10 +994,13 @@ dependencies = [ "bls", "clock", "derive_more 1.0.0", + "enum-iterator", "helper_functions", "hex-literal", + "http_api_utils", "itertools 0.13.0", "log", + "mime", "prometheus_metrics", "reqwest", "serde", @@ -3564,6 +3567,7 @@ dependencies = [ "fork_choice_store", "helper_functions", "hex-literal", + "http 1.2.0", "http-body-util", "itertools 0.13.0", "log", diff --git a/builder_api/Cargo.toml b/builder_api/Cargo.toml index acd4f9cc..8a710bfe 100644 --- a/builder_api/Cargo.toml +++ b/builder_api/Cargo.toml @@ -11,10 +11,13 @@ anyhow = { workspace = true } bls = { workspace = true } clock = { workspace = true } derive_more = { workspace = true } +enum-iterator = { workspace = true } helper_functions = { workspace = true } hex-literal = { workspace = true } +http_api_utils = { workspace = true } itertools = { workspace = true } log = { workspace = true } +mime = { workspace = true } prometheus_metrics = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/builder_api/src/api.rs b/builder_api/src/api.rs index 11077450..432314dd 100644 --- a/builder_api/src/api.rs +++ b/builder_api/src/api.rs @@ -5,11 +5,16 @@ use anyhow::{bail, ensure, Result}; use bls::PublicKeyBytes; use derive_more::Constructor; use helper_functions::{misc, signing::SignForAllForks}; +use http_api_utils::ETH_CONSENSUS_VERSION; use itertools::Itertools as _; use log::{debug, info}; +use mime::{APPLICATION_JSON, APPLICATION_OCTET_STREAM}; use prometheus_metrics::Metrics; -use reqwest::{Client, Response, StatusCode}; -use ssz::SszHash as _; +use reqwest::{ + header::{ACCEPT, CONTENT_TYPE}, + Client, RequestBuilder, Response, StatusCode, +}; +use ssz::{SszHash as _, SszRead as _, SszWrite as _}; use thiserror::Error; use typenum::Unsigned as _; use types::{ @@ -29,7 +34,7 @@ use crate::{ combined::{ExecutionPayloadAndBlobsBundle, SignedBuilderBid}, consts::BUILDER_PROPOSAL_DELAY_TOLERANCE, unphased::containers::SignedValidatorRegistrationV1, - BuilderConfig, + BuilderApiFormat, BuilderConfig, }; const REQUEST_TIMEOUT: Duration = Duration::from_secs(BUILDER_PROPOSAL_DELAY_TOLERANCE); @@ -157,11 +162,10 @@ impl Api { debug!("getting execution payload header from {url}"); let response = self - .client - .get(url.into_url()) - .timeout(REQUEST_TIMEOUT) + .request_with_accept_header(self.client.get(url.into_url()).timeout(REQUEST_TIMEOUT)) .send() .await?; + let response = handle_error(response).await?; if response.status() == StatusCode::NO_CONTENT { @@ -169,7 +173,15 @@ impl Api { return Ok(None); } - let builder_bid = response.json::>().await?; + let builder_bid = match self.config.builder_api_format { + BuilderApiFormat::Json => response.json().await?, + BuilderApiFormat::Ssz => { + let phase = http_api_utils::extract_phase_from_headers(response.headers())?; + let bytes = response.bytes().await?; + + SignedBuilderBid::

::from_ssz(&phase, &bytes)? + } + }; debug!("get_execution_payload_header response: {builder_bid:?}"); @@ -230,18 +242,33 @@ impl Api { let block_root = block.message().hash_tree_root(); let slot = block.message().slot(); - let response = self - .client - .post(url.into_url()) - .json(block) - .timeout(remaining_time) - .send() - .await?; + let request = self.request_with_accept_header( + self.client + .post(url.into_url()) + .timeout(remaining_time) + .header(ETH_CONSENSUS_VERSION, block.phase().as_ref()), + ); + + let request = match self.config.builder_api_format { + BuilderApiFormat::Json => request.json(block), + BuilderApiFormat::Ssz => request + .header(CONTENT_TYPE, APPLICATION_OCTET_STREAM.as_ref()) + .body(block.to_ssz()?), + }; + let response = request.send().await?; let response = handle_error(response).await?; - let response: WithBlobsAndMev, P> = response - .json::>() - .await? + + let response: WithBlobsAndMev, P> = + match self.config.builder_api_format { + BuilderApiFormat::Json => response.json().await?, + BuilderApiFormat::Ssz => { + let phase = http_api_utils::extract_phase_from_headers(response.headers())?; + let bytes = response.bytes().await?; + + ExecutionPayloadAndBlobsBundle::

::from_ssz(&phase, &bytes)? + } + } .into(); let execution_payload = &response.value; @@ -266,6 +293,15 @@ impl Api { Ok(response) } + fn request_with_accept_header(&self, request_builder: RequestBuilder) -> RequestBuilder { + let accept_header = match self.config.builder_api_format { + BuilderApiFormat::Json => APPLICATION_JSON, + BuilderApiFormat::Ssz => APPLICATION_OCTET_STREAM, + }; + + request_builder.header(ACCEPT, accept_header.as_ref()) + } + fn url(&self, path: &str) -> Result { self.config.builder_api_url.join(path).map_err(Into::into) } @@ -345,6 +381,7 @@ mod tests { ) -> Result<(), BuilderApiError> { let api = BuilderApi::new( BuilderConfig { + builder_api_format: BuilderApiFormat::Json, builder_api_url: "http://localhost" .parse() .expect("http://localhost should be a valid URL"), diff --git a/builder_api/src/bellatrix/containers.rs b/builder_api/src/bellatrix/containers.rs index a2ee59c2..78984707 100644 --- a/builder_api/src/bellatrix/containers.rs +++ b/builder_api/src/bellatrix/containers.rs @@ -12,15 +12,16 @@ use types::{ #[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] -#[ssz(derive_read = false, derive_size = false, derive_write = false)] +#[ssz(derive_write = false)] pub struct BuilderBid { pub header: Box>, pub value: Wei, pub pubkey: PublicKeyBytes, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct SignedBuilderBid { pub message: BuilderBid

, pub signature: SignatureBytes, diff --git a/builder_api/src/capella/containers.rs b/builder_api/src/capella/containers.rs index cf99d773..7a255c7b 100644 --- a/builder_api/src/capella/containers.rs +++ b/builder_api/src/capella/containers.rs @@ -11,15 +11,16 @@ use types::{ #[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] -#[ssz(derive_read = false, derive_size = false, derive_write = false)] +#[ssz(derive_write = false)] pub struct BuilderBid { pub header: Box>, pub value: Wei, pub pubkey: PublicKeyBytes, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct SignedBuilderBid { pub message: BuilderBid

, pub signature: SignatureBytes, diff --git a/builder_api/src/combined.rs b/builder_api/src/combined.rs index c45d2289..ccb465b4 100644 --- a/builder_api/src/combined.rs +++ b/builder_api/src/combined.rs @@ -1,6 +1,7 @@ use bls::{PublicKeyBytes, SignatureBytes}; +use enum_iterator::Sequence; use serde::Deserialize; -use ssz::ContiguousList; +use ssz::{ContiguousList, ReadError, Size, SszRead, SszReadDefault, SszSize}; use types::{ bellatrix::containers::ExecutionPayload as BellatrixExecutionPayload, capella::containers::ExecutionPayload as CapellaExecutionPayload, @@ -38,6 +39,40 @@ pub enum SignedBuilderBid { Electra(ElectraSignedBuilderBid

), } +impl SszSize for SignedBuilderBid

{ + // The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type. + // Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable. + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 2 }>([ + BellatrixSignedBuilderBid::

::SIZE, + CapellaSignedBuilderBid::

::SIZE, + DenebSignedBuilderBid::

::SIZE, + ElectraSignedBuilderBid::

::SIZE, + ]); +} + +impl SszRead for SignedBuilderBid

{ + fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result { + let block = match phase { + Phase::Phase0 => { + return Err(ReadError::Custom { + message: "signed builder bid is not available in Phase 0", + }); + } + Phase::Altair => { + return Err(ReadError::Custom { + message: "signed builder bid is not available in Altair", + }); + } + Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), + Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), + Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), + }; + + Ok(block) + } +} + impl SignedBuilderBid

{ #[must_use] pub(crate) const fn pubkey(&self) -> PublicKeyBytes { @@ -126,6 +161,39 @@ pub enum ExecutionPayloadAndBlobsBundle { Electra(DenebExecutionPayloadAndBlobsBundle

), } +impl SszSize for ExecutionPayloadAndBlobsBundle

{ + // The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type. + // Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable. + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 3 }>([ + BellatrixExecutionPayload::

::SIZE, + CapellaExecutionPayload::

::SIZE, + DenebExecutionPayloadAndBlobsBundle::

::SIZE, + ]); +} + +impl SszRead for ExecutionPayloadAndBlobsBundle

{ + fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result { + let block = match phase { + Phase::Phase0 => { + return Err(ReadError::Custom { + message: "execution payload and blobs bundle is not available in Phase 0", + }); + } + Phase::Altair => { + return Err(ReadError::Custom { + message: "execution payload and blobs bundle is not available in Altair", + }); + } + Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), + Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), + Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?), + }; + + Ok(block) + } +} + impl From> for WithBlobsAndMev, P> { diff --git a/builder_api/src/config.rs b/builder_api/src/config.rs index 7b15e833..8b32fa0f 100644 --- a/builder_api/src/config.rs +++ b/builder_api/src/config.rs @@ -1,12 +1,20 @@ -use derive_more::Debug; +use derive_more::{Debug, Display, FromStr}; use types::redacting_url::RedactingUrl; pub const DEFAULT_BUILDER_MAX_SKIPPED_SLOTS_PER_EPOCH: u64 = 8; pub const DEFAULT_BUILDER_MAX_SKIPPED_SLOTS: u64 = 3; +#[derive(Clone, Debug, Default, Display, FromStr)] +pub enum BuilderApiFormat { + #[default] + Json, + Ssz, +} + #[expect(clippy::struct_field_names)] #[derive(Clone, Debug)] pub struct Config { + pub builder_api_format: BuilderApiFormat, pub builder_api_url: RedactingUrl, pub builder_disable_checks: bool, pub builder_max_skipped_slots_per_epoch: u64, diff --git a/builder_api/src/deneb/containers.rs b/builder_api/src/deneb/containers.rs index 78e86727..b55088a6 100644 --- a/builder_api/src/deneb/containers.rs +++ b/builder_api/src/deneb/containers.rs @@ -16,6 +16,7 @@ use types::{ #[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct BuilderBid { pub header: Box>, pub blob_kzg_commitments: ContiguousList, @@ -23,23 +24,26 @@ pub struct BuilderBid { pub pubkey: PublicKeyBytes, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct SignedBuilderBid { pub message: BuilderBid

, pub signature: SignatureBytes, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct BlobsBundle { pub commitments: ContiguousList, pub proofs: ContiguousList, pub blobs: ContiguousList, P::MaxBlobsPerBlock>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct ExecutionPayloadAndBlobsBundle { pub execution_payload: ExecutionPayload

, pub blobs_bundle: BlobsBundle

, diff --git a/builder_api/src/electra/containers.rs b/builder_api/src/electra/containers.rs index 25915462..9858eb3a 100644 --- a/builder_api/src/electra/containers.rs +++ b/builder_api/src/electra/containers.rs @@ -14,6 +14,7 @@ use types::{ #[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct BuilderBid { pub header: Box>, pub blob_kzg_commitments: ContiguousList, @@ -22,8 +23,9 @@ pub struct BuilderBid { pub pubkey: PublicKeyBytes, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Ssz)] #[serde(bound = "", deny_unknown_fields)] +#[ssz(derive_write = false)] pub struct SignedBuilderBid { pub message: BuilderBid

, pub signature: SignatureBytes, diff --git a/builder_api/src/lib.rs b/builder_api/src/lib.rs index 11f1beb2..839687a9 100644 --- a/builder_api/src/lib.rs +++ b/builder_api/src/lib.rs @@ -1,7 +1,7 @@ pub use crate::{ api::Api as BuilderApi, config::{ - Config as BuilderConfig, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS, + BuilderApiFormat, Config as BuilderConfig, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS_PER_EPOCH, }, consts::PREFERRED_EXECUTION_GAS_LIMIT, diff --git a/grandine/src/grandine_args.rs b/grandine/src/grandine_args.rs index 912a7838..3ad2c2b1 100644 --- a/grandine/src/grandine_args.rs +++ b/grandine/src/grandine_args.rs @@ -15,8 +15,8 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::{ensure, Result}; use bls::PublicKeyBytes; use builder_api::{ - BuilderConfig, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS_PER_EPOCH, - PREFERRED_EXECUTION_GAS_LIMIT, + BuilderApiFormat, BuilderConfig, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS, + DEFAULT_BUILDER_MAX_SKIPPED_SLOTS_PER_EPOCH, PREFERRED_EXECUTION_GAS_LIMIT, }; use bytesize::ByteSize; use clap::{error::ErrorKind, Args, CommandFactory as _, Error as ClapError, Parser, ValueEnum}; @@ -720,6 +720,10 @@ struct ValidatorOptions { #[clap(long)] keystore_storage_password_file: Option, + /// Data format for communication with the builder API + #[clap(long = "builder-format", default_value_t = BuilderApiFormat::default())] + builder_api_format: BuilderApiFormat, + /// [DEPRECATED] External block builder API URL #[clap(long)] builder_api_url: Option, @@ -946,6 +950,7 @@ impl GrandineArgs { keystore_password_dir, keystore_password_file, keystore_storage_password_file, + builder_api_format, builder_api_url, builder_url, builder_disable_checks, @@ -1227,6 +1232,7 @@ impl GrandineArgs { }; let builder_config = builder_url.map(|url| BuilderConfig { + builder_api_format, builder_api_url: url, builder_disable_checks, builder_max_skipped_slots, diff --git a/grandine/src/grandine_config.rs b/grandine/src/grandine_config.rs index 3d42db34..905c2c70 100644 --- a/grandine/src/grandine_config.rs +++ b/grandine/src/grandine_config.rs @@ -150,8 +150,8 @@ impl GrandineConfig { if let Some(builder_config) = builder_config { info!( - "using external block builder (API URL: {})", - builder_config.builder_api_url, + "using external block builder (API URL: {}, format: {})", + builder_config.builder_api_url, builder_config.builder_api_format, ); } diff --git a/http_api/src/error.rs b/http_api/src/error.rs index 48313fc1..c40107d8 100644 --- a/http_api/src/error.rs +++ b/http_api/src/error.rs @@ -11,7 +11,7 @@ use axum::{ use axum_extra::extract::QueryRejection; use bls::SignatureBytes; use futures::channel::oneshot::Canceled; -use http_api_utils::ApiError; +use http_api_utils::{ApiError, PhaseHeaderError}; use serde::{Serialize, Serializer}; use ssz::H256; use thiserror::Error; @@ -28,6 +28,8 @@ pub enum Error { BlockNotFound, #[error(transparent)] Canceled(#[from] Canceled), + #[error(transparent)] + InvalidRequestConsensusHeader(#[from] PhaseHeaderError), #[error( "committees_at_slot ({requested}) does not match \ the expected number of committees ({computed})" @@ -73,8 +75,6 @@ pub enum Error { InvalidContributionAndProofs(Vec), #[error("invalid epoch")] InvalidEpoch(#[source] AnyhowError), - #[error("invalid eth-consensus-version header")] - InvalidEthConsensusVersionHeader(#[source] AnyhowError), #[error("invalid JSON body")] InvalidJsonBody(#[source] JsonRejection), #[error("invalid peer ID")] @@ -106,8 +106,6 @@ pub enum Error { LivenessTrackingNotEnabled, #[error("matching head block for attestation is not found")] MatchingAttestationHeadBlockNotFound, - #[error("eth-consensus-version header expected")] - MissingEthConsensusVersionHeader, #[error("beacon node is currently syncing and not serving requests on this endpoint")] NodeIsSyncing, #[error("peer not found")] @@ -199,9 +197,9 @@ impl Error { | Self::InvalidBlock(_) | Self::InvalidBlobIndex(_) | Self::InvalidBlockId(_) + | Self::InvalidRequestConsensusHeader(_) | Self::InvalidContributionAndProofs(_) | Self::InvalidEpoch(_) - | Self::InvalidEthConsensusVersionHeader(_) | Self::InvalidQuery(_) | Self::InvalidPeerId(_) | Self::InvalidProposerSlashing(_) @@ -212,7 +210,6 @@ impl Error { | Self::InvalidRandaoReveal | Self::InvalidValidatorId(_) | Self::InvalidValidatorSignatures(_) - | Self::MissingEthConsensusVersionHeader | Self::ProposalSlotNotLaterThanStateSlot | Self::SlotNotInEpoch | Self::StatePreCapella diff --git a/http_api/src/extractors.rs b/http_api/src/extractors.rs index 3542a936..33a13d51 100644 --- a/http_api/src/extractors.rs +++ b/http_api/src/extractors.rs @@ -41,7 +41,6 @@ use types::{ use crate::{ error::Error, - misc::ETH_CONSENSUS_VERSION, validator_status::{ValidatorId, ValidatorIdsAndStatusesBody}, }; @@ -215,7 +214,10 @@ impl FromRequest for EthJson>> { type Rejection = Error; async fn from_request(request: Request, _state: &S) -> Result { - match extract_phase(&request)? { + let phase = http_api_utils::extract_phase_from_headers(request.headers()) + .map_err(Error::InvalidRequestConsensusHeader)?; + + match phase { Phase::Phase0 | Phase::Altair | Phase::Bellatrix | Phase::Capella | Phase::Deneb => { request .extract() @@ -394,9 +396,12 @@ where request.extract_parts::>().await?; if content_type == ContentType::octet_stream() { - let phase = extract_phase(&request)?; + let phase = http_api_utils::extract_phase_from_headers(request.headers()) + .map_err(Error::InvalidRequestConsensusHeader)?; + let bytes = Bytes::from_request(request, state).await?; let block = T::from_ssz(&phase, bytes)?; + return Ok(Self(block)); } @@ -408,16 +413,3 @@ where run.await.map_err(Error::InvalidBlock) } } - -fn extract_phase(request: &Request) -> Result { - request - .headers() - .get(ETH_CONSENSUS_VERSION) - .ok_or(Error::MissingEthConsensusVersionHeader)? - .to_str() - .map_err(AnyhowError::msg) - .map_err(Error::InvalidEthConsensusVersionHeader)? - .parse() - .map_err(AnyhowError::msg) - .map_err(Error::InvalidEthConsensusVersionHeader) -} diff --git a/http_api/src/misc.rs b/http_api/src/misc.rs index 149fb688..853a0b90 100644 --- a/http_api/src/misc.rs +++ b/http_api/src/misc.rs @@ -32,8 +32,6 @@ use ::{ futures::{channel::mpsc::UnboundedReceiver, lock::Mutex}, }; -pub const ETH_CONSENSUS_VERSION: &str = "eth-consensus-version"; - const ORDERING: Ordering = Ordering::SeqCst; #[cfg(test)] diff --git a/http_api/src/response.rs b/http_api/src/response.rs index d255dafa..a683ef5c 100644 --- a/http_api/src/response.rs +++ b/http_api/src/response.rs @@ -4,6 +4,7 @@ use axum::{ response::{IntoResponse, Response}, Json, }; +use http_api_utils::ETH_CONSENSUS_VERSION; use mediatype::{MediaType, MediaTypeList}; use mime::APPLICATION_OCTET_STREAM; use serde::Serialize; @@ -11,7 +12,7 @@ use ssz::SszWrite; use tap::Pipe as _; use types::{bellatrix::primitives::Wei, nonstandard::Phase, phase0::primitives::H256}; -use crate::{error::Error, misc::ETH_CONSENSUS_VERSION}; +use crate::error::Error; const ETH_CONSENSUS_BLOCK_VALUE: &str = "eth-consensus-block-value"; const ETH_EXECUTION_PAYLOAD_BLINDED: &str = "eth-execution-payload-blinded"; diff --git a/http_api_utils/Cargo.toml b/http_api_utils/Cargo.toml index 778ff26b..a1916153 100644 --- a/http_api_utils/Cargo.toml +++ b/http_api_utils/Cargo.toml @@ -13,6 +13,7 @@ execution_engine = { workspace = true } features = { workspace = true } fork_choice_store = { workspace = true } helper_functions = { workspace = true } +http = { workspace = true } http-body-util = { workspace = true } itertools = { workspace = true } log = { workspace = true } diff --git a/http_api_utils/src/helpers.rs b/http_api_utils/src/helpers.rs index 99030167..d2755d34 100644 --- a/http_api_utils/src/helpers.rs +++ b/http_api_utils/src/helpers.rs @@ -1,14 +1,18 @@ use core::time::Duration; +use anyhow::{Error as AnyhowError, Result}; use axum::{error_handling::HandleErrorLayer, http::StatusCode, Router}; use features::Feature; +use http::{HeaderMap, HeaderValue}; +use thiserror::Error; use tower::ServiceBuilder; use tower_http::{ cors::{AllowOrigin, CorsLayer}, trace::TraceLayer, }; +use types::nonstandard::Phase; -use crate::{logging, middleware, misc::ApiMetrics, ApiError}; +use crate::{logging, middleware, misc::ApiMetrics, ApiError, ETH_CONSENSUS_VERSION}; pub fn extend_router_with_middleware( mut router: Router, @@ -59,3 +63,27 @@ pub fn extend_router_with_middleware( router } + +#[derive(Debug, Error)] +pub enum PhaseHeaderError { + #[error("invalid eth-consensus-version header")] + InvalidEthConsensusVersionHeader(#[source] AnyhowError), + #[error("eth-consensus-version header expected")] + MissingEthConsensusVersionHeader, +} + +pub fn extract_phase_from_headers( + headers: &HeaderMap, +) -> Result { + let phase = headers + .get(ETH_CONSENSUS_VERSION) + .ok_or(PhaseHeaderError::MissingEthConsensusVersionHeader)? + .to_str() + .map_err(AnyhowError::msg) + .map_err(PhaseHeaderError::InvalidEthConsensusVersionHeader)? + .parse() + .map_err(AnyhowError::msg) + .map_err(PhaseHeaderError::InvalidEthConsensusVersionHeader)?; + + Ok(phase) +} diff --git a/http_api_utils/src/lib.rs b/http_api_utils/src/lib.rs index a8b4f482..e71bc1c2 100644 --- a/http_api_utils/src/lib.rs +++ b/http_api_utils/src/lib.rs @@ -1,7 +1,7 @@ pub use block_id::BlockId; pub use events::{DependentRootsBundle, EventChannels, Topic, DEFAULT_MAX_EVENTS}; -pub use helpers::extend_router_with_middleware; -pub use misc::{ApiMetrics, Direction}; +pub use helpers::{extend_router_with_middleware, extract_phase_from_headers, PhaseHeaderError}; +pub use misc::{ApiMetrics, Direction, ETH_CONSENSUS_VERSION}; pub use state_id::StateId; pub use traits::ApiError; diff --git a/http_api_utils/src/misc.rs b/http_api_utils/src/misc.rs index 6a9e6c1b..65ecdc3b 100644 --- a/http_api_utils/src/misc.rs +++ b/http_api_utils/src/misc.rs @@ -4,6 +4,8 @@ use std::sync::Arc; use parse_display::Display; use prometheus_metrics::Metrics; +pub const ETH_CONSENSUS_VERSION: &str = "eth-consensus-version"; + #[derive(Clone, Copy)] enum ApiType { Http, diff --git a/types/src/combined.rs b/types/src/combined.rs index 857e36f7..07b09a6f 100644 --- a/types/src/combined.rs +++ b/types/src/combined.rs @@ -827,6 +827,17 @@ impl SszRead for SignedBlindedBeaconBlock

{ } } +impl SszWrite for SignedBlindedBeaconBlock

{ + fn write_variable(&self, bytes: &mut Vec) -> Result<(), WriteError> { + match self { + Self::Bellatrix(signed_blinded_block) => signed_blinded_block.write_variable(bytes), + Self::Capella(signed_blinded_block) => signed_blinded_block.write_variable(bytes), + Self::Deneb(signed_blinded_block) => signed_blinded_block.write_variable(bytes), + Self::Electra(signed_blinded_block) => signed_blinded_block.write_variable(bytes), + } + } +} + impl SignedBlindedBeaconBlock

{ pub fn split(self) -> (BlindedBeaconBlock

, SignatureBytes) { match self { @@ -1006,6 +1017,38 @@ impl SszHash for ExecutionPayload

{ } } +impl SszSize for ExecutionPayload

{ + // The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type. + // Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable. + const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 3 }>([ + BellatrixExecutionPayload::

::SIZE, + CapellaExecutionPayload::

::SIZE, + DenebExecutionPayload::

::SIZE, + ]); +} + +impl SszRead for ExecutionPayload

{ + fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result { + let block = match phase { + Phase::Phase0 => { + return Err(ReadError::Custom { + message: "execution payload is not available in Phase 0", + }); + } + Phase::Altair => { + return Err(ReadError::Custom { + message: "execution payload is not available in Altair", + }); + } + Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?), + Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?), + Phase::Deneb | Phase::Electra => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?), + }; + + Ok(block) + } +} + impl ExecutionPayload

{ pub const fn phase(&self) -> Phase { match self { @@ -1647,6 +1690,25 @@ mod spec_tests { assert_eq!(value.phase(), Phase::test_phase); } + #[duplicate_item( + glob function_name combined_type preset phase; + ["consensus-spec-tests/tests/mainnet/bellatrix/ssz_static/ExecutionPayload/*/*"] [bellatrix_mainnet_execution_payload] [ExecutionPayload] [Mainnet] [Bellatrix]; + ["consensus-spec-tests/tests/minimal/bellatrix/ssz_static/ExecutionPayload/*/*"] [bellatrix_minimal_execution_payload] [ExecutionPayload] [Minimal] [Bellatrix]; + ["consensus-spec-tests/tests/mainnet/capella/ssz_static/ExecutionPayload/*/*"] [capella_mainnet_execution_payload] [ExecutionPayload] [Mainnet] [Capella]; + ["consensus-spec-tests/tests/minimal/capella/ssz_static/ExecutionPayload/*/*"] [capella_minimal_execution_payload] [ExecutionPayload] [Minimal] [Capella]; + ["consensus-spec-tests/tests/mainnet/deneb/ssz_static/ExecutionPayload/*/*"] [deneb_mainnet_execution_payload] [ExecutionPayload] [Mainnet] [Deneb]; + ["consensus-spec-tests/tests/minimal/deneb/ssz_static/ExecutionPayload/*/*"] [deneb_minimal_execution_payload] [ExecutionPayload] [Minimal] [Deneb]; + ["consensus-spec-tests/tests/mainnet/electra/ssz_static/ExecutionPayload/*/*"] [electra_mainnet_execution_payload] [ExecutionPayload] [Mainnet] [Deneb]; + ["consensus-spec-tests/tests/minimal/electra/ssz_static/ExecutionPayload/*/*"] [electra_minimal_execution_payload] [ExecutionPayload] [Minimal] [Deneb]; + )] + #[test_resources(glob)] + fn function_name(case: Case) { + let expected_ssz_bytes = case.bytes("serialized.ssz_snappy"); + + combined_type::::from_ssz(&Phase::phase, expected_ssz_bytes.as_slice()) + .expect("SSZ decoding should succeed"); + } + #[duplicate_item( glob function_name combined_type preset phase; ["consensus-spec-tests/tests/mainnet/altair/ssz_static/LightClientBootstrap/*/*"] [altair_mainnet_bootstrap] [LightClientBootstrap] [Mainnet] [Altair];