Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c82bb11
refactor(pbs): read the bid stream through a generic reader
JasonVranek Oct 5, 2026
bb33556
fix(pbs): a bid stream holds its latest valid bid
JasonVranek Oct 5, 2026
b7d5e90
fix(pbs): a 204 answer to the bid stream handshake is no bid
JasonVranek Oct 5, 2026
a02add9
feat(pbs): race the relay's HTTP get_header alongside every bid stream
JasonVranek Oct 5, 2026
eaa9d60
fix(pbs): ask the bid stream to end one downlink trip before the dead…
JasonVranek Oct 5, 2026
2edf661
chore(deps): bump lighthouse to rev 31d8cfd for the gloas types
JasonVranek Oct 5, 2026
8683257
test: bind test listeners once instead of dropping and rebinding
JasonVranek Oct 5, 2026
66e3b1f
feat(pbs): serve the gloas ePBS builder-API endpoints
JasonVranek Oct 6, 2026
852a585
docs(epbs): keep the builder config anchor the troubleshooting table …
JasonVranek Oct 6, 2026
bb66bea
address review comments
JasonVranek Oct 6, 2026
d95d414
test: wait for PBS's startup status check in test_mux to fix race
JasonVranek Oct 6, 2026
1694f54
feat(pbs): dial builders outside the config for ePBS requests
JasonVranek Oct 6, 2026
70acaee
normalize gloas version header
JasonVranek Oct 7, 2026
de6429d
Merge branch 'epbs-pr1' into epbs-pr2
JasonVranek Oct 7, 2026
02ec952
fix(pbs): require an ePBS deadline buffer above 0
JasonVranek Oct 7, 2026
84f2f20
Merge main after #505
JasonVranek Oct 8, 2026
8aa4118
perf(pbs): share one client across dials
JasonVranek Oct 8, 2026
3283650
merge: the PBS bid stream race
JasonVranek Oct 6, 2026
6d11cdc
feat(pbs): stream the ePBS bid alongside the HTTP bid request
JasonVranek Oct 6, 2026
7d89eca
docs(pbs): say what the auth data checks accept
JasonVranek Oct 10, 2026
74afb3b
perf(pbs): reuse a dial check's lookup for its connection
JasonVranek Oct 10, 2026
b9f1283
fix(pbs): route via [[relays]], forward JSON blocks, refuse own host
JasonVranek Oct 12, 2026
47e4748
merge: #506's routing, JSON signed blocks and own-host refusal
JasonVranek Oct 12, 2026
f4b798e
fix(pbs): send the time left as the bid stream's X-Timeout-Ms
JasonVranek Oct 12, 2026
545903a
Merge main after #506
JasonVranek Oct 13, 2026
1021503
refactor(pbs): request ePBS bids from a stream relay over the stream …
JasonVranek Oct 13, 2026
05151ba
refactor(pbs): leave the PBS get_header stream as released in v0.11.0
JasonVranek Oct 13, 2026
94a524c
refactor(pbs): fold the ePBS bid stream reader into its route, and re…
JasonVranek Oct 13, 2026
ac9c587
test(pbs): derive the refused-handshake bound from the buffer default
JasonVranek Oct 13, 2026
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.

1 change: 1 addition & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ get_params = { param1 = "value1", param2 = "value2" }
# 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
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
59 changes: 56 additions & 3 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 @@ -89,7 +89,7 @@ pub struct RelayClient {
pub client: reqwest::Client,
/// Base url of the get_header stream, `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 @@ -244,6 +244,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 @@ -460,6 +478,41 @@ mod tests {
assert!(RelayClient::new(config).is_err());
}

#[test]
fn test_execution_payload_bid_stream_url() {
let parent_hash = B256::ZERO;
let parent_root = B256::repeat_byte(1);
let validator_pubkey = bls_pubkey_from_hex_unchecked(
"0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae",
);
let relay_config = r#"
{
"url": "https://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz:4444/relay-api"
}"#;
let mut config = serde_json::from_str::<RelayConfig>(relay_config).unwrap();
let url = |config: &RelayConfig| {
RelayClient::new(config.clone()).unwrap().get_execution_payload_bid_stream_url(
0,
&parent_hash,
&parent_root,
&validator_pubkey,
)
};

// Only a streaming relay has one
assert!(url(&config).is_none());

// The get_header stream's origin, at the fixed path, with get_params
config.get_header = GetHeaderTransport::Stream;
config.get_params = Some(HashMap::from([("token".to_string(), "abc".to_string())]));
assert_eq!(
url(&config).unwrap().to_string(),
format!(
"wss://abc.xyz:4444/eth/v1/builder/execution_payload_bid_stream/0/{parent_hash}/{parent_root}/{validator_pubkey}?token=abc"
)
);
}

#[test]
fn test_value_fingerprint_tracks_the_value() {
assert_eq!(value_fingerprint("s3cret").len(), 8);
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
1 change: 1 addition & 0 deletions crates/pbs/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub const SUBMIT_BUILDER_PREFERENCES_ENDPOINT_TAG: &str = "submit_builder_prefer
pub const SUBMIT_SIGNED_BEACON_BLOCK_ENDPOINT_TAG: &str = "submit_signed_beacon_block";
pub const RELOAD_ENDPOINT_TAG: &str = "reload";
pub const GET_HEADER_STREAM_ENDPOINT_TAG: &str = "get_header_stream";
pub const GET_EXECUTION_PAYLOAD_BID_STREAM_ENDPOINT_TAG: &str = "get_execution_payload_bid_stream";

/// For metrics recorded when a request times out
pub const TIMEOUT_ERROR_CODE: u16 = 555;
Expand Down
2 changes: 2 additions & 0 deletions crates/pbs/src/dial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ fn dial_config(url: Url) -> RelayConfig {
id: None,
headers: None,
get_params: None,
// HTTP only: a stream's connection skips DialResolver's address check
get_header: GetHeaderTransport::Http,
enable_timing_games: false,
target_first_request_ms: None,
Expand Down Expand Up @@ -329,6 +330,7 @@ mod tests {
(Ok(relay), Ok(url)) => {
assert_eq!(relay.config.entry.url.as_str(), url, "{case}");
assert_eq!(relay.id.as_str(), "dial", "{case}");
assert_eq!(relay.config.get_header, GetHeaderTransport::Http, "{case}");
}
(Err(err), Err(message)) => assert_eq!(err.to_string(), message, "{case}"),
_ => panic!("{case}: expected {expected:?}, got {:?}", res.map(|_| ())),
Expand Down
44 changes: 44 additions & 0 deletions crates/pbs/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,50 @@ lazy_static! {
)
.unwrap();

// THE ePBS BID STREAM
// Outcome and time-to-first-bid ride RELAY_STATUS_CODE / RELAY_LATENCY
// under `get_execution_payload_bid_stream`

/// ePBS bid stream handshake latency by relay
pub static ref RELAY_EPBS_STREAM_CONNECT_LATENCY: HistogramVec = register_histogram_vec_with_registry!(
"relay_epbs_stream_connect_latency",
"ePBS bid stream handshake latency by relay",
&["relay_id"],
vec![0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 1.0],
PBS_METRICS_REGISTRY
)
.unwrap();

/// Bids that decoded per ePBS bid stream, by relay
pub static ref RELAY_EPBS_STREAM_UPDATES: HistogramVec = register_histogram_vec_with_registry!(
"relay_epbs_stream_updates",
"Bids that decoded per ePBS bid stream, by relay",
&["relay_id"],
vec![0.0, 1.0, 2.0, 3.0, 5.0, 10.0, 20.0, 50.0, 100.0],
PBS_METRICS_REGISTRY
)
.unwrap();

/// ePBS bid stream frames that could not be read or decoded, by relay
pub static ref RELAY_EPBS_STREAM_INVALID_FRAMES: IntCounterVec = register_int_counter_vec_with_registry!(
"relay_epbs_stream_invalid_frames_total",
"ePBS bid stream frames that could not be read or decoded, by relay",
&["relay_id"],
PBS_METRICS_REGISTRY
)
.unwrap();

/// ePBS bid streams that fell back to HTTP, by relay
// Only a failed connect with time left retries; one at the deadline shows
// on the status series alone
pub static ref RELAY_EPBS_STREAM_FALLBACK: IntCounterVec = register_int_counter_vec_with_registry!(
"relay_epbs_stream_fallback_total",
"ePBS bid streams that fell back to HTTP, by relay",
&["relay_id"],
PBS_METRICS_REGISTRY
)
.unwrap();

// TO BEACON NODE
/// Status code returned to beacon node by endpoint
pub static ref BEACON_NODE_STATUS: IntCounterVec = register_int_counter_vec_with_registry!(
Expand Down
Loading
Loading