diff --git a/Cargo.lock b/Cargo.lock index 899435a66ba..f4eb6fc7f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1119,10 +1119,12 @@ name = "builder_client" version = "0.1.0" dependencies = [ "eth2", + "ethereum_ssz", "lighthouse_version", "reqwest", "sensitive_url", "serde", + "serde_json", ] [[package]] diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index 3531e81c847..1920bd0ebb2 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -6,7 +6,9 @@ authors = ["Sean Anderson "] [dependencies] eth2 = { workspace = true } +ethereum_ssz = { workspace = true } lighthouse_version = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 91ee00a65f7..5f64ac7e43c 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -1,16 +1,24 @@ use eth2::types::builder_bid::SignedBuilderBid; +use eth2::types::fork_versioned_response::EmptyMetadata; use eth2::types::{ - EthSpec, ExecutionBlockHash, ForkVersionedResponse, PublicKeyBytes, - SignedValidatorRegistrationData, Slot, + ContentType, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, ForkVersionDeserialize, + ForkVersionedResponse, PublicKeyBytes, SignedValidatorRegistrationData, Slot, }; use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; pub use eth2::Error; -use eth2::{ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER}; -use reqwest::header::{HeaderMap, HeaderValue}; +use eth2::{ + ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, + JSON_CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER, +}; +use reqwest::header::{HeaderMap, HeaderValue, ACCEPT}; use reqwest::{IntoUrl, Response}; use sensitive_url::SensitiveUrl; use serde::de::DeserializeOwned; use serde::Serialize; +use ssz::Encode; +use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000; @@ -49,6 +57,7 @@ pub struct BuilderHttpClient { server: SensitiveUrl, timeouts: Timeouts, user_agent: String, + ssz_enabled: Arc, } impl BuilderHttpClient { @@ -64,6 +73,7 @@ impl BuilderHttpClient { server, timeouts: Timeouts::new(builder_header_timeout), user_agent, + ssz_enabled: Arc::new(false.into()), }) } @@ -71,6 +81,78 @@ impl BuilderHttpClient { &self.user_agent } + fn fork_name_from_header(&self, headers: &HeaderMap) -> Result, String> { + headers + .get(CONSENSUS_VERSION_HEADER) + .map(|fork_name| { + fork_name + .to_str() + .map_err(|e| e.to_string()) + .and_then(ForkName::from_str) + }) + .transpose() + } + + fn content_type_from_header(&self, headers: &HeaderMap) -> ContentType { + let Some(content_type) = headers.get(CONTENT_TYPE_HEADER).map(|content_type| { + let content_type = content_type.to_str(); + match content_type { + Ok(SSZ_CONTENT_TYPE_HEADER) => ContentType::Ssz, + _ => ContentType::Json, + } + }) else { + return ContentType::Json; + }; + content_type + } + + async fn get_with_header< + T: DeserializeOwned + ForkVersionDecode + ForkVersionDeserialize, + U: IntoUrl, + >( + &self, + url: U, + timeout: Duration, + headers: HeaderMap, + ) -> Result, Error> { + let response = self + .get_response_with_header(url, Some(timeout), headers) + .await?; + + let headers = response.headers().clone(); + let response_bytes = response.bytes().await?; + + let Ok(Some(fork_name)) = self.fork_name_from_header(&headers) else { + // if no fork version specified, attempt to fallback to JSON + self.ssz_enabled.store(false, Ordering::SeqCst); + return serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson); + }; + + let content_type = self.content_type_from_header(&headers); + + match content_type { + ContentType::Ssz => { + self.ssz_enabled.store(true, Ordering::SeqCst); + T::from_ssz_bytes_by_fork(&response_bytes, fork_name) + .map(|data| ForkVersionedResponse { + version: Some(fork_name), + metadata: EmptyMetadata {}, + data, + }) + .map_err(Error::InvalidSsz) + } + ContentType::Json => { + self.ssz_enabled.store(false, Ordering::SeqCst); + serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson) + } + } + } + + /// Return `true` if the most recently received response from the builder had SSZ Content-Type. + pub fn is_ssz_enabled(&self) -> bool { + self.ssz_enabled.load(Ordering::SeqCst) + } + async fn get_with_timeout( &self, url: U, @@ -83,6 +165,21 @@ impl BuilderHttpClient { .map_err(Into::into) } + /// Perform a HTTP GET request, returning the `Response` for further processing. + async fn get_response_with_header( + &self, + url: U, + timeout: Option, + headers: HeaderMap, + ) -> Result { + let mut builder = self.client.get(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + let response = builder.headers(headers).send().await.map_err(Error::from)?; + ok_or_error(response).await + } + /// Perform a HTTP GET request, returning the `Response` for further processing. async fn get_response_with_timeout( &self, @@ -112,6 +209,32 @@ impl BuilderHttpClient { ok_or_error(response).await } + async fn post_ssz_with_raw_response( + &self, + url: U, + ssz_body: Vec, + mut headers: HeaderMap, + timeout: Option, + ) -> Result { + let mut builder = self.client.post(url); + if let Some(timeout) = timeout { + builder = builder.timeout(timeout); + } + + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_static(SSZ_CONTENT_TYPE_HEADER), + ); + + let response = builder + .headers(headers) + .body(ssz_body) + .send() + .await + .map_err(Error::from)?; + ok_or_error(response).await + } + async fn post_with_raw_response( &self, url: U, @@ -152,6 +275,42 @@ impl BuilderHttpClient { Ok(()) } + /// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body + pub async fn post_builder_blinded_blocks_ssz( + &self, + blinded_block: &SignedBlindedBeaconBlock, + ) -> Result, Error> { + let mut path = self.server.full.clone(); + + let body = blinded_block.as_ssz_bytes(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("eth") + .push("v1") + .push("builder") + .push("blinded_blocks"); + + let mut headers = HeaderMap::new(); + if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) { + headers.insert(CONSENSUS_VERSION_HEADER, value); + } + + let result = self + .post_ssz_with_raw_response( + path, + body, + headers, + Some(self.timeouts.post_blinded_blocks), + ) + .await? + .bytes() + .await?; + + FullPayloadContents::from_ssz_bytes_by_fork(&result, blinded_block.fork_name_unchecked()) + .map_err(Error::InvalidSsz) + } + /// `POST /eth/v1/builder/blinded_blocks` pub async fn post_builder_blinded_blocks( &self, @@ -202,7 +361,17 @@ impl BuilderHttpClient { .push(format!("{parent_hash:?}").as_str()) .push(pubkey.as_hex_string().as_str()); - let resp = self.get_with_timeout(path, self.timeouts.get_header).await; + let mut headers = HeaderMap::new(); + if let Ok(ssz_content_type_header) = HeaderValue::from_str(&format!( + "{}; q=1.0,{}; q=0.9", + SSZ_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER + )) { + headers.insert(ACCEPT, ssz_content_type_header); + }; + + let resp = self + .get_with_header(path, self.timeouts.get_header, headers) + .await; if matches!(resp, Err(Error::StatusCode(StatusCode::NO_CONTENT))) { Ok(None) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index d5fef4c5aaa..4e0fe1de16b 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -1900,11 +1900,18 @@ impl ExecutionLayer { if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { - builder - .post_builder_blinded_blocks(block) - .await - .map_err(Error::Builder) - .map(|d| d.data) + if builder.is_ssz_enabled() { + builder + .post_builder_blinded_blocks_ssz(block) + .await + .map_err(Error::Builder) + } else { + builder + .post_builder_blinded_blocks(block) + .await + .map_err(Error::Builder) + .map(|d| d.data) + } }) .await; diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 3540909fe46..f07ee7ac6f8 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -1,15 +1,20 @@ use crate::test_utils::{DEFAULT_BUILDER_PAYLOAD_VALUE_WEI, DEFAULT_JWT_SECRET}; use crate::{Config, ExecutionLayer, PayloadAttributes, PayloadParameters}; +use bytes::Bytes; use eth2::types::PublishBlockRequest; use eth2::types::{ BlobsBundle, BlockId, BroadcastValidation, EventKind, EventTopic, FullPayloadContents, ProposerData, StateId, ValidatorId, }; -use eth2::{BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER}; +use eth2::{ + BeaconNodeHttpClient, Timeouts, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, + SSZ_CONTENT_TYPE_HEADER, +}; use fork_choice::ForkchoiceUpdateParameters; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use slog::{debug, error, info, warn, Logger}; +use ssz::Encode; use std::collections::HashMap; use std::fmt::Debug; use std::future::Future; @@ -26,11 +31,12 @@ use types::builder_bid::{ }; use types::{ Address, BeaconState, ChainSpec, Epoch, EthSpec, ExecPayload, ExecutionPayload, - ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionedResponse, Hash256, - PublicKeyBytes, Signature, SignedBlindedBeaconBlock, SignedRoot, - SignedValidatorRegistrationData, Slot, Uint256, + ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, ForkVersionDecode, + ForkVersionedResponse, Hash256, PublicKeyBytes, Signature, SignedBlindedBeaconBlock, + SignedRoot, SignedValidatorRegistrationData, Slot, Uint256, }; use types::{ExecutionBlockHash, SecretKey}; +use warp::reply::{self, Reply}; use warp::{Filter, Rejection}; pub const DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); @@ -955,6 +961,33 @@ pub fn serve( ) .boxed(); + let blinded_block_ssz = prefix + .and(warp::path("blinded_blocks")) + .and(warp::body::bytes()) + .and(warp::header::header::(CONSENSUS_VERSION_HEADER)) + .and(warp::path::end()) + .and(ctx_filter.clone()) + .and_then( + |block_bytes: Bytes, fork_name: ForkName, builder: MockBuilder| async move { + let block = + SignedBlindedBeaconBlock::::from_ssz_bytes_by_fork(&block_bytes, fork_name) + .map_err(|e| warp::reject::custom(Custom(format!("{:?}", e))))?; + let payload = builder + .submit_blinded_block(block) + .await + .map_err(|e| warp::reject::custom(Custom(e)))?; + + Ok::<_, warp::reject::Rejection>( + warp::http::Response::builder() + .status(200) + .body(payload.as_ssz_bytes()) + .map(add_ssz_content_type_header) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap(), + ) + }, + ); + let blinded_block = prefix .and(warp::path("blinded_blocks")) @@ -1007,35 +1040,47 @@ pub fn serve( ) .and(warp::path::end()) .and(ctx_filter.clone()) + .and(warp::header::optional::("accept")) .and_then( |slot: Slot, parent_hash: ExecutionBlockHash, pubkey: PublicKeyBytes, - builder: MockBuilder| async move { + builder: MockBuilder, + accept_header: Option| async move { let fork_name = builder.fork_name_at_slot(slot); let signed_bid = builder .get_header(slot, parent_hash, pubkey) .await .map_err(|e| warp::reject::custom(Custom(e)))?; - - let resp: ForkVersionedResponse<_> = ForkVersionedResponse { - version: Some(fork_name), - metadata: Default::default(), - data: signed_bid, - }; - let json_bid = serde_json::to_string(&resp) - .map_err(|_| reject("coudn't serialize signed bid"))?; - Ok::<_, Rejection>( - warp::http::Response::builder() - .status(200) - .body(json_bid) - .unwrap(), - ) + let accept_header = accept_header.unwrap_or(eth2::types::Accept::Any); + match accept_header { + eth2::types::Accept::Ssz => Ok::<_, Rejection>( + warp::http::Response::builder() + .status(200) + .body(signed_bid.as_ssz_bytes()) + .map(add_ssz_content_type_header) + .map(|res| add_consensus_version_header(res, fork_name)) + .unwrap(), + ), + eth2::types::Accept::Json | eth2::types::Accept::Any => { + let resp: ForkVersionedResponse<_> = ForkVersionedResponse { + version: Some(fork_name), + metadata: Default::default(), + data: signed_bid, + }; + Ok::<_, Rejection>(warp::reply::json(&resp).into_response()) + } + } }, ); let routes = warp::post() - .and(validators.or(blinded_block)) + // Routes which expect `application/octet-stream` go within this `and`. + .and( + warp::header::exact(CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER) + .and(blinded_block_ssz), + ) + .or(validators.or(blinded_block)) .or(warp::get().and(status).or(header)) .map(|reply| warp::reply::with_header(reply, "Server", "lighthouse-mock-builder-server")); @@ -1048,3 +1093,13 @@ pub fn serve( fn reject(msg: &'static str) -> Rejection { warp::reject::custom(Custom(msg.to_string())) } + +/// Add the 'Content-Type application/octet-stream` header to a response. +fn add_ssz_content_type_header(reply: T) -> warp::reply::Response { + reply::with_header(reply, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER).into_response() +} + +/// Add the `Eth-Consensus-Version` header to a response. +fn add_consensus_version_header(reply: T, fork_name: ForkName) -> warp::reply::Response { + reply::with_header(reply, CONSENSUS_VERSION_HEADER, fork_name.to_string()).into_response() +} diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 75251cb5fb4..7928957571b 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -662,7 +662,9 @@ impl, Cold: ItemStore> HotColdDB .hot_db .get_bytes(ExecutionPayload::::db_column(), key)? { - Some(bytes) => Ok(Some(ExecutionPayload::from_ssz_bytes(&bytes, fork_name)?)), + Some(bytes) => Ok(Some(ExecutionPayload::from_ssz_bytes_by_fork( + &bytes, fork_name, + )?)), None => Ok(None), } } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index af8573a5789..1cf59903d3c 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -48,6 +48,7 @@ pub const CONSENSUS_BLOCK_VALUE_HEADER: &str = "Eth-Consensus-Block-Value"; pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; pub const SSZ_CONTENT_TYPE_HEADER: &str = "application/octet-stream"; +pub const JSON_CONTENT_TYPE_HEADER: &str = "application/json"; #[derive(Debug)] pub enum Error { @@ -111,9 +112,9 @@ impl Error { Error::InvalidSignatureHeader => None, Error::MissingSignatureHeader => None, Error::InvalidJson(_) => None, + Error::InvalidSsz(_) => None, Error::InvalidServerSentEvent(_) => None, Error::InvalidHeaders(_) => None, - Error::InvalidSsz(_) => None, Error::TokenReadError(..) => None, Error::NoServerPubkey | Error::NoToken => None, } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c6e95e1ba30..59374f629d6 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1664,7 +1664,7 @@ impl FullBlockContents { } /// SSZ decode with fork variant determined by slot. - pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { + pub fn from_ssz_bytes(bytes: &[u8], spec: &ChainSpec) -> Result { let slot_len = ::ssz_fixed_len(); let slot_bytes = bytes .get(0..slot_len) @@ -1678,10 +1678,7 @@ impl FullBlockContents { } /// SSZ decode with fork variant passed in explicitly. - pub fn from_ssz_bytes_for_fork( - bytes: &[u8], - fork_name: ForkName, - ) -> Result { + pub fn from_ssz_bytes_for_fork(bytes: &[u8], fork_name: ForkName) -> Result { if fork_name.deneb_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); @@ -1836,7 +1833,7 @@ impl PublishBlockRequest { } /// SSZ decode with fork variant determined by `fork_name`. - pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { + pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { if fork_name.deneb_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; @@ -1845,7 +1842,7 @@ impl PublishBlockRequest { let mut decoder = builder.build()?; let block = decoder.decode_next_with(|bytes| { - SignedBeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) + SignedBeaconBlock::from_ssz_bytes_by_fork(bytes, fork_name) })?; let kzg_proofs = decoder.decode_next()?; let blobs = decoder.decode_next()?; @@ -1854,7 +1851,7 @@ impl PublishBlockRequest { Some((kzg_proofs, blobs)), )) } else { - SignedBeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) + SignedBeaconBlock::from_ssz_bytes_by_fork(bytes, fork_name) .map(|block| PublishBlockRequest::Block(Arc::new(block))) } } @@ -1946,6 +1943,24 @@ pub enum FullPayloadContents { PayloadAndBlobs(ExecutionPayloadAndBlobs), } +impl ForkVersionDecode for FullPayloadContents { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + if fork_name.deneb_enabled() { + Ok(Self::PayloadAndBlobs( + ExecutionPayloadAndBlobs::from_ssz_bytes_by_fork(bytes, fork_name)?, + )) + } else if fork_name.bellatrix_enabled() { + Ok(Self::Payload(ExecutionPayload::from_ssz_bytes_by_fork( + bytes, fork_name, + )?)) + } else { + Err(ssz::DecodeError::BytesInvalid(format!( + "FullPayloadContents decoding for {fork_name} not implemented" + ))) + } + } +} + impl FullPayloadContents { pub fn new( execution_payload: ExecutionPayload, @@ -2012,6 +2027,36 @@ pub struct ExecutionPayloadAndBlobs { pub blobs_bundle: BlobsBundle, } +impl ForkVersionDecode for ExecutionPayloadAndBlobs { + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + let mut builder = ssz::SszDecoderBuilder::new(bytes); + builder.register_anonymous_variable_length_item()?; + builder.register_type::>()?; + let mut decoder = builder.build()?; + + if fork_name.deneb_enabled() { + let execution_payload = decoder.decode_next_with(|bytes| { + ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) + })?; + let blobs_bundle = decoder.decode_next()?; + Ok(Self { + execution_payload, + blobs_bundle, + }) + } else { + Err(DecodeError::BytesInvalid(format!( + "ExecutionPayloadAndBlobs decoding for {fork_name} not implemented" + ))) + } + } +} + +#[derive(Debug)] +pub enum ContentType { + Json, + Ssz, +} + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "E: EthSpec")] pub struct BlobsBundle { diff --git a/consensus/types/src/builder_bid.rs b/consensus/types/src/builder_bid.rs index ac53c41216f..49911c39095 100644 --- a/consensus/types/src/builder_bid.rs +++ b/consensus/types/src/builder_bid.rs @@ -3,25 +3,37 @@ use crate::{ ChainSpec, EthSpec, ExecutionPayloadHeaderBellatrix, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, ExecutionPayloadHeaderElectra, ExecutionPayloadHeaderFulu, ExecutionPayloadHeaderRef, ExecutionPayloadHeaderRefMut, ExecutionRequests, ForkName, - ForkVersionDeserialize, SignedRoot, Uint256, + ForkVersionDecode, ForkVersionDeserialize, SignedRoot, Uint256, }; use bls::PublicKeyBytes; use bls::Signature; use serde::{Deserialize, Deserializer, Serialize}; +use ssz::Decode; +use ssz_derive::{Decode, Encode}; use superstruct::superstruct; use tree_hash_derive::TreeHash; #[superstruct( variants(Bellatrix, Capella, Deneb, Electra, Fulu), variant_attributes( - derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone), + derive( + PartialEq, + Debug, + Encode, + Serialize, + Deserialize, + TreeHash, + Decode, + Clone + ), serde(bound = "E: EthSpec", deny_unknown_fields) ), map_ref_into(ExecutionPayloadHeaderRef), map_ref_mut_into(ExecutionPayloadHeaderRefMut) )] -#[derive(PartialEq, Debug, Serialize, Deserialize, TreeHash, Clone)] +#[derive(PartialEq, Debug, Encode, Serialize, Deserialize, TreeHash, Clone)] #[serde(bound = "E: EthSpec", deny_unknown_fields, untagged)] +#[ssz(enum_behaviour = "transparent")] #[tree_hash(enum_behaviour = "transparent")] pub struct BuilderBid { #[superstruct(only(Bellatrix), partial_getter(rename = "header_bellatrix"))] @@ -65,16 +77,54 @@ impl<'a, E: EthSpec> BuilderBidRefMut<'a, E> { } } +impl ForkVersionDecode for BuilderBid { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + let builder_bid = match fork_name { + ForkName::Altair | ForkName::Base => { + return Err(ssz::DecodeError::BytesInvalid(format!( + "unsupported fork for ExecutionPayloadHeader: {fork_name}", + ))) + } + ForkName::Bellatrix => { + BuilderBid::Bellatrix(BuilderBidBellatrix::from_ssz_bytes(bytes)?) + } + ForkName::Capella => BuilderBid::Capella(BuilderBidCapella::from_ssz_bytes(bytes)?), + ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => BuilderBid::Electra(BuilderBidElectra::from_ssz_bytes(bytes)?), + ForkName::Fulu => BuilderBid::Fulu(BuilderBidFulu::from_ssz_bytes(bytes)?), + }; + Ok(builder_bid) + } +} + impl SignedRoot for BuilderBid {} /// Validator registration, for use in interacting with servers implementing the builder API. -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +#[derive(PartialEq, Debug, Encode, Serialize, Deserialize, Clone)] #[serde(bound = "E: EthSpec")] pub struct SignedBuilderBid { pub message: BuilderBid, pub signature: Signature, } +impl ForkVersionDecode for SignedBuilderBid { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + let mut builder = ssz::SszDecoderBuilder::new(bytes); + + builder.register_anonymous_variable_length_item()?; + builder.register_type::()?; + + let mut decoder = builder.build()?; + let message = decoder + .decode_next_with(|bytes| BuilderBid::from_ssz_bytes_by_fork(bytes, fork_name))?; + let signature = decoder.decode_next()?; + + Ok(Self { message, signature }) + } +} + impl ForkVersionDeserialize for BuilderBid { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: serde_json::value::Value, diff --git a/consensus/types/src/execution_payload.rs b/consensus/types/src/execution_payload.rs index 2df66343af1..5d756c8529f 100644 --- a/consensus/types/src/execution_payload.rs +++ b/consensus/types/src/execution_payload.rs @@ -40,7 +40,7 @@ pub type Withdrawals = VariableList::MaxWithdrawal map_ref_into(ExecutionPayloadHeader) )] #[derive( - Debug, Clone, Serialize, Encode, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary, + Debug, Clone, Serialize, Deserialize, Encode, TreeHash, Derivative, arbitrary::Arbitrary, )] #[derivative(PartialEq, Hash(bound = "E: EthSpec"))] #[serde(bound = "E: EthSpec", untagged)] @@ -102,8 +102,9 @@ impl<'a, E: EthSpec> ExecutionPayloadRef<'a, E> { } } -impl ExecutionPayload { - pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { +impl ForkVersionDecode for ExecutionPayload { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { match fork_name { ForkName::Base | ForkName::Altair => Err(ssz::DecodeError::BytesInvalid(format!( "unsupported fork for ExecutionPayload: {fork_name}", @@ -117,7 +118,9 @@ impl ExecutionPayload { ForkName::Fulu => ExecutionPayloadFulu::from_ssz_bytes(bytes).map(Self::Fulu), } } +} +impl ExecutionPayload { #[allow(clippy::arithmetic_side_effects)] /// Returns the maximum size of an execution payload. pub fn max_execution_payload_bellatrix_size() -> usize { diff --git a/consensus/types/src/fork_versioned_response.rs b/consensus/types/src/fork_versioned_response.rs index cd78b5b3ca0..7e4efd05d66 100644 --- a/consensus/types/src/fork_versioned_response.rs +++ b/consensus/types/src/fork_versioned_response.rs @@ -4,6 +4,11 @@ use serde::{Deserialize, Deserializer, Serialize}; use serde_json::value::Value; use std::sync::Arc; +pub trait ForkVersionDecode: Sized { + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result; +} + pub trait ForkVersionDeserialize: Sized + DeserializeOwned { fn deserialize_by_fork<'de, D: Deserializer<'de>>( value: Value, diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 11d1f5271b7..73a50b4ef3e 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -178,7 +178,9 @@ pub use crate::fork::Fork; pub use crate::fork_context::ForkContext; pub use crate::fork_data::ForkData; pub use crate::fork_name::{ForkName, InconsistentFork}; -pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedResponse}; +pub use crate::fork_versioned_response::{ + ForkVersionDecode, ForkVersionDeserialize, ForkVersionedResponse, +}; pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::indexed_attestation::{ diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index d9bf9bf55dd..eb5925a29b5 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -86,6 +86,17 @@ pub struct SignedBeaconBlock = FullP pub signature: Signature, } +impl> ForkVersionDecode + for SignedBeaconBlock +{ + /// SSZ decode with explicit fork variant. + fn from_ssz_bytes_by_fork(bytes: &[u8], fork_name: ForkName) -> Result { + Self::from_ssz_bytes_with(bytes, |bytes| { + BeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) + }) + } +} + pub type SignedBlindedBeaconBlock = SignedBeaconBlock>; impl> SignedBeaconBlock { @@ -108,16 +119,6 @@ impl> SignedBeaconBlock Self::from_ssz_bytes_with(bytes, |bytes| BeaconBlock::from_ssz_bytes(bytes, spec)) } - /// SSZ decode with explicit fork variant. - pub fn from_ssz_bytes_for_fork( - bytes: &[u8], - fork_name: ForkName, - ) -> Result { - Self::from_ssz_bytes_with(bytes, |bytes| { - BeaconBlock::from_ssz_bytes_for_fork(bytes, fork_name) - }) - } - /// SSZ decode which attempts to decode all variants (slow). pub fn any_from_ssz_bytes(bytes: &[u8]) -> Result { Self::from_ssz_bytes_with(bytes, BeaconBlock::any_from_ssz_bytes) diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index adb5bee7681..7178edb151c 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -25,9 +25,9 @@ use std::fmt::Debug; use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconState, - BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, FullPayload, - ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, SyncAggregate, - WithdrawalRequest, + BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, + ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, + SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -398,7 +398,7 @@ impl Operation for WithdrawalsPayload { fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { ssz_decode_file_with(path, |bytes| { - ExecutionPayload::from_ssz_bytes(bytes, fork_name) + ExecutionPayload::from_ssz_bytes_by_fork(bytes, fork_name) }) .map(|payload| WithdrawalsPayload { payload: payload.into(),