Skip to content

Commit 19adc85

Browse files
authored
Merge of #6859
2 parents bc2a99a + 3eee2d1 commit 19adc85

File tree

14 files changed

+406
-62
lines changed

14 files changed

+406
-62
lines changed

Cargo.lock

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

beacon_node/builder_client/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ authors = ["Sean Anderson <[email protected]>"]
66

77
[dependencies]
88
eth2 = { workspace = true }
9+
ethereum_ssz = { workspace = true }
910
lighthouse_version = { workspace = true }
1011
reqwest = { workspace = true }
1112
sensitive_url = { workspace = true }
1213
serde = { workspace = true }
14+
serde_json = { workspace = true }

beacon_node/builder_client/src/lib.rs

Lines changed: 174 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
use eth2::types::builder_bid::SignedBuilderBid;
2+
use eth2::types::fork_versioned_response::EmptyMetadata;
23
use eth2::types::{
3-
EthSpec, ExecutionBlockHash, ForkVersionedResponse, PublicKeyBytes,
4-
SignedValidatorRegistrationData, Slot,
4+
ContentType, EthSpec, ExecutionBlockHash, ForkName, ForkVersionDecode, ForkVersionDeserialize,
5+
ForkVersionedResponse, PublicKeyBytes, SignedValidatorRegistrationData, Slot,
56
};
67
use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock};
78
pub use eth2::Error;
8-
use eth2::{ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER};
9-
use reqwest::header::{HeaderMap, HeaderValue};
9+
use eth2::{
10+
ok_or_error, StatusCode, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER,
11+
JSON_CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER,
12+
};
13+
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT};
1014
use reqwest::{IntoUrl, Response};
1115
use sensitive_url::SensitiveUrl;
1216
use serde::de::DeserializeOwned;
1317
use serde::Serialize;
18+
use ssz::Encode;
19+
use std::str::FromStr;
20+
use std::sync::atomic::{AtomicBool, Ordering};
21+
use std::sync::Arc;
1422
use std::time::Duration;
1523

1624
pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000;
@@ -49,6 +57,7 @@ pub struct BuilderHttpClient {
4957
server: SensitiveUrl,
5058
timeouts: Timeouts,
5159
user_agent: String,
60+
ssz_enabled: Arc<AtomicBool>,
5261
}
5362

5463
impl BuilderHttpClient {
@@ -64,13 +73,86 @@ impl BuilderHttpClient {
6473
server,
6574
timeouts: Timeouts::new(builder_header_timeout),
6675
user_agent,
76+
ssz_enabled: Arc::new(false.into()),
6777
})
6878
}
6979

7080
pub fn get_user_agent(&self) -> &str {
7181
&self.user_agent
7282
}
7383

84+
fn fork_name_from_header(&self, headers: &HeaderMap) -> Result<Option<ForkName>, String> {
85+
headers
86+
.get(CONSENSUS_VERSION_HEADER)
87+
.map(|fork_name| {
88+
fork_name
89+
.to_str()
90+
.map_err(|e| e.to_string())
91+
.and_then(ForkName::from_str)
92+
})
93+
.transpose()
94+
}
95+
96+
fn content_type_from_header(&self, headers: &HeaderMap) -> ContentType {
97+
let Some(content_type) = headers.get(CONTENT_TYPE_HEADER).map(|content_type| {
98+
let content_type = content_type.to_str();
99+
match content_type {
100+
Ok(SSZ_CONTENT_TYPE_HEADER) => ContentType::Ssz,
101+
_ => ContentType::Json,
102+
}
103+
}) else {
104+
return ContentType::Json;
105+
};
106+
content_type
107+
}
108+
109+
async fn get_with_header<
110+
T: DeserializeOwned + ForkVersionDecode + ForkVersionDeserialize,
111+
U: IntoUrl,
112+
>(
113+
&self,
114+
url: U,
115+
timeout: Duration,
116+
headers: HeaderMap,
117+
) -> Result<ForkVersionedResponse<T>, Error> {
118+
let response = self
119+
.get_response_with_header(url, Some(timeout), headers)
120+
.await?;
121+
122+
let headers = response.headers().clone();
123+
let response_bytes = response.bytes().await?;
124+
125+
let Ok(Some(fork_name)) = self.fork_name_from_header(&headers) else {
126+
// if no fork version specified, attempt to fallback to JSON
127+
self.ssz_enabled.store(false, Ordering::SeqCst);
128+
return serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson);
129+
};
130+
131+
let content_type = self.content_type_from_header(&headers);
132+
133+
match content_type {
134+
ContentType::Ssz => {
135+
self.ssz_enabled.store(true, Ordering::SeqCst);
136+
T::from_ssz_bytes_by_fork(&response_bytes, fork_name)
137+
.map(|data| ForkVersionedResponse {
138+
version: Some(fork_name),
139+
metadata: EmptyMetadata {},
140+
data,
141+
})
142+
.map_err(Error::InvalidSsz)
143+
}
144+
ContentType::Json => {
145+
self.ssz_enabled.store(false, Ordering::SeqCst);
146+
serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson)
147+
}
148+
}
149+
}
150+
151+
/// Return `true` if the most recently received response from the builder had SSZ Content-Type.
152+
pub fn is_ssz_enabled(&self) -> bool {
153+
self.ssz_enabled.load(Ordering::SeqCst)
154+
}
155+
74156
async fn get_with_timeout<T: DeserializeOwned, U: IntoUrl>(
75157
&self,
76158
url: U,
@@ -83,6 +165,21 @@ impl BuilderHttpClient {
83165
.map_err(Into::into)
84166
}
85167

168+
/// Perform a HTTP GET request, returning the `Response` for further processing.
169+
async fn get_response_with_header<U: IntoUrl>(
170+
&self,
171+
url: U,
172+
timeout: Option<Duration>,
173+
headers: HeaderMap,
174+
) -> Result<Response, Error> {
175+
let mut builder = self.client.get(url);
176+
if let Some(timeout) = timeout {
177+
builder = builder.timeout(timeout);
178+
}
179+
let response = builder.headers(headers).send().await.map_err(Error::from)?;
180+
ok_or_error(response).await
181+
}
182+
86183
/// Perform a HTTP GET request, returning the `Response` for further processing.
87184
async fn get_response_with_timeout<U: IntoUrl>(
88185
&self,
@@ -112,6 +209,32 @@ impl BuilderHttpClient {
112209
ok_or_error(response).await
113210
}
114211

212+
async fn post_ssz_with_raw_response<U: IntoUrl>(
213+
&self,
214+
url: U,
215+
ssz_body: Vec<u8>,
216+
mut headers: HeaderMap,
217+
timeout: Option<Duration>,
218+
) -> Result<Response, Error> {
219+
let mut builder = self.client.post(url);
220+
if let Some(timeout) = timeout {
221+
builder = builder.timeout(timeout);
222+
}
223+
224+
headers.insert(
225+
CONTENT_TYPE_HEADER,
226+
HeaderValue::from_static(SSZ_CONTENT_TYPE_HEADER),
227+
);
228+
229+
let response = builder
230+
.headers(headers)
231+
.body(ssz_body)
232+
.send()
233+
.await
234+
.map_err(Error::from)?;
235+
ok_or_error(response).await
236+
}
237+
115238
async fn post_with_raw_response<T: Serialize, U: IntoUrl>(
116239
&self,
117240
url: U,
@@ -152,6 +275,42 @@ impl BuilderHttpClient {
152275
Ok(())
153276
}
154277

278+
/// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body
279+
pub async fn post_builder_blinded_blocks_ssz<E: EthSpec>(
280+
&self,
281+
blinded_block: &SignedBlindedBeaconBlock<E>,
282+
) -> Result<FullPayloadContents<E>, Error> {
283+
let mut path = self.server.full.clone();
284+
285+
let body = blinded_block.as_ssz_bytes();
286+
287+
path.path_segments_mut()
288+
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
289+
.push("eth")
290+
.push("v1")
291+
.push("builder")
292+
.push("blinded_blocks");
293+
294+
let mut headers = HeaderMap::new();
295+
if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) {
296+
headers.insert(CONSENSUS_VERSION_HEADER, value);
297+
}
298+
299+
let result = self
300+
.post_ssz_with_raw_response(
301+
path,
302+
body,
303+
headers,
304+
Some(self.timeouts.post_blinded_blocks),
305+
)
306+
.await?
307+
.bytes()
308+
.await?;
309+
310+
FullPayloadContents::from_ssz_bytes_by_fork(&result, blinded_block.fork_name_unchecked())
311+
.map_err(Error::InvalidSsz)
312+
}
313+
155314
/// `POST /eth/v1/builder/blinded_blocks`
156315
pub async fn post_builder_blinded_blocks<E: EthSpec>(
157316
&self,
@@ -202,7 +361,17 @@ impl BuilderHttpClient {
202361
.push(format!("{parent_hash:?}").as_str())
203362
.push(pubkey.as_hex_string().as_str());
204363

205-
let resp = self.get_with_timeout(path, self.timeouts.get_header).await;
364+
let mut headers = HeaderMap::new();
365+
if let Ok(ssz_content_type_header) = HeaderValue::from_str(&format!(
366+
"{}; q=1.0,{}; q=0.9",
367+
SSZ_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER
368+
)) {
369+
headers.insert(ACCEPT, ssz_content_type_header);
370+
};
371+
372+
let resp = self
373+
.get_with_header(path, self.timeouts.get_header, headers)
374+
.await;
206375

207376
if matches!(resp, Err(Error::StatusCode(StatusCode::NO_CONTENT))) {
208377
Ok(None)

beacon_node/execution_layer/src/lib.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,11 +1900,18 @@ impl<E: EthSpec> ExecutionLayer<E> {
19001900
if let Some(builder) = self.builder() {
19011901
let (payload_result, duration) =
19021902
timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async {
1903-
builder
1904-
.post_builder_blinded_blocks(block)
1905-
.await
1906-
.map_err(Error::Builder)
1907-
.map(|d| d.data)
1903+
if builder.is_ssz_enabled() {
1904+
builder
1905+
.post_builder_blinded_blocks_ssz(block)
1906+
.await
1907+
.map_err(Error::Builder)
1908+
} else {
1909+
builder
1910+
.post_builder_blinded_blocks(block)
1911+
.await
1912+
.map_err(Error::Builder)
1913+
.map(|d| d.data)
1914+
}
19081915
})
19091916
.await;
19101917

0 commit comments

Comments
 (0)