Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bad5b08
Update to v0.9.1 (#398)
jclapis Nov 4, 2025
3e4a7da
Changed get_accept_type() to allow multiple types
jclapis Nov 4, 2025
29398f6
Update to v0.9.2 (#400)
jclapis Nov 5, 2025
7ab1f7e
get_header()'s impl now works with multiple types
jclapis Nov 6, 2025
bfbcfe4
Added retry-different-accept-types thing to get_header
jclapis Nov 7, 2025
adfec62
Refactored and added some unit tests
jclapis Nov 10, 2025
b22eed8
Added explicit lowercase matching to EncodingType
jclapis Nov 10, 2025
0155533
Added the Fulu fork slot for Mainnet
jclapis Nov 10, 2025
a975048
Merge branch 'update-fulu-slot' into ssz-update-v2
jclapis Nov 10, 2025
3ad487b
Added the Fulu fork slot for Mainnet (#402)
jclapis Nov 10, 2025
d64adbb
Merge branch 'main' into ssz-update-v2
jclapis Nov 10, 2025
8ebfbd0
Cleaned up some error handling
jclapis Nov 10, 2025
2499bd5
Made some strings static
jclapis Nov 10, 2025
41d879e
PbsClientError can noe be created from BodyDeserializeError
jclapis Nov 10, 2025
42b8060
Fix clippy
jclapis Nov 11, 2025
a9680f7
Removed consensus-version-header from submit_block response
jclapis Nov 11, 2025
d7bde7f
Added multi-type support to submit_block
jclapis Nov 11, 2025
ad3f019
Updated the mock relay with multi-type support on submit_block
jclapis Nov 11, 2025
aaa0967
Updated the submit_block unit tests
jclapis Nov 11, 2025
83ca8f8
Added more multitype tests to submit_block, not done yet though
jclapis Nov 12, 2025
d36ef3b
Switched relay response handling to switch to JSON on any 4xx
jclapis Nov 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
rust-version = "1.89"
version = "0.9.0"
version = "0.9.2"

[workspace.dependencies]
aes = "0.8"
Expand Down
3 changes: 3 additions & 0 deletions crates/common/src/pbs/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub enum PbsError {
#[error("json decode error: {err:?}, raw: {raw}")]
JsonDecode { err: serde_json::Error, raw: String },

#[error("error with request: {0}")]
GeneralRequest(String),

#[error("{0}")]
ReadResponse(#[from] ResponseReadError),

Expand Down
1 change: 1 addition & 0 deletions crates/common/src/pbs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ mod types;

pub use builder::*;
pub use constants::*;
pub use lh_types::ForkVersionDecode;
pub use relay::*;
pub use types::*;
3 changes: 2 additions & 1 deletion crates/common/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ impl KnownChain {

pub fn fulu_fork_slot(&self) -> u64 {
match self {
KnownChain::Mainnet | KnownChain::Helder => u64::MAX,
KnownChain::Mainnet => 13164544,
KnownChain::Helder => u64::MAX,
KnownChain::Holesky => 5283840,
KnownChain::Sepolia => 8724480,
KnownChain::Hoodi => 1622016,
Expand Down
190 changes: 151 additions & 39 deletions crates/common/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[cfg(feature = "testing-flags")]
use std::cell::Cell;
use std::{
collections::HashSet,
fmt::Display,
net::Ipv4Addr,
str::FromStr,
Expand Down Expand Up @@ -45,9 +46,9 @@ use crate::{
types::{BlsPublicKey, Chain, Jwt, JwtClaims, ModuleId},
};

const APPLICATION_JSON: &str = "application/json";
const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
const WILDCARD: &str = "*/*";
pub const APPLICATION_JSON: &str = "application/json";
pub const APPLICATION_OCTET_STREAM: &str = "application/octet-stream";
pub const WILDCARD: &str = "*/*";

const MILLIS_PER_SECOND: u64 = 1_000;
pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version";
Expand Down Expand Up @@ -433,36 +434,34 @@ pub fn get_user_agent_with_version(req_headers: &HeaderMap) -> eyre::Result<Head
/// defaulting to JSON if missing. Returns an error if malformed or unsupported
/// types are requested. Supports requests with multiple ACCEPT headers or
/// headers with multiple media types.
pub fn get_accept_type(req_headers: &HeaderMap) -> eyre::Result<EncodingType> {
let accept = Accept::from_str(
req_headers.get(ACCEPT).and_then(|value| value.to_str().ok()).unwrap_or(APPLICATION_JSON),
)
.map_err(|e| eyre::eyre!("invalid accept header: {e}"))?;

if accept.media_types().count() == 0 {
// No valid media types found, default to JSON
return Ok(EncodingType::Json);
}

// Get the SSZ and JSON media types if present
let mut ssz_type = false;
let mut json_type = false;
pub fn get_accept_types(req_headers: &HeaderMap) -> eyre::Result<HashSet<EncodingType>> {
let mut accepted_types = HashSet::new();
let mut unsupported_type = false;
accept.media_types().for_each(|mt| match mt.essence().to_string().as_str() {
APPLICATION_OCTET_STREAM => ssz_type = true,
APPLICATION_JSON | WILDCARD => json_type = true,
_ => unsupported_type = true,
});

// If SSZ is present, prioritize it
if ssz_type {
return Ok(EncodingType::Ssz);
for header in req_headers.get_all(ACCEPT).iter() {
let accept = Accept::from_str(header.to_str()?)
.map_err(|e| eyre::eyre!("invalid accept header: {e}"))?;
for mt in accept.media_types() {
match mt.essence().to_string().as_str() {
APPLICATION_OCTET_STREAM => {
accepted_types.insert(EncodingType::Ssz);
}
APPLICATION_JSON | WILDCARD => {
accepted_types.insert(EncodingType::Json);
}
_ => unsupported_type = true,
};
}
}
// If there aren't any unsupported types, use JSON
if !unsupported_type {
return Ok(EncodingType::Json);

if accepted_types.is_empty() {
if unsupported_type {
return Err(eyre::eyre!("unsupported accept type"));
}

// No accept header so just return the same type as the content type
accepted_types.insert(get_content_type(req_headers));
}
Err(eyre::eyre!("unsupported accept type"))
Ok(accepted_types)
}

/// Parse CONTENT TYPE header to get the encoding type of the body, defaulting
Expand Down Expand Up @@ -490,7 +489,7 @@ pub fn get_consensus_version_header(req_headers: &HeaderMap) -> Option<ForkName>

/// Enum for types that can be used to encode incoming request bodies or
/// outgoing response bodies
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EncodingType {
/// Body is UTF-8 encoded as JSON
Json,
Expand All @@ -499,21 +498,28 @@ pub enum EncodingType {
Ssz,
}

impl std::fmt::Display for EncodingType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl EncodingType {
/// Get the content type string for the encoding type
pub fn content_type(&self) -> &str {
match self {
EncodingType::Json => write!(f, "application/json"),
EncodingType::Ssz => write!(f, "application/octet-stream"),
EncodingType::Json => APPLICATION_JSON,
EncodingType::Ssz => APPLICATION_OCTET_STREAM,
}
}
}

impl std::fmt::Display for EncodingType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.content_type())
}
}

impl FromStr for EncodingType {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"application/json" | "" => Ok(EncodingType::Json),
"application/octet-stream" => Ok(EncodingType::Ssz),
match value.to_ascii_lowercase().as_str() {
APPLICATION_JSON | "" => Ok(EncodingType::Json),
APPLICATION_OCTET_STREAM => Ok(EncodingType::Ssz),
_ => Err(format!("unsupported encoding type: {value}")),
}
}
Expand Down Expand Up @@ -636,8 +642,18 @@ pub fn bls_pubkey_from_hex_unchecked(hex: &str) -> BlsPublicKey {

#[cfg(test)]
mod test {
use axum::http::{HeaderMap, HeaderValue};
use reqwest::header::ACCEPT;

use super::{create_jwt, decode_jwt, validate_jwt};
use crate::types::{Jwt, ModuleId};
use crate::{
types::{Jwt, ModuleId},
utils::{
APPLICATION_JSON, APPLICATION_OCTET_STREAM, EncodingType, WILDCARD, get_accept_types,
},
};

const APPLICATION_TEXT: &str = "application/text";

#[test]
fn test_jwt_validation() {
Expand All @@ -660,4 +676,100 @@ mod test {
assert!(response.is_err());
assert_eq!(response.unwrap_err().to_string(), "InvalidSignature");
}

/// Make sure a missing Accept header is interpreted as JSON
#[test]
fn test_missing_accept_header() {
let headers = HeaderMap::new();
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&EncodingType::Json));
}

/// Test accepting JSON
#[test]
fn test_accept_header_json() {
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(APPLICATION_JSON).unwrap());
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&EncodingType::Json));
}

/// Test accepting SSZ
#[test]
fn test_accept_header_ssz() {
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(APPLICATION_OCTET_STREAM).unwrap());
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&EncodingType::Ssz));
}

/// Test accepting wildcards
#[test]
fn test_accept_header_wildcard() {
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(WILDCARD).unwrap());
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains(&EncodingType::Json));
}

/// Test accepting one header with multiple values
#[test]
fn test_accept_header_multiple_values() {
let header_string = format!("{APPLICATION_JSON}, {APPLICATION_OCTET_STREAM}");
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(&header_string).unwrap());
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&EncodingType::Json));
assert!(result.contains(&EncodingType::Ssz));
}

/// Test accepting multiple headers
#[test]
fn test_multiple_accept_headers() {
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(APPLICATION_JSON).unwrap());
headers.append(ACCEPT, HeaderValue::from_str(APPLICATION_OCTET_STREAM).unwrap());
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&EncodingType::Json));
assert!(result.contains(&EncodingType::Ssz));
}

/// Test accepting one header with multiple values, including a type that
/// can't be used
#[test]
fn test_accept_header_multiple_values_including_unknown() {
let header_string =
format!("{APPLICATION_JSON}, {APPLICATION_OCTET_STREAM}, {APPLICATION_TEXT}");
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(&header_string).unwrap());
let result = get_accept_types(&headers).unwrap();
assert_eq!(result.len(), 2);
assert!(result.contains(&EncodingType::Json));
assert!(result.contains(&EncodingType::Ssz));
}

/// Test rejecting an unknown accept type
#[test]
fn test_invalid_accept_header_type() {
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(APPLICATION_TEXT).unwrap());
let result = get_accept_types(&headers);
assert!(result.is_err());
}

/// Test accepting one header with multiple values
#[test]
fn test_accept_header_invalid_parse() {
let header_string = format!("{APPLICATION_JSON}, a?;ef)");
let mut headers = HeaderMap::new();
headers.append(ACCEPT, HeaderValue::from_str(&header_string).unwrap());
let result = get_accept_types(&headers);
assert!(result.is_err());
}
}
1 change: 1 addition & 0 deletions crates/pbs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ cb-metrics.workspace = true
ethereum_ssz.workspace = true
eyre.workspace = true
futures.workspace = true
headers.workspace = true
lazy_static.workspace = true
parking_lot.workspace = true
prometheus.workspace = true
Expand Down
Loading
Loading