-
Notifications
You must be signed in to change notification settings - Fork 56
Add SSZ to PBS #372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add SSZ to PBS #372
Changes from 10 commits
3a2c7d5
5059593
35b8542
4acddbe
7005422
06c207f
24fabca
f0875cd
bad5675
50fccb5
86fa858
e7335f6
8c82b84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,27 @@ | ||
#[cfg(test)] | ||
use std::cell::Cell; | ||
use std::{ | ||
fmt, | ||
net::Ipv4Addr, | ||
str::FromStr, | ||
time::{SystemTime, UNIX_EPOCH}, | ||
}; | ||
|
||
use alloy::{hex, primitives::U256}; | ||
use axum::http::HeaderValue; | ||
use axum::{ | ||
extract::{FromRequest, Request}, | ||
http::HeaderValue, | ||
response::{IntoResponse, Response as AxumResponse}, | ||
}; | ||
use bytes::Bytes; | ||
use futures::StreamExt; | ||
use lh_types::test_utils::{SeedableRng, TestRandom, XorShiftRng}; | ||
use mediatype::{MediaType, MediaTypeList, names}; | ||
use rand::{Rng, distr::Alphanumeric}; | ||
use reqwest::{Response, header::HeaderMap}; | ||
use reqwest::{ | ||
Response, StatusCode, | ||
header::{ACCEPT, CONTENT_TYPE, HeaderMap}, | ||
}; | ||
use serde::{Serialize, de::DeserializeOwned}; | ||
use serde_json::Value; | ||
use ssz::{Decode, Encode}; | ||
|
@@ -31,6 +42,7 @@ use crate::{ | |
}; | ||
|
||
const MILLIS_PER_SECOND: u64 = 1_000; | ||
pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum ResponseReadError { | ||
|
@@ -408,6 +420,189 @@ pub fn get_user_agent_with_version(req_headers: &HeaderMap) -> eyre::Result<Head | |
Ok(HeaderValue::from_str(&format!("commit-boost/{HEADER_VERSION_VALUE} {ua}"))?) | ||
} | ||
|
||
/// Parse ACCEPT header, default to JSON if missing or mal-formatted | ||
pub fn get_accept_header(req_headers: &HeaderMap) -> Accept { | ||
Accept::from_str( | ||
req_headers.get(ACCEPT).and_then(|value| value.to_str().ok()).unwrap_or("application/json"), | ||
) | ||
.unwrap_or(Accept::Json) | ||
} | ||
|
||
/// Parse CONTENT TYPE header, default to JSON if missing or mal-formatted | ||
pub fn get_content_type_header(req_headers: &HeaderMap) -> ContentType { | ||
ContentType::from_str( | ||
req_headers | ||
.get(CONTENT_TYPE) | ||
.and_then(|value| value.to_str().ok()) | ||
.unwrap_or("application/json"), | ||
) | ||
.unwrap_or(ContentType::Json) | ||
} | ||
|
||
/// Parse CONSENSUS_VERSION header | ||
pub fn get_consensus_version_header(req_headers: &HeaderMap) -> Option<ForkName> { | ||
ForkName::from_str( | ||
req_headers | ||
.get(CONSENSUS_VERSION_HEADER) | ||
.and_then(|value| value.to_str().ok()) | ||
.unwrap_or(""), | ||
) | ||
.ok() | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
pub enum ForkName { | ||
Electra, | ||
} | ||
|
||
impl std::fmt::Display for ForkName { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
ForkName::Electra => write!(f, "electra"), | ||
} | ||
} | ||
} | ||
|
||
impl FromStr for ForkName { | ||
type Err = String; | ||
fn from_str(value: &str) -> Result<Self, Self::Err> { | ||
match value { | ||
"electra" => Ok(ForkName::Electra), | ||
_ => Err(format!("Invalid fork name {value}")), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
pub enum ContentType { | ||
Json, | ||
Ssz, | ||
} | ||
|
||
impl std::fmt::Display for ContentType { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
ContentType::Json => write!(f, "application/json"), | ||
ContentType::Ssz => write!(f, "application/octet-stream"), | ||
} | ||
} | ||
} | ||
|
||
impl FromStr for ContentType { | ||
type Err = String; | ||
fn from_str(value: &str) -> Result<Self, Self::Err> { | ||
match value { | ||
"application/json" => Ok(ContentType::Json), | ||
"application/octet-stream" => Ok(ContentType::Ssz), | ||
_ => Ok(ContentType::Json), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, PartialEq)] | ||
pub enum Accept { | ||
Json, | ||
Ssz, | ||
Any, | ||
} | ||
|
||
impl fmt::Display for Accept { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
Accept::Ssz => write!(f, "application/octet-stream"), | ||
Accept::Json => write!(f, "application/json"), | ||
Accept::Any => write!(f, "*/*"), | ||
} | ||
} | ||
} | ||
|
||
impl FromStr for Accept { | ||
|
||
type Err = String; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
let media_type_list = MediaTypeList::new(s); | ||
|
||
// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 | ||
// find the highest q-factor supported accept type | ||
let mut highest_q = 0_u16; | ||
let mut accept_type = None; | ||
|
||
const APPLICATION: &str = names::APPLICATION.as_str(); | ||
const OCTET_STREAM: &str = names::OCTET_STREAM.as_str(); | ||
const JSON: &str = names::JSON.as_str(); | ||
const STAR: &str = names::_STAR.as_str(); | ||
const Q: &str = names::Q.as_str(); | ||
|
||
media_type_list.into_iter().for_each(|item| { | ||
if let Ok(MediaType { ty, subty, suffix: _, params }) = item { | ||
let q_accept = match (ty.as_str(), subty.as_str()) { | ||
(APPLICATION, OCTET_STREAM) => Some(Accept::Ssz), | ||
(APPLICATION, JSON) => Some(Accept::Json), | ||
(STAR, STAR) => Some(Accept::Any), | ||
_ => None, | ||
} | ||
.map(|item_accept_type| { | ||
let q_val = params | ||
.iter() | ||
.find_map(|(n, v)| match n.as_str() { | ||
Q => { | ||
Some((v.as_str().parse::<f32>().unwrap_or(0_f32) * 1000_f32) as u16) | ||
} | ||
_ => None, | ||
}) | ||
.or(Some(1000_u16)); | ||
|
||
(q_val.unwrap(), item_accept_type) | ||
}); | ||
|
||
match q_accept { | ||
Some((q, accept)) if q > highest_q => { | ||
highest_q = q; | ||
accept_type = Some(accept); | ||
} | ||
_ => (), | ||
} | ||
} | ||
}); | ||
accept_type.ok_or_else(|| "accept header is not supported".to_string()) | ||
} | ||
} | ||
|
||
#[must_use] | ||
#[derive(Debug, Clone, Copy, Default)] | ||
pub struct JsonOrSsz<T>(pub T); | ||
|
||
|
||
impl<T, S> FromRequest<S> for JsonOrSsz<T> | ||
where | ||
T: serde::de::DeserializeOwned + ssz::Decode + 'static, | ||
S: Send + Sync, | ||
{ | ||
type Rejection = AxumResponse; | ||
|
||
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> { | ||
let headers = req.headers().clone(); | ||
let content_type = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()); | ||
|
||
let bytes = Bytes::from_request(req, _state).await.map_err(IntoResponse::into_response)?; | ||
|
||
if let Some(content_type) = content_type { | ||
if content_type.starts_with(&ContentType::Json.to_string()) { | ||
let payload: T = serde_json::from_slice(&bytes) | ||
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?; | ||
return Ok(Self(payload)); | ||
} | ||
|
||
if content_type.starts_with(&ContentType::Ssz.to_string()) { | ||
let payload = T::from_ssz_bytes(&bytes) | ||
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?; | ||
return Ok(Self(payload)); | ||
} | ||
} | ||
|
||
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) | ||
} | ||
} | ||
|
||
#[cfg(unix)] | ||
pub async fn wait_for_signal() -> eyre::Result<()> { | ||
use tokio::signal::unix::{SignalKind, signal}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,15 @@ | ||
use alloy::primitives::utils::format_ether; | ||
use axum::{ | ||
extract::{Path, State}, | ||
http::HeaderMap, | ||
http::{HeaderMap, HeaderValue}, | ||
response::IntoResponse, | ||
}; | ||
use cb_common::{ | ||
pbs::GetHeaderParams, | ||
utils::{get_user_agent, ms_into_slot}, | ||
pbs::{GetHeaderParams, VersionedResponse}, | ||
utils::{Accept, CONSENSUS_VERSION_HEADER, get_accept_header, get_user_agent, ms_into_slot}, | ||
}; | ||
use reqwest::StatusCode; | ||
use reqwest::{StatusCode, header::CONTENT_TYPE}; | ||
use ssz::Encode; | ||
use tracing::{error, info}; | ||
|
||
use crate::{ | ||
|
@@ -32,16 +33,44 @@ pub async fn handle_get_header<S: BuilderApiState, A: BuilderApi<S>>( | |
|
||
let ua = get_user_agent(&req_headers); | ||
let ms_into_slot = ms_into_slot(params.slot, state.config.chain); | ||
let accept_header = get_accept_header(&req_headers); | ||
|
||
info!(ua, ms_into_slot, "new request"); | ||
|
||
match A::get_header(params, req_headers, state.clone()).await { | ||
Ok(res) => { | ||
if let Some(max_bid) = res { | ||
info!(value_eth = format_ether(max_bid.value()), block_hash =% max_bid.block_hash(), "received header"); | ||
|
||
BEACON_NODE_STATUS.with_label_values(&["200", GET_HEADER_ENDPOINT_TAG]).inc(); | ||
Ok((StatusCode::OK, axum::Json(max_bid)).into_response()) | ||
let response = match accept_header { | ||
|
||
Accept::Ssz => { | ||
let mut res = match &max_bid { | ||
VersionedResponse::Electra(max_bid) => { | ||
(StatusCode::OK, max_bid.as_ssz_bytes()).into_response() | ||
} | ||
}; | ||
let Ok(consensus_version_header) = HeaderValue::from_str(max_bid.version()) | ||
else { | ||
info!("sending response as JSON"); | ||
return Ok((StatusCode::OK, axum::Json(max_bid)).into_response()); | ||
}; | ||
let Ok(content_type_header) = | ||
HeaderValue::from_str(&format!("{}", Accept::Ssz)) | ||
else { | ||
info!("sending response as JSON"); | ||
return Ok((StatusCode::OK, axum::Json(max_bid)).into_response()); | ||
}; | ||
res.headers_mut() | ||
.insert(CONSENSUS_VERSION_HEADER, consensus_version_header); | ||
res.headers_mut().insert(CONTENT_TYPE, content_type_header); | ||
info!("sending response as SSZ"); | ||
res | ||
} | ||
Accept::Json | Accept::Any => { | ||
(StatusCode::OK, axum::Json(max_bid)).into_response() | ||
} | ||
}; | ||
Ok(response) | ||
} else { | ||
// spec: return 204 if request is valid but no bid available | ||
info!("no header available for slot"); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
woth double checking this is not case sensitive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just realized we have our own
ForkName
, we could also import it from lighthouseThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For case sensitivity, it's defined the way the spec defines it (https://github.com/ethereum/beacon-APIs/blob/672e03e25ace85a3bacaea553fbf374f4f844435/apis/beacon/blocks/blocks.yaml#L21) but Rust will convert everything to lower case regardless.
For ForkName, done in 86fa858.