Skip to content

Commit 6a3578a

Browse files
committed
Add SSZ support to Builder API
ethereum/builder-specs#104
1 parent e616519 commit 6a3578a

File tree

21 files changed

+276
-61
lines changed

21 files changed

+276
-61
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder_api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ anyhow = { workspace = true }
1111
bls = { workspace = true }
1212
clock = { workspace = true }
1313
derive_more = { workspace = true }
14+
enum-iterator = { workspace = true }
1415
helper_functions = { workspace = true }
1516
hex-literal = { workspace = true }
17+
http_api_utils = { workspace = true }
1618
itertools = { workspace = true }
1719
log = { workspace = true }
20+
mime = { workspace = true }
1821
prometheus_metrics = { workspace = true }
1922
reqwest = { workspace = true }
2023
serde = { workspace = true }

builder_api/src/api.rs

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ use anyhow::{bail, ensure, Result};
55
use bls::PublicKeyBytes;
66
use derive_more::Constructor;
77
use helper_functions::{misc, signing::SignForAllForks};
8+
use http_api_utils::ETH_CONSENSUS_VERSION;
89
use itertools::Itertools as _;
910
use log::{debug, info};
11+
use mime::{APPLICATION_JSON, APPLICATION_OCTET_STREAM};
1012
use prometheus_metrics::Metrics;
11-
use reqwest::{Client, Response, StatusCode};
12-
use ssz::SszHash as _;
13+
use reqwest::{
14+
header::{ACCEPT, CONTENT_TYPE},
15+
Client, RequestBuilder, Response, StatusCode,
16+
};
17+
use ssz::{SszHash as _, SszRead as _, SszWrite as _};
1318
use thiserror::Error;
1419
use typenum::Unsigned as _;
1520
use types::{
@@ -29,7 +34,7 @@ use crate::{
2934
combined::{ExecutionPayloadAndBlobsBundle, SignedBuilderBid},
3035
consts::BUILDER_PROPOSAL_DELAY_TOLERANCE,
3136
unphased::containers::SignedValidatorRegistrationV1,
32-
BuilderConfig,
37+
BuilderApiFormat, BuilderConfig,
3338
};
3439

3540
const REQUEST_TIMEOUT: Duration = Duration::from_secs(BUILDER_PROPOSAL_DELAY_TOLERANCE);
@@ -157,19 +162,26 @@ impl Api {
157162
debug!("getting execution payload header from {url}");
158163

159164
let response = self
160-
.client
161-
.get(url.into_url())
162-
.timeout(REQUEST_TIMEOUT)
165+
.request_with_accept_header(self.client.get(url.into_url()).timeout(REQUEST_TIMEOUT))
163166
.send()
164167
.await?;
168+
165169
let response = handle_error(response).await?;
166170

167171
if response.status() == StatusCode::NO_CONTENT {
168172
info!("builder has no execution payload header available for slot {slot}");
169173
return Ok(None);
170174
}
171175

172-
let builder_bid = response.json::<SignedBuilderBid<P>>().await?;
176+
let builder_bid = match self.config.builder_api_format {
177+
BuilderApiFormat::Json => response.json().await?,
178+
BuilderApiFormat::Ssz => {
179+
let phase = http_api_utils::extract_phase_from_headers(response.headers())?;
180+
let bytes = response.bytes().await?;
181+
182+
SignedBuilderBid::<P>::from_ssz(&phase, &bytes)?
183+
}
184+
};
173185

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

@@ -230,18 +242,33 @@ impl Api {
230242
let block_root = block.message().hash_tree_root();
231243
let slot = block.message().slot();
232244

233-
let response = self
234-
.client
235-
.post(url.into_url())
236-
.json(block)
237-
.timeout(remaining_time)
238-
.send()
239-
.await?;
245+
let request = self.request_with_accept_header(
246+
self.client
247+
.post(url.into_url())
248+
.timeout(remaining_time)
249+
.header(ETH_CONSENSUS_VERSION, block.phase().as_ref()),
250+
);
251+
252+
let request = match self.config.builder_api_format {
253+
BuilderApiFormat::Json => request.json(block),
254+
BuilderApiFormat::Ssz => request
255+
.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM.as_ref())
256+
.body(block.to_ssz()?),
257+
};
240258

259+
let response = request.send().await?;
241260
let response = handle_error(response).await?;
242-
let response: WithBlobsAndMev<ExecutionPayload<P>, P> = response
243-
.json::<ExecutionPayloadAndBlobsBundle<P>>()
244-
.await?
261+
262+
let response: WithBlobsAndMev<ExecutionPayload<P>, P> =
263+
match self.config.builder_api_format {
264+
BuilderApiFormat::Json => response.json().await?,
265+
BuilderApiFormat::Ssz => {
266+
let phase = http_api_utils::extract_phase_from_headers(response.headers())?;
267+
let bytes = response.bytes().await?;
268+
269+
ExecutionPayloadAndBlobsBundle::<P>::from_ssz(&phase, &bytes)?
270+
}
271+
}
245272
.into();
246273

247274
let execution_payload = &response.value;
@@ -266,6 +293,15 @@ impl Api {
266293
Ok(response)
267294
}
268295

296+
fn request_with_accept_header(&self, request_builder: RequestBuilder) -> RequestBuilder {
297+
let accept_header = match self.config.builder_api_format {
298+
BuilderApiFormat::Json => APPLICATION_JSON,
299+
BuilderApiFormat::Ssz => APPLICATION_OCTET_STREAM,
300+
};
301+
302+
request_builder.header(ACCEPT, accept_header.as_ref())
303+
}
304+
269305
fn url(&self, path: &str) -> Result<RedactingUrl> {
270306
self.config.builder_api_url.join(path).map_err(Into::into)
271307
}
@@ -345,6 +381,7 @@ mod tests {
345381
) -> Result<(), BuilderApiError> {
346382
let api = BuilderApi::new(
347383
BuilderConfig {
384+
builder_api_format: BuilderApiFormat::Json,
348385
builder_api_url: "http://localhost"
349386
.parse()
350387
.expect("http://localhost should be a valid URL"),

builder_api/src/bellatrix/containers.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ use types::{
1212

1313
#[derive(Debug, Deserialize, Ssz)]
1414
#[serde(bound = "", deny_unknown_fields)]
15-
#[ssz(derive_read = false, derive_size = false, derive_write = false)]
15+
#[ssz(derive_write = false)]
1616
pub struct BuilderBid<P: Preset> {
1717
pub header: Box<ExecutionPayloadHeader<P>>,
1818
pub value: Wei,
1919
pub pubkey: PublicKeyBytes,
2020
}
2121

22-
#[derive(Debug, Deserialize)]
22+
#[derive(Debug, Deserialize, Ssz)]
2323
#[serde(bound = "", deny_unknown_fields)]
24+
#[ssz(derive_write = false)]
2425
pub struct SignedBuilderBid<P: Preset> {
2526
pub message: BuilderBid<P>,
2627
pub signature: SignatureBytes,

builder_api/src/capella/containers.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ use types::{
1111

1212
#[derive(Debug, Deserialize, Ssz)]
1313
#[serde(bound = "", deny_unknown_fields)]
14-
#[ssz(derive_read = false, derive_size = false, derive_write = false)]
14+
#[ssz(derive_write = false)]
1515
pub struct BuilderBid<P: Preset> {
1616
pub header: Box<ExecutionPayloadHeader<P>>,
1717
pub value: Wei,
1818
pub pubkey: PublicKeyBytes,
1919
}
2020

21-
#[derive(Debug, Deserialize)]
21+
#[derive(Debug, Deserialize, Ssz)]
2222
#[serde(bound = "", deny_unknown_fields)]
23+
#[ssz(derive_write = false)]
2324
pub struct SignedBuilderBid<P: Preset> {
2425
pub message: BuilderBid<P>,
2526
pub signature: SignatureBytes,

builder_api/src/combined.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use bls::{PublicKeyBytes, SignatureBytes};
2+
use enum_iterator::Sequence;
23
use serde::Deserialize;
3-
use ssz::ContiguousList;
4+
use ssz::{ContiguousList, ReadError, Size, SszRead, SszReadDefault, SszSize};
45
use types::{
56
bellatrix::containers::ExecutionPayload as BellatrixExecutionPayload,
67
capella::containers::ExecutionPayload as CapellaExecutionPayload,
@@ -38,6 +39,40 @@ pub enum SignedBuilderBid<P: Preset> {
3839
Electra(ElectraSignedBuilderBid<P>),
3940
}
4041

42+
impl<P: Preset> SszSize for SignedBuilderBid<P> {
43+
// The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type.
44+
// Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable.
45+
const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 2 }>([
46+
BellatrixSignedBuilderBid::<P>::SIZE,
47+
CapellaSignedBuilderBid::<P>::SIZE,
48+
DenebSignedBuilderBid::<P>::SIZE,
49+
ElectraSignedBuilderBid::<P>::SIZE,
50+
]);
51+
}
52+
53+
impl<P: Preset> SszRead<Phase> for SignedBuilderBid<P> {
54+
fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result<Self, ReadError> {
55+
let block = match phase {
56+
Phase::Phase0 => {
57+
return Err(ReadError::Custom {
58+
message: "signed builder bid is not available in Phase 0",
59+
});
60+
}
61+
Phase::Altair => {
62+
return Err(ReadError::Custom {
63+
message: "signed builder bid is not available in Altair",
64+
});
65+
}
66+
Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?),
67+
Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?),
68+
Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?),
69+
Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?),
70+
};
71+
72+
Ok(block)
73+
}
74+
}
75+
4176
impl<P: Preset> SignedBuilderBid<P> {
4277
#[must_use]
4378
pub(crate) const fn pubkey(&self) -> PublicKeyBytes {
@@ -126,6 +161,39 @@ pub enum ExecutionPayloadAndBlobsBundle<P: Preset> {
126161
Electra(DenebExecutionPayloadAndBlobsBundle<P>),
127162
}
128163

164+
impl<P: Preset> SszSize for ExecutionPayloadAndBlobsBundle<P> {
165+
// The const parameter should be `Self::VARIANT_COUNT`, but `Self` refers to a generic type.
166+
// Type parameters cannot be used in `const` contexts until `generic_const_exprs` is stable.
167+
const SIZE: Size = Size::for_untagged_union::<{ Phase::CARDINALITY - 3 }>([
168+
BellatrixExecutionPayload::<P>::SIZE,
169+
CapellaExecutionPayload::<P>::SIZE,
170+
DenebExecutionPayloadAndBlobsBundle::<P>::SIZE,
171+
]);
172+
}
173+
174+
impl<P: Preset> SszRead<Phase> for ExecutionPayloadAndBlobsBundle<P> {
175+
fn from_ssz_unchecked(phase: &Phase, bytes: &[u8]) -> Result<Self, ReadError> {
176+
let block = match phase {
177+
Phase::Phase0 => {
178+
return Err(ReadError::Custom {
179+
message: "execution payload and blobs bundle is not available in Phase 0",
180+
});
181+
}
182+
Phase::Altair => {
183+
return Err(ReadError::Custom {
184+
message: "execution payload and blobs bundle is not available in Altair",
185+
});
186+
}
187+
Phase::Bellatrix => Self::Bellatrix(SszReadDefault::from_ssz_default(bytes)?),
188+
Phase::Capella => Self::Capella(SszReadDefault::from_ssz_default(bytes)?),
189+
Phase::Deneb => Self::Deneb(SszReadDefault::from_ssz_default(bytes)?),
190+
Phase::Electra => Self::Electra(SszReadDefault::from_ssz_default(bytes)?),
191+
};
192+
193+
Ok(block)
194+
}
195+
}
196+
129197
impl<P: Preset> From<ExecutionPayloadAndBlobsBundle<P>>
130198
for WithBlobsAndMev<ExecutionPayload<P>, P>
131199
{

builder_api/src/config.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
use derive_more::Debug;
1+
use derive_more::{Debug, Display, FromStr};
22
use types::redacting_url::RedactingUrl;
33

44
pub const DEFAULT_BUILDER_MAX_SKIPPED_SLOTS_PER_EPOCH: u64 = 8;
55
pub const DEFAULT_BUILDER_MAX_SKIPPED_SLOTS: u64 = 3;
66

7+
#[derive(Clone, Debug, Default, Display, FromStr)]
8+
pub enum BuilderApiFormat {
9+
#[default]
10+
Json,
11+
Ssz,
12+
}
13+
714
#[expect(clippy::struct_field_names)]
815
#[derive(Clone, Debug)]
916
pub struct Config {
17+
pub builder_api_format: BuilderApiFormat,
1018
pub builder_api_url: RedactingUrl,
1119
pub builder_disable_checks: bool,
1220
pub builder_max_skipped_slots_per_epoch: u64,

builder_api/src/deneb/containers.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,34 @@ use types::{
1616

1717
#[derive(Debug, Deserialize, Ssz)]
1818
#[serde(bound = "", deny_unknown_fields)]
19+
#[ssz(derive_write = false)]
1920
pub struct BuilderBid<P: Preset> {
2021
pub header: Box<ExecutionPayloadHeader<P>>,
2122
pub blob_kzg_commitments: ContiguousList<KzgCommitment, P::MaxBlobCommitmentsPerBlock>,
2223
pub value: Uint256,
2324
pub pubkey: PublicKeyBytes,
2425
}
2526

26-
#[derive(Debug, Deserialize)]
27+
#[derive(Debug, Deserialize, Ssz)]
2728
#[serde(bound = "", deny_unknown_fields)]
29+
#[ssz(derive_write = false)]
2830
pub struct SignedBuilderBid<P: Preset> {
2931
pub message: BuilderBid<P>,
3032
pub signature: SignatureBytes,
3133
}
3234

33-
#[derive(Debug, Deserialize)]
35+
#[derive(Debug, Deserialize, Ssz)]
3436
#[serde(bound = "", deny_unknown_fields)]
37+
#[ssz(derive_write = false)]
3538
pub struct BlobsBundle<P: Preset> {
3639
pub commitments: ContiguousList<KzgCommitment, P::MaxBlobCommitmentsPerBlock>,
3740
pub proofs: ContiguousList<KzgProof, P::MaxBlobCommitmentsPerBlock>,
3841
pub blobs: ContiguousList<Blob<P>, P::MaxBlobCommitmentsPerBlock>,
3942
}
4043

41-
#[derive(Debug, Deserialize)]
44+
#[derive(Debug, Deserialize, Ssz)]
4245
#[serde(bound = "", deny_unknown_fields)]
46+
#[ssz(derive_write = false)]
4347
pub struct ExecutionPayloadAndBlobsBundle<P: Preset> {
4448
pub execution_payload: ExecutionPayload<P>,
4549
pub blobs_bundle: BlobsBundle<P>,

builder_api/src/electra/containers.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use types::{
1414

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

25-
#[derive(Debug, Deserialize)]
26+
#[derive(Debug, Deserialize, Ssz)]
2627
#[serde(bound = "", deny_unknown_fields)]
28+
#[ssz(derive_write = false)]
2729
pub struct SignedBuilderBid<P: Preset> {
2830
pub message: BuilderBid<P>,
2931
pub signature: SignatureBytes,

builder_api/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pub use crate::{
22
api::Api as BuilderApi,
33
config::{
4-
Config as BuilderConfig, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS,
4+
BuilderApiFormat, Config as BuilderConfig, DEFAULT_BUILDER_MAX_SKIPPED_SLOTS,
55
DEFAULT_BUILDER_MAX_SKIPPED_SLOTS_PER_EPOCH,
66
},
77
consts::PREFERRED_EXECUTION_GAS_LIMIT,

0 commit comments

Comments
 (0)