Skip to content

Commit b994451

Browse files
authored
Add SSZ types, content negotiation, and encoding helpers (#466)
1 parent 5b3d5b3 commit b994451

25 files changed

Lines changed: 1202 additions & 220 deletions

Cargo.lock

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ ethereum_ssz_derive = "0.10"
4949
eyre = "0.6.12"
5050
futures = "0.3.30"
5151
headers = "0.4.0"
52+
headers-accept = "0.2.1"
5253
indexmap = "2.2.6"
5354
jsonwebtoken = { version = "9.3.1", default-features = false }
5455
lazy_static = "1.5.0"
56+
mediatype = "0.20.0"
5557
lh_eth2 = { package = "eth2", git = "https://github.com/sigp/lighthouse", tag = "v8.1.3", features = ["events"] }
5658
lh_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", tag = "v8.1.3" }
5759
lh_bls = { package = "bls", git = "https://github.com/sigp/lighthouse", tag = "v8.1.3" }

crates/common/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ ethereum_ssz.workspace = true
2626
ethereum_ssz_derive.workspace = true
2727
eyre.workspace = true
2828
futures.workspace = true
29+
headers-accept.workspace = true
2930
jsonwebtoken.workspace = true
3031
lazy_static.workspace = true
3132
lh_bls.workspace = true
3233
lh_eth2.workspace = true
3334
lh_eth2_keystore.workspace = true
3435
lh_types.workspace = true
36+
mediatype.workspace = true
3537
notify.workspace = true
3638
pbkdf2.workspace = true
3739
rand.workspace = true

crates/common/src/config/mux.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ use serde::{Deserialize, Serialize};
1818
use tracing::{debug, info, warn};
1919
use url::Url;
2020

21-
use super::{MUX_PATH_ENV, PbsConfig, RelayConfig, load_optional_env_var};
21+
use super::{MUX_PATH_ENV, MUXER_HTTP_MAX_LENGTH, PbsConfig, RelayConfig, load_optional_env_var};
2222
use crate::{
23-
config::{remove_duplicate_keys, safe_read_http_response},
23+
config::remove_duplicate_keys,
2424
interop::{lido::utils::*, ssv::utils::*},
2525
pbs::RelayClient,
2626
types::{BlsPublicKey, Chain},
2727
utils::default_bool,
28+
wire::safe_read_http_response,
2829
};
2930

3031
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -156,7 +157,7 @@ pub struct MuxConfig {
156157

157158
impl MuxConfig {
158159
/// Returns the env, actual path, and internal path to use for the file
159-
/// loader. In File mode, validates the mux file prior to returning.
160+
/// loader. In File mode, validates the mux file prior to returning.
160161
pub fn loader_env(&self) -> eyre::Result<Option<(String, String, String)>> {
161162
let Some(loader) = self.loader.as_ref() else {
162163
return Ok(None);
@@ -235,9 +236,10 @@ impl MuxKeysLoader {
235236
"Mux keys URL {url} is insecure; consider using HTTPS if possible instead"
236237
);
237238
}
239+
let url = url.as_str();
238240
let client = reqwest::ClientBuilder::new().timeout(http_timeout).build()?;
239241
let response = client.get(url).send().await?;
240-
let pubkey_bytes = safe_read_http_response(response).await?;
242+
let pubkey_bytes = safe_read_http_response(response, MUXER_HTTP_MAX_LENGTH).await?;
241243
serde_json::from_slice(&pubkey_bytes)
242244
.wrap_err("failed to fetch mux keys from HTTP endpoint")
243245
}

crates/common/src/config/utils.rs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ use eyre::{Context, Result, bail};
77
use serde::de::DeserializeOwned;
88

99
use crate::{
10-
config::{ADMIN_JWT_ENV, JWTS_ENV, MUXER_HTTP_MAX_LENGTH},
10+
config::{ADMIN_JWT_ENV, JWTS_ENV},
1111
types::{BlsPublicKey, ModuleId},
12-
utils::read_chunked_body_with_max,
1312
};
1413

1514
pub fn load_env_var(env: &str) -> Result<String> {
@@ -42,32 +41,6 @@ pub fn load_jwt_secrets() -> Result<(String, HashMap<ModuleId, String>)> {
4241
decode_string_to_map(&jwt_secrets).map(|secrets| (admin_jwt, secrets))
4342
}
4443

45-
/// Reads an HTTP response safely, erroring out if it failed or if the body is
46-
/// too large.
47-
pub async fn safe_read_http_response(response: reqwest::Response) -> Result<Vec<u8>> {
48-
// Read the response to a buffer in chunks
49-
let status_code = response.status();
50-
match read_chunked_body_with_max(response, MUXER_HTTP_MAX_LENGTH).await {
51-
Ok(response_bytes) => {
52-
if status_code.is_success() {
53-
return Ok(response_bytes);
54-
}
55-
bail!(
56-
"Request failed with status: {status_code}, body: {}",
57-
String::from_utf8_lossy(&response_bytes)
58-
)
59-
}
60-
Err(e) => {
61-
if status_code.is_success() {
62-
return Err(e).wrap_err("Failed to read response body");
63-
}
64-
Err(e).wrap_err(format!(
65-
"Request failed with status {status_code}, but decoding the response body failed"
66-
))
67-
}
68-
}
69-
}
70-
7144
/// Removes duplicate entries from a vector of BlsPublicKey
7245
pub fn remove_duplicate_keys(keys: Vec<BlsPublicKey>) -> Vec<BlsPublicKey> {
7346
let mut unique_keys = Vec::new();

crates/common/src/interop/ssv/utils.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ use serde_json::json;
66
use url::Url;
77

88
use crate::{
9-
config::safe_read_http_response,
9+
config::MUXER_HTTP_MAX_LENGTH,
1010
interop::ssv::types::{SSVNodeResponse, SSVPublicResponse},
11+
wire::safe_read_http_response,
1112
};
1213

1314
pub async fn request_ssv_pubkeys_from_ssv_node(
@@ -28,7 +29,7 @@ pub async fn request_ssv_pubkeys_from_ssv_node(
2829
})?;
2930

3031
// Parse the response as JSON
31-
let body_bytes = safe_read_http_response(response).await?;
32+
let body_bytes = safe_read_http_response(response, MUXER_HTTP_MAX_LENGTH).await?;
3233
serde_json::from_slice::<SSVNodeResponse>(&body_bytes).wrap_err("failed to parse SSV response")
3334
}
3435

@@ -46,7 +47,7 @@ pub async fn request_ssv_pubkeys_from_public_api(
4647
})?;
4748

4849
// Parse the response as JSON
49-
let body_bytes = safe_read_http_response(response).await?;
50+
let body_bytes = safe_read_http_response(response, MUXER_HTTP_MAX_LENGTH).await?;
5051
serde_json::from_slice::<SSVPublicResponse>(&body_bytes)
5152
.wrap_err("failed to parse SSV response")
5253
}

crates/common/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ pub mod interop;
77
pub mod pbs;
88
pub mod signature;
99
pub mod signer;
10+
pub mod ssz;
1011
pub mod types;
1112
pub mod utils;
13+
pub mod wire;
1214

1315
pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);

crates/common/src/pbs/error.rs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use alloy::primitives::{B256, U256};
2+
use lh_types::ForkName;
23
use thiserror::Error;
34

4-
use crate::{types::BlsPublicKeyBytes, utils::ResponseReadError};
5+
use crate::{types::BlsPublicKeyBytes, wire::ResponseReadError};
56

67
#[derive(Debug, Error)]
78
pub enum PbsError {
@@ -28,31 +29,37 @@ pub enum PbsError {
2829

2930
#[error("tokio join error: {0}")]
3031
TokioJoinError(#[from] tokio::task::JoinError),
32+
33+
#[error("SSZ error: {0}")]
34+
SszError(#[from] SszValueError),
3135
}
3236

3337
impl PbsError {
3438
pub fn is_timeout(&self) -> bool {
3539
matches!(self, PbsError::Reqwest(err) if err.is_timeout())
3640
}
3741

42+
/// Extract the HTTP status code from relay-originated errors.
43+
fn relay_status_code(&self) -> Option<u16> {
44+
match self {
45+
PbsError::RelayResponse { code, .. } => Some(*code),
46+
PbsError::ReadResponse(ResponseReadError::NonSuccess { status_code, .. }) => {
47+
Some(*status_code)
48+
}
49+
_ => None,
50+
}
51+
}
52+
3853
/// Whether the error is retryable in requests to relays
3954
pub fn should_retry(&self) -> bool {
4055
match self {
41-
PbsError::Reqwest(err) => {
42-
// Retry on timeout or connection error
43-
err.is_timeout() || err.is_connect()
44-
}
45-
PbsError::RelayResponse { code, .. } => match *code {
46-
500..509 => true, // Retry on server errors
47-
400 | 429 => false, // Do not retry if rate limited or bad request
48-
_ => false,
49-
},
50-
_ => false,
56+
PbsError::Reqwest(err) => err.is_timeout() || err.is_connect(),
57+
_ => matches!(self.relay_status_code(), Some(500..=508)),
5158
}
5259
}
5360

5461
pub fn is_not_found(&self) -> bool {
55-
matches!(&self, PbsError::RelayResponse { code: 404, .. })
62+
self.relay_status_code() == Some(404)
5663
}
5764
}
5865

@@ -107,3 +114,12 @@ pub enum ValidationError {
107114
#[error("unsupported fork")]
108115
UnsupportedFork,
109116
}
117+
118+
#[derive(Debug, Error, PartialEq, Eq)]
119+
pub enum SszValueError {
120+
#[error("invalid payload length: required {required} but payload was {actual}")]
121+
InvalidPayloadLength { required: usize, actual: usize },
122+
123+
#[error("unsupported fork: {name}")]
124+
UnsupportedFork { name: ForkName },
125+
}

crates/common/src/pbs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ mod types;
66

77
pub use builder::*;
88
pub use constants::*;
9+
pub use lh_types::ForkVersionDecode;
910
pub use relay::*;
1011
pub use types::*;

crates/common/src/pbs/types/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloy::primitives::{B256, U256, b256};
2-
use lh_eth2::ForkVersionedResponse;
2+
pub use lh_eth2::ForkVersionedResponse;
33
pub use lh_types::ForkName;
44
use lh_types::{BlindedPayload, ExecPayload, MainnetEthSpec};
55
use serde::{Deserialize, Serialize};
@@ -26,6 +26,10 @@ pub type PayloadAndBlobs = lh_eth2::types::ExecutionPayloadAndBlobs<MainnetEthSp
2626
pub type SubmitBlindedBlockResponse = ForkVersionedResponse<PayloadAndBlobs>;
2727

2828
pub type ExecutionPayloadHeader = lh_types::ExecutionPayloadHeader<MainnetEthSpec>;
29+
pub type ExecutionPayloadHeaderBellatrix =
30+
lh_types::ExecutionPayloadHeaderBellatrix<MainnetEthSpec>;
31+
pub type ExecutionPayloadHeaderCapella = lh_types::ExecutionPayloadHeaderCapella<MainnetEthSpec>;
32+
pub type ExecutionPayloadHeaderDeneb = lh_types::ExecutionPayloadHeaderDeneb<MainnetEthSpec>;
2933
pub type ExecutionPayloadHeaderElectra = lh_types::ExecutionPayloadHeaderElectra<MainnetEthSpec>;
3034
pub type ExecutionPayloadHeaderFulu = lh_types::ExecutionPayloadHeaderFulu<MainnetEthSpec>;
3135
pub type ExecutionPayloadHeaderRef<'a> = lh_types::ExecutionPayloadHeaderRef<'a, MainnetEthSpec>;
@@ -34,7 +38,11 @@ pub type ExecutionPayloadElectra = lh_types::ExecutionPayloadElectra<MainnetEthS
3438
pub type ExecutionPayloadFulu = lh_types::ExecutionPayloadFulu<MainnetEthSpec>;
3539
pub type SignedBuilderBid = lh_types::SignedBuilderBid<MainnetEthSpec>;
3640
pub type BuilderBid = lh_types::BuilderBid<MainnetEthSpec>;
41+
pub type BuilderBidBellatrix = lh_types::BuilderBidBellatrix<MainnetEthSpec>;
42+
pub type BuilderBidCapella = lh_types::BuilderBidCapella<MainnetEthSpec>;
43+
pub type BuilderBidDeneb = lh_types::BuilderBidDeneb<MainnetEthSpec>;
3744
pub type BuilderBidElectra = lh_types::BuilderBidElectra<MainnetEthSpec>;
45+
pub type BuilderBidFulu = lh_types::BuilderBidFulu<MainnetEthSpec>;
3846

3947
/// Response object of GET
4048
/// `/eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}`

0 commit comments

Comments
 (0)