Skip to content

Commit 18a9ae5

Browse files
authored
feat: add relay monitors (#73)
* add relay monitor support * use `Url` instead of strings
1 parent ed0d34f commit 18a9ae5

21 files changed

+263
-121
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ clap = { version = "4.5.4", features = ["derive", "env"] }
7171
thiserror = "1.0.61"
7272
color-eyre = "0.6.3"
7373
eyre = "0.6.12"
74-
url = "2.5.0"
74+
url = { version = "2.5.0", features = ["serde"] }
7575
uuid = { version = "1.8.0", features = ["v4", "fast-rng", "serde"] }
7676
typenum = "1.17.0"
7777
rand = "0.8.5"

config.example.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ skip_sigverify = false
3434
# Minimum bid in ETH that will be accepted from `get_header`
3535
# OPTIONAL, DEFAULT: 0.0
3636
min_bid_eth = 0.0
37+
# List of URLs of relay monitors to send registrations to
38+
# OPTIONAL
39+
relay_monitors = []
3740
# How late in milliseconds in the slot is "late". This impacts the `get_header` requests, by shortening timeouts for `get_header` calls to
3841
# relays and make sure a header is returned within this deadline. If the request from the CL comes later in the slot, then fetching headers is skipped
3942
# to force local building and miniminzing the risk of missed slots. See also the timing games section below

crates/common/src/config/mod.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,21 @@ pub struct CommitBoostConfig {
3434
}
3535

3636
impl CommitBoostConfig {
37+
/// Validate config
38+
pub fn validate(&self) -> Result<()> {
39+
self.pbs.pbs_config.validate()?;
40+
Ok(())
41+
}
42+
3743
pub fn from_file(path: &str) -> Result<Self> {
38-
load_from_file(path)
44+
let config: Self = load_from_file(path)?;
45+
config.validate()?;
46+
Ok(config)
3947
}
4048

4149
pub fn from_env_path() -> Result<Self> {
42-
load_file_from_env(CB_CONFIG_ENV)
50+
let config: Self = load_file_from_env(CB_CONFIG_ENV)?;
51+
config.validate()?;
52+
Ok(config)
4353
}
4454
}

crates/common/src/config/pbs.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{collections::HashMap, sync::Arc};
55
use alloy::primitives::U256;
66
use eyre::Result;
77
use serde::{de::DeserializeOwned, Deserialize, Serialize};
8+
use url::Url;
89

910
use super::{constants::PBS_DEFAULT_IMAGE, CommitBoostConfig};
1011
use crate::{
@@ -15,7 +16,7 @@ use crate::{
1516
utils::{as_eth_str, default_bool, default_u256, default_u64},
1617
};
1718

18-
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
19+
#[derive(Debug, Clone, Deserialize, Serialize)]
1920
pub struct RelayConfig {
2021
/// Relay ID, if missing will default to the URL hostname from the entry
2122
pub id: Option<String>,
@@ -33,7 +34,7 @@ pub struct RelayConfig {
3334
pub frequency_get_header_ms: Option<u64>,
3435
}
3536

36-
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
37+
#[derive(Debug, Clone, Deserialize, Serialize)]
3738
pub struct PbsConfig {
3839
/// Port to receive BuilderAPI calls from beacon node
3940
pub port: u16,
@@ -55,13 +56,23 @@ pub struct PbsConfig {
5556
/// Minimum bid that will be accepted from get_header
5657
#[serde(rename = "min_bid_eth", with = "as_eth_str", default = "default_u256")]
5758
pub min_bid_wei: U256,
59+
/// List of relay monitor urls in the form of scheme://host
60+
#[serde(default)]
61+
pub relay_monitors: Vec<Url>,
5862
/// How late in the slot we consider to be "late"
5963
#[serde(default = "default_u64::<LATE_IN_SLOT_TIME_MS>")]
6064
pub late_in_slot_time_ms: u64,
6165
}
6266

67+
impl PbsConfig {
68+
/// Validate PBS config parameters
69+
pub fn validate(&self) -> Result<()> {
70+
Ok(())
71+
}
72+
}
73+
6374
/// Static pbs config from config file
64-
#[derive(Debug, Default, Deserialize, Serialize)]
75+
#[derive(Debug, Deserialize, Serialize)]
6576
pub struct StaticPbsConfig {
6677
/// Docker image of the module
6778
#[serde(default = "default_pbs")]
@@ -96,6 +107,7 @@ fn default_pbs() -> String {
96107
/// Loads the default pbs config, i.e. with no signer client or custom data
97108
pub fn load_pbs_config() -> Result<PbsModuleConfig> {
98109
let config = CommitBoostConfig::from_env_path()?;
110+
99111
let relay_clients =
100112
config.relays.into_iter().map(RelayClient::new).collect::<Result<Vec<_>>>()?;
101113
let maybe_publiher = BuilderEventPublisher::new_from_env();
@@ -128,6 +140,8 @@ pub fn load_pbs_custom_config<T: DeserializeOwned>() -> Result<(PbsModuleConfig,
128140

129141
// load module config including the extra data (if any)
130142
let cb_config: StubConfig<T> = load_file_from_env(CB_CONFIG_ENV)?;
143+
cb_config.pbs.static_config.pbs_config.validate()?;
144+
131145
let relay_clients =
132146
cb_config.relays.into_iter().map(RelayClient::new).collect::<Result<Vec<_>>>()?;
133147
let maybe_publiher = BuilderEventPublisher::new_from_env();

crates/common/src/pbs/constants.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub const BULDER_API_PATH: &str = "/eth/v1/builder";
1+
pub const BUILDER_API_PATH: &str = "/eth/v1/builder";
22

33
pub const GET_HEADER_PATH: &str = "/header/:slot/:parent_hash/:pubkey";
44
pub const GET_STATUS_PATH: &str = "/status";

crates/common/src/pbs/error.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use alloy::{
2+
primitives::{B256, U256},
3+
rpc::types::beacon::BlsPublicKey,
4+
};
5+
use thiserror::Error;
6+
7+
use crate::error::BlstErrorWrapper;
8+
9+
#[derive(Debug, Error)]
10+
pub enum PbsError {
11+
#[error("axum error: {0}")]
12+
AxumError(#[from] axum::Error),
13+
14+
#[error("reqwest error: {0}")]
15+
Reqwest(#[from] reqwest::Error),
16+
17+
#[error("serde decode error: {0}")]
18+
SerdeDecodeError(#[from] serde_json::Error),
19+
20+
#[error("relay response error. Code: {code}, err: {error_msg}")]
21+
RelayResponse { error_msg: String, code: u16 },
22+
23+
#[error("failed validating relay response: {0}")]
24+
Validation(#[from] ValidationError),
25+
26+
#[error("URL parsing error: {0}")]
27+
UrlParsing(#[from] url::ParseError),
28+
}
29+
30+
impl PbsError {
31+
pub fn is_timeout(&self) -> bool {
32+
matches!(self, PbsError::Reqwest(err) if err.is_timeout())
33+
}
34+
}
35+
36+
#[derive(Debug, Error, PartialEq, Eq)]
37+
pub enum ValidationError {
38+
#[error("empty blockhash")]
39+
EmptyBlockhash,
40+
41+
#[error("pubkey mismatch: expected {expected} got {got}")]
42+
PubkeyMismatch { expected: BlsPublicKey, got: BlsPublicKey },
43+
44+
#[error("parent hash mismatch: expected {expected} got {got}")]
45+
ParentHashMismatch { expected: B256, got: B256 },
46+
47+
#[error("block hash mismatch: expected {expected} got {got}")]
48+
BlockHashMismatch { expected: B256, got: B256 },
49+
50+
#[error("mismatch in KZG commitments: exepcted_blobs: {expected_blobs} got_blobs: {got_blobs} got_commitments: {got_commitments} got_proofs: {got_proofs}")]
51+
KzgCommitments {
52+
expected_blobs: usize,
53+
got_blobs: usize,
54+
got_commitments: usize,
55+
got_proofs: usize,
56+
},
57+
58+
#[error("mismatch in KZG blob commitment: expected: {expected} got: {got} index: {index}")]
59+
KzgMismatch { expected: String, got: String, index: usize },
60+
61+
#[error("bid below minimum: min: {min} got {got}")]
62+
BidTooLow { min: U256, got: U256 },
63+
64+
#[error("empty tx root")]
65+
EmptyTxRoot,
66+
67+
#[error("failed signature verification: {0:?}")]
68+
Sigverify(#[from] BlstErrorWrapper),
69+
}

crates/common/src/pbs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod constants;
2+
pub mod error;
23
mod event;
34
mod relay;
45
mod types;

crates/common/src/pbs/relay.rs

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,34 @@ use alloy::{
44
primitives::{hex::FromHex, B256},
55
rpc::types::beacon::BlsPublicKey,
66
};
7-
use eyre::{Result, WrapErr};
7+
use eyre::WrapErr;
88
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
99
use serde::{Deserialize, Serialize};
1010
use url::Url;
1111

1212
use super::{
13-
constants::{BULDER_API_PATH, GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH},
13+
constants::{BUILDER_API_PATH, GET_STATUS_PATH, REGISTER_VALIDATOR_PATH, SUBMIT_BLOCK_PATH},
14+
error::PbsError,
1415
HEADER_VERSION_KEY, HEADER_VERSION_VALUE,
1516
};
1617
use crate::{config::RelayConfig, DEFAULT_REQUEST_TIMEOUT};
1718
/// A parsed entry of the relay url in the format: scheme://pubkey@host
18-
#[derive(Debug, Default, Clone)]
19+
#[derive(Debug, Clone)]
1920
pub struct RelayEntry {
2021
/// Default if of the relay, the hostname of the url
2122
pub id: String,
2223
/// Public key of the relay
2324
pub pubkey: BlsPublicKey,
2425
/// Full url of the relay
25-
pub url: String,
26+
pub url: Url,
2627
}
2728

2829
impl Serialize for RelayEntry {
2930
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3031
where
3132
S: serde::Serializer,
3233
{
33-
serializer.serialize_str(&self.url)
34+
self.url.serialize(serializer)
3435
}
3536
}
3637

@@ -39,12 +40,11 @@ impl<'de> Deserialize<'de> for RelayEntry {
3940
where
4041
D: serde::Deserializer<'de>,
4142
{
42-
let str = String::deserialize(deserializer)?;
43-
let url = Url::parse(&str).map_err(serde::de::Error::custom)?;
43+
let url = Url::deserialize(deserializer)?;
4444
let pubkey = BlsPublicKey::from_hex(url.username()).map_err(serde::de::Error::custom)?;
4545
let id = url.host().ok_or(serde::de::Error::custom("missing host"))?.to_string();
4646

47-
Ok(RelayEntry { pubkey, url: str, id })
47+
Ok(RelayEntry { pubkey, url, id })
4848
}
4949
}
5050

@@ -60,7 +60,7 @@ pub struct RelayClient {
6060
}
6161

6262
impl RelayClient {
63-
pub fn new(config: RelayConfig) -> Result<Self> {
63+
pub fn new(config: RelayConfig) -> eyre::Result<Self> {
6464
let mut headers = HeaderMap::new();
6565
headers.insert(HEADER_VERSION_KEY, HeaderValue::from_static(HEADER_VERSION_VALUE));
6666

@@ -90,46 +90,87 @@ impl RelayClient {
9090
}
9191

9292
// URL builders
93-
pub fn get_url(&self, path: &str) -> String {
94-
format!("{}{path}", &self.config.entry.url)
93+
pub fn get_url(&self, path: &str) -> Result<Url, PbsError> {
94+
self.config.entry.url.join(path).map_err(PbsError::UrlParsing)
95+
}
96+
pub fn builder_api_url(&self, path: &str) -> Result<Url, PbsError> {
97+
self.get_url(&format!("{BUILDER_API_PATH}{path}"))
9598
}
9699

97100
pub fn get_header_url(
98101
&self,
99102
slot: u64,
100103
parent_hash: B256,
101104
validator_pubkey: BlsPublicKey,
102-
) -> String {
103-
self.get_url(&format!("{BULDER_API_PATH}/header/{slot}/{parent_hash}/{validator_pubkey}"))
105+
) -> Result<Url, PbsError> {
106+
self.builder_api_url(&format!("/header/{slot}/{parent_hash}/{validator_pubkey}"))
104107
}
105108

106-
pub fn get_status_url(&self) -> String {
107-
self.get_url(&format!("{BULDER_API_PATH}{GET_STATUS_PATH}"))
109+
pub fn get_status_url(&self) -> Result<Url, PbsError> {
110+
self.builder_api_url(GET_STATUS_PATH)
108111
}
109112

110-
pub fn register_validator_url(&self) -> String {
111-
self.get_url(&format!("{BULDER_API_PATH}{REGISTER_VALIDATOR_PATH}"))
113+
pub fn register_validator_url(&self) -> Result<Url, PbsError> {
114+
self.builder_api_url(REGISTER_VALIDATOR_PATH)
112115
}
113116

114-
pub fn submit_block_url(&self) -> String {
115-
self.get_url(&format!("{BULDER_API_PATH}{SUBMIT_BLOCK_PATH}"))
117+
pub fn submit_block_url(&self) -> Result<Url, PbsError> {
118+
self.builder_api_url(SUBMIT_BLOCK_PATH)
116119
}
117120
}
118121

119122
#[cfg(test)]
120123
mod tests {
121-
use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey};
124+
use alloy::{
125+
primitives::{hex::FromHex, B256},
126+
rpc::types::beacon::BlsPublicKey,
127+
};
122128

123-
use super::RelayEntry;
129+
use super::{RelayClient, RelayEntry};
130+
use crate::config::RelayConfig;
124131

125132
#[test]
126133
fn test_relay_entry() {
127-
let s = "http://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@abc.xyz";
134+
let s = "http://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@abc.xyz/";
128135

129136
let parsed = serde_json::from_str::<RelayEntry>(&format!("\"{s}\"")).unwrap();
130137

131138
assert_eq!(parsed.pubkey, BlsPublicKey::from_hex("0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae").unwrap());
132-
assert_eq!(parsed.url, s);
139+
assert_eq!(parsed.url.as_str(), s);
133140
assert_eq!(parsed.id, "abc.xyz");
134141
}
142+
143+
#[test]
144+
fn test_relay_url() {
145+
let slot = 0;
146+
let parent_hash = B256::ZERO;
147+
let validator_pubkey = BlsPublicKey::ZERO;
148+
let expected = format!("http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz/eth/v1/builder/header/{slot}/{parent_hash}/{validator_pubkey}");
149+
150+
let relay_config = r#"
151+
{
152+
"url": "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz"
153+
}"#;
154+
155+
let config = serde_json::from_str::<RelayConfig>(relay_config).unwrap();
156+
let relay = RelayClient::new(config).unwrap();
157+
158+
assert_eq!(
159+
relay.get_header_url(slot, parent_hash, validator_pubkey).unwrap().to_string(),
160+
expected
161+
);
162+
163+
let relay_config = r#"
164+
{
165+
"url": "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz//"
166+
}"#;
167+
168+
let config = serde_json::from_str::<RelayConfig>(relay_config).unwrap();
169+
let relay = RelayClient::new(config).unwrap();
170+
171+
assert_eq!(
172+
relay.get_header_url(slot, parent_hash, validator_pubkey).unwrap().to_string(),
173+
expected
174+
);
175+
}
135176
}

crates/pbs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ blst.workspace = true
3434
# misc
3535
thiserror.workspace = true
3636
eyre.workspace = true
37+
url.workspace = true
3738
uuid.workspace = true
3839
lazy_static.workspace = true

0 commit comments

Comments
 (0)