Skip to content

Commit

Permalink
Add SSZ support to Builder API
Browse files Browse the repository at this point in the history
  • Loading branch information
Tumas committed Jan 31, 2025
1 parent dbae6e1 commit 5308ab3
Show file tree
Hide file tree
Showing 21 changed files with 270 additions and 54 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions builder_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
71 changes: 54 additions & 17 deletions builder_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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);
Expand Down Expand Up @@ -157,19 +162,26 @@ 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 {
info!("builder has no execution payload header available for slot {slot}");
return Ok(None);
}

let builder_bid = response.json::<SignedBuilderBid<P>>().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::<P>::from_ssz(&phase, &bytes)?
}
};

debug!("get_execution_payload_header response: {builder_bid:?}");

Expand Down Expand Up @@ -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<ExecutionPayload<P>, P> = response
.json::<ExecutionPayloadAndBlobsBundle<P>>()
.await?

let response: WithBlobsAndMev<ExecutionPayload<P>, 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::<P>::from_ssz(&phase, &bytes)?
}
}
.into();

let execution_payload = &response.value;
Expand All @@ -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<RedactingUrl> {
self.config.builder_api_url.join(path).map_err(Into::into)
}
Expand Down Expand Up @@ -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"),
Expand Down
5 changes: 3 additions & 2 deletions builder_api/src/bellatrix/containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P: Preset> {
pub header: Box<ExecutionPayloadHeader<P>>,
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<P: Preset> {
pub message: BuilderBid<P>,
pub signature: SignatureBytes,
Expand Down
5 changes: 3 additions & 2 deletions builder_api/src/capella/containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<P: Preset> {
pub header: Box<ExecutionPayloadHeader<P>>,
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<P: Preset> {
pub message: BuilderBid<P>,
pub signature: SignatureBytes,
Expand Down
70 changes: 69 additions & 1 deletion builder_api/src/combined.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -38,6 +39,40 @@ pub enum SignedBuilderBid<P: Preset> {
Electra(ElectraSignedBuilderBid<P>),
}

impl<P: Preset> SszSize for SignedBuilderBid<P> {
// 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::<P>::SIZE,
CapellaSignedBuilderBid::<P>::SIZE,
DenebSignedBuilderBid::<P>::SIZE,
ElectraSignedBuilderBid::<P>::SIZE,
]);
}

impl<P: Preset> SszRead<Phase> for SignedBuilderBid<P> {
fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result<Self, ReadError> {
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<P: Preset> SignedBuilderBid<P> {
#[must_use]
pub(crate) const fn pubkey(&self) -> PublicKeyBytes {
Expand Down Expand Up @@ -126,6 +161,39 @@ pub enum ExecutionPayloadAndBlobsBundle<P: Preset> {
Electra(DenebExecutionPayloadAndBlobsBundle<P>),
}

impl<P: Preset> SszSize for ExecutionPayloadAndBlobsBundle<P> {
// 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::<P>::SIZE,
CapellaExecutionPayload::<P>::SIZE,
DenebExecutionPayloadAndBlobsBundle::<P>::SIZE,
]);
}

impl<P: Preset> SszRead<Phase> for ExecutionPayloadAndBlobsBundle<P> {
fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result<Self, ReadError> {
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<P: Preset> From<ExecutionPayloadAndBlobsBundle<P>>
for WithBlobsAndMev<ExecutionPayload<P>, P>
{
Expand Down
10 changes: 9 additions & 1 deletion builder_api/src/config.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
10 changes: 7 additions & 3 deletions builder_api/src/deneb/containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,34 @@ use types::{

#[derive(Debug, Deserialize, Ssz)]
#[serde(bound = "", deny_unknown_fields)]
#[ssz(derive_write = false)]
pub struct BuilderBid<P: Preset> {
pub header: Box<ExecutionPayloadHeader<P>>,
pub blob_kzg_commitments: ContiguousList<KzgCommitment, P::MaxBlobCommitmentsPerBlock>,
pub value: Uint256,
pub pubkey: PublicKeyBytes,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Ssz)]
#[serde(bound = "", deny_unknown_fields)]
#[ssz(derive_write = false)]
pub struct SignedBuilderBid<P: Preset> {
pub message: BuilderBid<P>,
pub signature: SignatureBytes,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Ssz)]
#[serde(bound = "", deny_unknown_fields)]
#[ssz(derive_write = false)]
pub struct BlobsBundle<P: Preset> {
pub commitments: ContiguousList<KzgCommitment, P::MaxBlobCommitmentsPerBlock>,
pub proofs: ContiguousList<KzgProof, P::MaxBlobsPerBlock>,
pub blobs: ContiguousList<Blob<P>, P::MaxBlobsPerBlock>,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Ssz)]
#[serde(bound = "", deny_unknown_fields)]
#[ssz(derive_write = false)]
pub struct ExecutionPayloadAndBlobsBundle<P: Preset> {
pub execution_payload: ExecutionPayload<P>,
pub blobs_bundle: BlobsBundle<P>,
Expand Down
4 changes: 3 additions & 1 deletion builder_api/src/electra/containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use types::{

#[derive(Debug, Deserialize, Ssz)]
#[serde(bound = "", deny_unknown_fields)]
#[ssz(derive_write = false)]
pub struct BuilderBid<P: Preset> {
pub header: Box<ExecutionPayloadHeader<P>>,
pub blob_kzg_commitments: ContiguousList<KzgCommitment, P::MaxBlobCommitmentsPerBlock>,
Expand All @@ -22,8 +23,9 @@ pub struct BuilderBid<P: Preset> {
pub pubkey: PublicKeyBytes,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Ssz)]
#[serde(bound = "", deny_unknown_fields)]
#[ssz(derive_write = false)]
pub struct SignedBuilderBid<P: Preset> {
pub message: BuilderBid<P>,
pub signature: SignatureBytes,
Expand Down
2 changes: 1 addition & 1 deletion builder_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading

0 comments on commit 5308ab3

Please sign in to comment.