Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

8 changes: 6 additions & 2 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,17 @@ headers = { X-MyCustomHeader = "MyCustomValue" }
# GET parameters to add to each request URL for this relay
# OPTIONAL
get_params = { param1 = "value1", param2 = "value2" }
# How to fetch headers from this relay.
# How to fetch bids from this relay.
# "http" -> one request per get_header, at the relay url above
# "stream" -> websocket stream of bid updates, only for relays that support it. Connects to
# ws(s)://<relay url>/eth/v1/builder/header_stream/{slot}/{parent_hash}/{pubkey}.
# ws(s)://<relay host>/eth/v1/builder/header_stream/{slot}/{parent_hash}/{pubkey}.
# (unreleased, from v0.12.0-rc1) The relay's normal HTTP request runs alongside,
# timing games included, and the better bid wins, so a failed stream still
# leaves the HTTP bid.
# Requires a UUID api key, set it in `headers` field above under the `X-Api-Key`
# name; it is sent on the websocket handshake. Can use
# https://www.uuidgenerator.net/version4 to generate one.
# (unreleased, from v0.12.0-rc1) After Gloas this also streams ePBS bids (https://commit-boost.github.io/commit-boost-client/get_started/epbs#bid-streaming)
# OPTIONAL, DEFAULT: "http"
get_header = "http"
# Whether to enable timing games, as tuned by `target_first_request_ms` and `frequency_get_header_ms`.
Expand Down
11 changes: 10 additions & 1 deletion crates/common/src/config/pbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ pub enum GetHeaderTransport {
Stream,
}

impl GetHeaderTransport {
pub fn as_str(&self) -> &'static str {
match self {
Self::Http => "http",
Self::Stream => "stream",
}
}
}

/// A custom relay header value: a literal, or a secret read from a file or an
/// environment variable when the relay client is built (at startup and on every
/// reload), so an API key never has to sit in plaintext in the config file.
Expand Down Expand Up @@ -131,7 +140,7 @@ pub struct RelayConfig {
pub headers: Option<HashMap<String, HeaderSource>>,
/// Optional GET parameters to add to each request
pub get_params: Option<HashMap<String, String>>,
/// How to fetch headers from this relay
/// How to fetch bids from this relay
#[serde(default)]
pub get_header: GetHeaderTransport,
/// Whether to enable timing games
Expand Down
4 changes: 4 additions & 0 deletions crates/common/src/pbs/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub const RELOAD_PATH: &str = "/reload";

pub const GET_EXECUTION_PAYLOAD_BID_PATH: &str =
"/execution_payload_bid/{slot}/{parent_hash}/{parent_root}/{proposer_pubkey}";
pub const GET_EXECUTION_PAYLOAD_BID_STREAM_PATH: &str = "/execution_payload_bid_stream";
pub const SUBMIT_BUILDER_PREFERENCES_PATH: &str = "/builder_preferences/{proposer_pubkey}";
pub const SUBMIT_SIGNED_BEACON_BLOCK_PATH: &str = "/beacon_blocks";

Expand All @@ -24,6 +25,9 @@ pub const HEADER_VERSION_KEY: &str = "X-CommitBoost-Version";
pub const HEADER_VERSION_VALUE: &str = COMMIT_BOOST_VERSION;
pub const HEADER_START_TIME_UNIX_MS: &str = "Date-Milliseconds";
pub const HEADER_TIMEOUT_MS: &str = "X-Timeout-Ms";
/// The SSZ `SignedBuilderRequestAuth` as padded standard base64, on the ePBS
/// bid stream handshake: a websocket upgrade has no body
pub const HEADER_REQUEST_AUTH: &str = "X-Request-Auth";
pub const HEADER_API_KEY: &str = "X-Api-Key";
pub const HEADER_CONSENSUS_VERSION: &str = "Eth-Consensus-Version";

Expand Down
111 changes: 69 additions & 42 deletions crates/common/src/pbs/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use url::Url;
use super::{
HEADER_VERSION_KEY, HEADER_VERSION_VALUE,
constants::{
GET_HEADER_STREAM_PATH, GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH,
SUBMIT_SIGNED_BEACON_BLOCK_PATH,
GET_EXECUTION_PAYLOAD_BID_STREAM_PATH, GET_HEADER_STREAM_PATH, GET_STATUS_PATH,
REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH, SUBMIT_SIGNED_BEACON_BLOCK_PATH,
},
error::PbsError,
};
Expand Down Expand Up @@ -63,6 +63,15 @@ pub enum GetHeaderRequest {
Stream(Url),
}

impl GetHeaderRequest {
pub fn transport(&self) -> GetHeaderTransport {
match self {
Self::Http(_) => GetHeaderTransport::Http,
Self::Stream(_) => GetHeaderTransport::Stream,
}
}
}

fn stream_url(entry: &Url) -> eyre::Result<Url> {
let scheme = match entry.scheme() {
"http" | "ws" => "ws",
Expand All @@ -87,9 +96,10 @@ pub struct RelayClient {
pub id: Arc<String>,
/// HTTP client to send requests
pub client: reqwest::Client,
/// Base url of the get_header stream, `Some` only when the relay streams.
/// Base url of the get_header stream, whose origin the ePBS bid stream
/// shares. `Some` only when the relay streams.
stream_url: Option<Url>,
/// Baseline headers for the get_header stream handshake.
/// Baseline headers for the bid stream handshakes.
stream_headers: Arc<HeaderMap>,
/// Configuration of the relay
pub config: Arc<RelayConfig>,
Expand Down Expand Up @@ -195,24 +205,17 @@ impl RelayClient {
)
}

pub fn get_header_request(
/// The get_header stream, `None` when the relay does not stream
pub fn get_header_stream_url(
&self,
slot: u64,
parent_hash: &B256,
validator_pubkey: &BlsPublicKey,
) -> Result<GetHeaderRequest, PbsError> {
Ok(match &self.stream_url {
None => {
GetHeaderRequest::Http(self.get_header_url(slot, parent_hash, validator_pubkey)?)
}
Some(base) => {
let mut url = base.clone();
url.set_path(&format!("{}/{slot}/{parent_hash}/{validator_pubkey}", base.path()));

self.append_get_params(&mut url);
GetHeaderRequest::Stream(url)
}
})
) -> Option<Url> {
let mut url = self.stream_url.clone()?;
url.set_path(&format!("{}/{slot}/{parent_hash}/{validator_pubkey}", url.path()));
self.append_get_params(&mut url);
Some(url)
}

pub fn get_status_url(&self) -> Result<Url, PbsError> {
Expand Down Expand Up @@ -244,6 +247,24 @@ impl RelayClient {
)
}

/// The ePBS bid stream, at the get_header stream's origin. `None` when the
/// relay does not stream.
pub fn get_execution_payload_bid_stream_url(
&self,
slot: u64,
parent_hash: &B256,
parent_root: &B256,
validator_pubkey: &BlsPublicKey,
) -> Option<Url> {
let mut url = self.stream_url.clone()?;
url.set_path(&format!(
"{}{GET_EXECUTION_PAYLOAD_BID_STREAM_PATH}/{slot}/{parent_hash}/{parent_root}/{validator_pubkey}",
BuilderApiVersion::V1.path()
));
self.append_get_params(&mut url);
Some(url)
}

/// builder-API: POST /eth/v1/builder/builder_preferences/{proposer_pubkey}
pub fn submit_builder_preferences_url(
&self,
Expand Down Expand Up @@ -281,9 +302,7 @@ mod tests {

use alloy::primitives::B256;

use super::{
GetHeaderRequest, RelayClient, RelayEntry, decode_auth_data_url, value_fingerprint,
};
use super::{RelayClient, RelayEntry, decode_auth_data_url, value_fingerprint};
use crate::{
config::{GetHeaderTransport, RelayConfig, test_env::RELAY_URL},
utils::bls_pubkey_from_hex_unchecked,
Expand Down Expand Up @@ -391,9 +410,10 @@ mod tests {
}

#[test]
fn test_get_header_request() {
fn test_stream_urls() {
let slot = 0;
let parent_hash = B256::ZERO;
let parent_root = B256::repeat_byte(1);
let validator_pubkey = bls_pubkey_from_hex_unchecked(
"0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae",
);
Expand All @@ -403,29 +423,26 @@ mod tests {
}"#;
let base_config = serde_json::from_str::<RelayConfig>(relay_config).unwrap();

// Default transport: plain HTTP endpoint
// Default transport: no stream
let relay = RelayClient::new(base_config.clone()).unwrap();
let GetHeaderRequest::Http(url) =
relay.get_header_request(slot, &parent_hash, &validator_pubkey).unwrap()
else {
panic!("expected http request");
};
assert_eq!(
url,
relay.get_header_url(slot, &parent_hash, &validator_pubkey).unwrap(),
"http dispatch must match the plain url builder"
assert!(relay.get_header_stream_url(slot, &parent_hash, &validator_pubkey).is_none());
assert!(
relay
.get_execution_payload_bid_stream_url(
slot,
&parent_hash,
&parent_root,
&validator_pubkey
)
.is_none()
);

// Streaming: the relay url over ws, at the fixed stream path, with the
// pubkey credentials dropped
let mut config = base_config.clone();
config.get_header = GetHeaderTransport::Stream;
let relay = RelayClient::new(config).unwrap();
let GetHeaderRequest::Stream(url) =
relay.get_header_request(slot, &parent_hash, &validator_pubkey).unwrap()
else {
panic!("expected stream request");
};
let url = relay.get_header_stream_url(slot, &parent_hash, &validator_pubkey).unwrap();
assert_eq!(
url.to_string(),
format!(
Expand All @@ -440,17 +457,27 @@ mod tests {
config.get_header = GetHeaderTransport::Stream;
config.get_params = Some(HashMap::from([("token".to_string(), "abc".to_string())]));
let relay = RelayClient::new(config).unwrap();
let GetHeaderRequest::Stream(url) =
relay.get_header_request(slot, &parent_hash, &validator_pubkey).unwrap()
else {
panic!("expected stream request");
};
let url = relay.get_header_stream_url(slot, &parent_hash, &validator_pubkey).unwrap();
assert_eq!(
url.to_string(),
format!(
"wss://abc.xyz:4444/eth/v1/builder/header_stream/{slot}/{parent_hash}/{validator_pubkey}?token=abc"
)
);
let url = relay
.get_execution_payload_bid_stream_url(
slot,
&parent_hash,
&parent_root,
&validator_pubkey,
)
.unwrap();
assert_eq!(
url.to_string(),
format!(
"wss://abc.xyz:4444/eth/v1/builder/execution_payload_bid_stream/{slot}/{parent_hash}/{parent_root}/{validator_pubkey}?token=abc"
)
);

// A relay url we can't stream over is rejected at construction
let mut config = base_config;
Expand Down
1 change: 1 addition & 0 deletions crates/pbs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ alloy.workspace = true
async-trait.workspace = true
axum.workspace = true
axum-extra.workspace = true
base64.workspace = true
cb-common.workspace = true
cb-metrics.workspace = true
ethereum_serde_utils.workspace = true
Expand Down
Loading
Loading