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: 1 addition & 1 deletion config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ wait_all_registrations = true
# this should be lower than that, leaving some margin for overhead
# OPTIONAL, DEFAULT: 950
timeout_get_header_ms = 950
# (unreleased, from v0.12.0-rc1) ePBS bids only: ms kept back from the beacon node's X-Timeout-Ms deadline, must be under one slot, 12000 on mainnet (https://commit-boost.github.io/commit-boost-client/get_started/epbs#timing)
# (unreleased, from v0.12.0-rc1) ePBS bids only: ms kept back from the beacon node's X-Timeout-Ms deadline, must be above 0 and under one slot, 12000 on mainnet (https://commit-boost.github.io/commit-boost-client/get_started/epbs#timing)
# OPTIONAL, DEFAULT: 50
# proposer_deadline_buffer_ms = 50
# Timeout in milliseconds for the `submit_blinded_block` call to relays.
Expand Down
45 changes: 40 additions & 5 deletions crates/common/src/config/pbs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,12 @@ impl PbsConfig {
);
ensure!(self.late_in_slot_time_ms > 0, "late_in_slot_time_ms must be greater than 0");

// The buffer comes out of the proposer's deadline, which is clamped to
// one slot, so a buffer of a slot or more leaves no time for the bid
// request. 0 is allowed (no reserve).
// 0 leaves the bid no time to get back to the beacon node, and a slot or
// more leaves the builder none, as the proposer's deadline is clamped to a slot
let slot_time_ms = chain.slot_time_sec().saturating_mul(1000);
ensure!(
self.proposer_deadline_buffer_ms < slot_time_ms,
"proposer_deadline_buffer_ms must be less than one slot ({slot_time_ms} ms)"
(1..slot_time_ms).contains(&self.proposer_deadline_buffer_ms),
"proposer_deadline_buffer_ms must be greater than 0 and less than one slot ({slot_time_ms} ms)"
);

ensure!(
Expand Down Expand Up @@ -557,6 +556,42 @@ mod tests {
use super::*;
use crate::config::test_env::{RELAY_URL, with_env};

fn config_with_buffer(buffer: u64) -> String {
format!(
"chain = \"Holesky\"\n[pbs]\nproposer_deadline_buffer_ms = {buffer}\n\
[[relays]]\nurl = \"{RELAY_URL}\"\n"
)
}

#[tokio::test]
async fn test_deadline_buffer_range() {
for (buffer, accepted) in [(0, false), (1, true), (11999, true), (12000, false)] {
let config: CommitBoostConfig = toml::from_str(&config_with_buffer(buffer)).unwrap();
assert_eq!(config.validate().await.is_ok(), accepted, "{buffer}");
}
}

// The service and a custom PBS module load [pbs] through different functions
#[test]
fn test_loaders_reject_zero_deadline_buffer() {
#[derive(Deserialize)]
struct NoExtra {}
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cb-config.toml");
std::fs::write(&path, config_with_buffer(0)).unwrap();
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let service = runtime.block_on(load_pbs_config(Some(path.clone()))).map(|_| ());
let custom = with_env(&[(CONFIG_ENV, path.to_str())], || {
runtime.block_on(load_pbs_custom_config::<NoExtra>()).map(|_| ())
});
for result in [service, custom] {
let err = result.expect_err("a zero deadline buffer loaded");
let expected =
"proposer_deadline_buffer_ms must be greater than 0 and less than one slot";
assert!(format!("{err:#}").contains(expected), "{err:#}");
}
}

fn relay_with_headers(headers: &str) -> Result<RelayConfig, toml::de::Error> {
toml::from_str(&format!("url = \"{RELAY_URL}\"\nheaders = {headers}\n"))
}
Expand Down
41 changes: 36 additions & 5 deletions crates/common/src/pbs/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ pub struct RelayClient {

impl RelayClient {
pub fn new(config: RelayConfig) -> eyre::Result<Self> {
Self::with_client_builder(config, reqwest::Client::builder())
}

/// [`Self::new`] on a client builder that already holds extra settings
pub fn with_client_builder(
config: RelayConfig,
builder: reqwest::ClientBuilder,
) -> eyre::Result<Self> {
let stream_url = match config.get_header {
GetHeaderTransport::Http => None,
GetHeaderTransport::Stream => Some(stream_url(&config.entry.url)?),
Expand Down Expand Up @@ -131,10 +139,8 @@ impl RelayClient {
}
}

let client = reqwest::Client::builder()
.default_headers(headers.clone())
.timeout(DEFAULT_REQUEST_TIMEOUT)
.build()?;
let client =
builder.default_headers(headers.clone()).timeout(DEFAULT_REQUEST_TIMEOUT).build()?;

Ok(Self {
id: Arc::new(config.id().to_owned()),
Expand Down Expand Up @@ -255,6 +261,13 @@ impl RelayClient {
}
}

/// The builder URL in URL-form auth data. Only `http` and `https` count, since
/// opaque data such as `builder-a:prod` parses as a URL too.
pub fn decode_auth_data_url(data: &[u8]) -> Option<Url> {
let url = Url::parse(std::str::from_utf8(data).ok()?).ok()?;
matches!(url.scheme(), "http" | "https").then_some(url)
}

/// First 4 bytes of the value's SHA-256: enough to see a rotation
/// across reloads, too short to identify a value on its own
fn value_fingerprint(value: &str) -> String {
Expand All @@ -268,12 +281,30 @@ mod tests {

use alloy::primitives::B256;

use super::{GetHeaderRequest, RelayClient, RelayEntry, value_fingerprint};
use super::{
GetHeaderRequest, RelayClient, RelayEntry, decode_auth_data_url, value_fingerprint,
};
use crate::{
config::{GetHeaderTransport, RelayConfig, test_env::RELAY_URL},
utils::bls_pubkey_from_hex_unchecked,
};

#[test]
fn decode_auth_data_url_accepts_only_http_urls() {
let cases: [(&[u8], Option<&str>); 5] = [
(b"https://Builder-A.example.com:443/eth", Some("https://builder-a.example.com/eth")),
(b"http://10.0.0.1:18550", Some("http://10.0.0.1:18550/")),
// Opaque data and host:port parse as URLs with their own scheme
(b"builder-a:prod", None),
(b"localhost:18550", None),
(b"builder-a.example.com", None),
];
for (data, expected) in cases {
let decoded = decode_auth_data_url(data).map(|url| url.to_string());
assert_eq!(decoded.as_deref(), expected, "{}", String::from_utf8_lossy(data));
}
}

#[test]
fn test_relay_entry() {
let pubkey = bls_pubkey_from_hex_unchecked(
Expand Down
6 changes: 6 additions & 0 deletions crates/pbs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ publish = false
rust-version.workspace = true
version.workspace = true

[features]
testing-flags = []

[dependencies]
alloy.workspace = true
async-trait.workspace = true
Expand Down Expand Up @@ -35,3 +38,6 @@ url.workspace = true
uuid.workspace = true
webpki-roots.workspace = true
thiserror.workspace = true

[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
Loading
Loading