Skip to content

Commit 522b3cb

Browse files
authored
Fix builder API headers (#7009)
Resolves #7000 Set the accept header on builder to the correct value when requesting ssz. This PR also adds a flag to disable ssz over the builder api altogether. In the case that builders/relays have an ssz bug, we can react quickly by asking clients to restart their nodes with the `--disable-ssz-builder` flag to force json. I'm not fully convinced if this is useful so open to removing it or opening another PR for it. Testing this currently.
1 parent b3b6aea commit 522b3cb

File tree

7 files changed

+149
-27
lines changed

7 files changed

+149
-27
lines changed

beacon_node/beacon_chain/src/test_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@ where
779779
SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(),
780780
None,
781781
None,
782+
false,
782783
)
783784
.unwrap();
784785

beacon_node/builder_client/src/lib.rs

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ pub const DEFAULT_GET_HEADER_TIMEOUT_MILLIS: u64 = 1000;
2929
/// Default user agent for HTTP requests.
3030
pub const DEFAULT_USER_AGENT: &str = lighthouse_version::VERSION;
3131

32+
/// The value we set on the `ACCEPT` http header to indicate a preference for ssz response.
33+
pub const PREFERENCE_ACCEPT_VALUE: &str = "application/octet-stream;q=1.0,application/json;q=0.9";
34+
/// Only accept json responses.
35+
pub const JSON_ACCEPT_VALUE: &str = "application/json";
36+
3237
#[derive(Clone)]
3338
pub struct Timeouts {
3439
get_header: Duration,
@@ -57,14 +62,20 @@ pub struct BuilderHttpClient {
5762
server: SensitiveUrl,
5863
timeouts: Timeouts,
5964
user_agent: String,
60-
ssz_enabled: Arc<AtomicBool>,
65+
/// Only use json for all requests/responses types.
66+
disable_ssz: bool,
67+
/// Indicates that the `get_header` response had content-type ssz
68+
/// so we can set content-type header to ssz to make the `submit_blinded_blocks`
69+
/// request.
70+
ssz_available: Arc<AtomicBool>,
6171
}
6272

6373
impl BuilderHttpClient {
6474
pub fn new(
6575
server: SensitiveUrl,
6676
user_agent: Option<String>,
6777
builder_header_timeout: Option<Duration>,
78+
disable_ssz: bool,
6879
) -> Result<Self, Error> {
6980
let user_agent = user_agent.unwrap_or(DEFAULT_USER_AGENT.to_string());
7081
let client = reqwest::Client::builder().user_agent(&user_agent).build()?;
@@ -73,7 +84,8 @@ impl BuilderHttpClient {
7384
server,
7485
timeouts: Timeouts::new(builder_header_timeout),
7586
user_agent,
76-
ssz_enabled: Arc::new(false.into()),
87+
disable_ssz,
88+
ssz_available: Arc::new(false.into()),
7789
})
7890
}
7991

@@ -124,15 +136,15 @@ impl BuilderHttpClient {
124136

125137
let Ok(Some(fork_name)) = self.fork_name_from_header(&headers) else {
126138
// if no fork version specified, attempt to fallback to JSON
127-
self.ssz_enabled.store(false, Ordering::SeqCst);
139+
self.ssz_available.store(false, Ordering::SeqCst);
128140
return serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson);
129141
};
130142

131143
let content_type = self.content_type_from_header(&headers);
132144

133145
match content_type {
134146
ContentType::Ssz => {
135-
self.ssz_enabled.store(true, Ordering::SeqCst);
147+
self.ssz_available.store(true, Ordering::SeqCst);
136148
T::from_ssz_bytes_by_fork(&response_bytes, fork_name)
137149
.map(|data| ForkVersionedResponse {
138150
version: Some(fork_name),
@@ -142,15 +154,17 @@ impl BuilderHttpClient {
142154
.map_err(Error::InvalidSsz)
143155
}
144156
ContentType::Json => {
145-
self.ssz_enabled.store(false, Ordering::SeqCst);
157+
self.ssz_available.store(false, Ordering::SeqCst);
146158
serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson)
147159
}
148160
}
149161
}
150162

151163
/// 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)
164+
/// Return `false` otherwise.
165+
/// Also returns `false` if we have explicitly disabled ssz.
166+
pub fn is_ssz_available(&self) -> bool {
167+
!self.disable_ssz && self.ssz_available.load(Ordering::SeqCst)
154168
}
155169

156170
async fn get_with_timeout<T: DeserializeOwned, U: IntoUrl>(
@@ -213,19 +227,14 @@ impl BuilderHttpClient {
213227
&self,
214228
url: U,
215229
ssz_body: Vec<u8>,
216-
mut headers: HeaderMap,
230+
headers: HeaderMap,
217231
timeout: Option<Duration>,
218232
) -> Result<Response, Error> {
219233
let mut builder = self.client.post(url);
220234
if let Some(timeout) = timeout {
221235
builder = builder.timeout(timeout);
222236
}
223237

224-
headers.insert(
225-
CONTENT_TYPE_HEADER,
226-
HeaderValue::from_static(SSZ_CONTENT_TYPE_HEADER),
227-
);
228-
229238
let response = builder
230239
.headers(headers)
231240
.body(ssz_body)
@@ -292,9 +301,21 @@ impl BuilderHttpClient {
292301
.push("blinded_blocks");
293302

294303
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-
}
304+
headers.insert(
305+
CONSENSUS_VERSION_HEADER,
306+
HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string())
307+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
308+
);
309+
headers.insert(
310+
CONTENT_TYPE_HEADER,
311+
HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER)
312+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
313+
);
314+
headers.insert(
315+
ACCEPT,
316+
HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE)
317+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
318+
);
298319

299320
let result = self
300321
.post_ssz_with_raw_response(
@@ -326,9 +347,21 @@ impl BuilderHttpClient {
326347
.push("blinded_blocks");
327348

328349
let mut headers = HeaderMap::new();
329-
if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) {
330-
headers.insert(CONSENSUS_VERSION_HEADER, value);
331-
}
350+
headers.insert(
351+
CONSENSUS_VERSION_HEADER,
352+
HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string())
353+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
354+
);
355+
headers.insert(
356+
CONTENT_TYPE_HEADER,
357+
HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER)
358+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
359+
);
360+
headers.insert(
361+
ACCEPT,
362+
HeaderValue::from_str(JSON_ACCEPT_VALUE)
363+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
364+
);
332365

333366
Ok(self
334367
.post_with_raw_response(
@@ -362,12 +395,20 @@ impl BuilderHttpClient {
362395
.push(pubkey.as_hex_string().as_str());
363396

364397
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-
};
398+
if self.disable_ssz {
399+
headers.insert(
400+
ACCEPT,
401+
HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER)
402+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
403+
);
404+
} else {
405+
// Indicate preference for ssz response in the accept header
406+
headers.insert(
407+
ACCEPT,
408+
HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE)
409+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
410+
);
411+
}
371412

372413
let resp = self
373414
.get_with_header(path, self.timeouts.get_header, headers)
@@ -395,3 +436,18 @@ impl BuilderHttpClient {
395436
.await
396437
}
397438
}
439+
440+
#[cfg(test)]
441+
mod tests {
442+
use super::*;
443+
444+
#[test]
445+
fn test_headers_no_panic() {
446+
for fork in ForkName::list_all() {
447+
assert!(HeaderValue::from_str(&fork.to_string()).is_ok());
448+
}
449+
assert!(HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE).is_ok());
450+
assert!(HeaderValue::from_str(JSON_ACCEPT_VALUE).is_ok());
451+
assert!(HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER).is_ok());
452+
}
453+
}

beacon_node/execution_layer/src/lib.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,8 @@ pub struct Config {
441441
pub builder_header_timeout: Option<Duration>,
442442
/// User agent to send with requests to the builder API.
443443
pub builder_user_agent: Option<String>,
444+
/// Disable ssz requests on builder. Only use json.
445+
pub disable_builder_ssz_requests: bool,
444446
/// JWT secret for the above endpoint running the engine api.
445447
pub secret_file: Option<PathBuf>,
446448
/// The default fee recipient to use on the beacon node if none if provided from
@@ -470,6 +472,7 @@ impl<E: EthSpec> ExecutionLayer<E> {
470472
builder_url,
471473
builder_user_agent,
472474
builder_header_timeout,
475+
disable_builder_ssz_requests,
473476
secret_file,
474477
suggested_fee_recipient,
475478
jwt_id,
@@ -539,7 +542,12 @@ impl<E: EthSpec> ExecutionLayer<E> {
539542
};
540543

541544
if let Some(builder_url) = builder_url {
542-
el.set_builder_url(builder_url, builder_user_agent, builder_header_timeout)?;
545+
el.set_builder_url(
546+
builder_url,
547+
builder_user_agent,
548+
builder_header_timeout,
549+
disable_builder_ssz_requests,
550+
)?;
543551
}
544552

545553
Ok(el)
@@ -562,18 +570,21 @@ impl<E: EthSpec> ExecutionLayer<E> {
562570
builder_url: SensitiveUrl,
563571
builder_user_agent: Option<String>,
564572
builder_header_timeout: Option<Duration>,
573+
disable_ssz: bool,
565574
) -> Result<(), Error> {
566575
let builder_client = BuilderHttpClient::new(
567576
builder_url.clone(),
568577
builder_user_agent,
569578
builder_header_timeout,
579+
disable_ssz,
570580
)
571581
.map_err(Error::Builder)?;
572582
info!(
573583
self.log(),
574584
"Using external block builder";
575585
"builder_url" => ?builder_url,
576586
"local_user_agent" => builder_client.get_user_agent(),
587+
"ssz_disabled" => disable_ssz
577588
);
578589
self.inner.builder.swap(Some(Arc::new(builder_client)));
579590
Ok(())
@@ -1901,7 +1912,14 @@ impl<E: EthSpec> ExecutionLayer<E> {
19011912
if let Some(builder) = self.builder() {
19021913
let (payload_result, duration) =
19031914
timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async {
1904-
if builder.is_ssz_enabled() {
1915+
let ssz_enabled = builder.is_ssz_available();
1916+
debug!(
1917+
self.log(),
1918+
"Calling submit_blinded_block on builder";
1919+
"block_root" => ?block_root,
1920+
"ssz" => ssz_enabled
1921+
);
1922+
if ssz_enabled {
19051923
builder
19061924
.post_builder_blinded_blocks_ssz(block)
19071925
.await

beacon_node/src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,15 @@ pub fn cli_app() -> Command {
14601460
.action(ArgAction::Set)
14611461
.display_order(0)
14621462
)
1463+
.arg(
1464+
Arg::new("builder-disable-ssz")
1465+
.long("builder-disable-ssz")
1466+
.value_name("BOOLEAN")
1467+
.help("Disables sending requests using SSZ over the builder API.")
1468+
.requires("builder")
1469+
.action(ArgAction::SetTrue)
1470+
.display_order(0)
1471+
)
14631472
.arg(
14641473
Arg::new("reset-payload-statuses")
14651474
.long("reset-payload-statuses")

beacon_node/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ pub fn get_config<E: EthSpec>(
346346
el_config.builder_header_timeout =
347347
clap_utils::parse_optional(cli_args, "builder-header-timeout")?
348348
.map(Duration::from_millis);
349+
350+
el_config.disable_builder_ssz_requests = cli_args.get_flag("builder-disable-ssz");
349351
}
350352

351353
// Set config values from parse values.

book/src/help_bn.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Options:
2828
network. Multiaddr is also supported.
2929
--builder <builder>
3030
The URL of a service compatible with the MEV-boost API.
31+
--builder-disable-ssz
32+
Disables sending requests using SSZ over the builder API.
3133
--builder-fallback-epochs-since-finalization <builder-fallback-epochs-since-finalization>
3234
If this node is proposing a block and the chain has not finalized
3335
within this number of epochs, it will NOT query any connected

lighthouse/tests/beacon_node.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,40 @@ fn builder_user_agent() {
720720
);
721721
}
722722

723+
#[test]
724+
fn test_builder_disable_ssz_flag() {
725+
run_payload_builder_flag_test_with_config(
726+
"builder",
727+
"http://meow.cats",
728+
None,
729+
None,
730+
|config| {
731+
assert!(
732+
!config
733+
.execution_layer
734+
.as_ref()
735+
.unwrap()
736+
.disable_builder_ssz_requests,
737+
);
738+
},
739+
);
740+
run_payload_builder_flag_test_with_config(
741+
"builder",
742+
"http://meow.cats",
743+
Some("builder-disable-ssz"),
744+
None,
745+
|config| {
746+
assert!(
747+
config
748+
.execution_layer
749+
.as_ref()
750+
.unwrap()
751+
.disable_builder_ssz_requests,
752+
);
753+
},
754+
);
755+
}
756+
723757
fn run_jwt_optional_flags_test(jwt_flag: &str, jwt_id_flag: &str, jwt_version_flag: &str) {
724758
use sensitive_url::SensitiveUrl;
725759

0 commit comments

Comments
 (0)