Skip to content

Commit 2844dcc

Browse files
authored
feat: improve registrations (#374)
1 parent adb64a8 commit 2844dcc

File tree

9 files changed

+49
-171
lines changed

9 files changed

+49
-171
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/common/src/config/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::PathBuf;
22

3-
use eyre::Result;
3+
use eyre::{Result, bail};
44
use serde::{Deserialize, Serialize};
55

66
use crate::types::{Chain, ChainLoader, ForkVersion, load_chain_from_file};
@@ -45,6 +45,13 @@ impl CommitBoostConfig {
4545
if let Some(signer) = &self.signer {
4646
signer.validate().await?;
4747
}
48+
49+
if self.relays.iter().any(|r| r.validator_registration_batch_size.is_some()) {
50+
bail!(
51+
"validator_registration_batch_size is now obsolete on a per-relay basis. Please use validator_registration_batch_size in the [pbs] section instead"
52+
)
53+
}
54+
4855
Ok(())
4956
}
5057

crates/common/src/config/pbs.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::{
3636
};
3737

3838
#[derive(Debug, Clone, Deserialize, Serialize)]
39+
#[serde(deny_unknown_fields)]
3940
pub struct RelayConfig {
4041
/// Relay ID, if missing will default to the URL hostname from the entry
4142
pub id: Option<String>,
@@ -128,6 +129,10 @@ pub struct PbsConfig {
128129
/// Maximum number of retries for validator registration request per relay
129130
#[serde(default = "default_u32::<REGISTER_VALIDATOR_RETRY_LIMIT>")]
130131
pub register_validator_retry_limit: u32,
132+
/// Maximum number of validators to send to relays in a single registration
133+
/// request
134+
#[serde(deserialize_with = "empty_string_as_none", default)]
135+
pub validator_registration_batch_size: Option<usize>,
131136
}
132137

133138
impl PbsConfig {

crates/pbs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ lazy_static.workspace = true
1818
parking_lot.workspace = true
1919
prometheus.workspace = true
2020
reqwest.workspace = true
21+
serde.workspace = true
2122
serde_json.workspace = true
2223
tokio.workspace = true
2324
tower-http.workspace = true

crates/pbs/src/api.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use alloy::rpc::types::beacon::relay::ValidatorRegistration;
21
use async_trait::async_trait;
32
use axum::{Router, http::HeaderMap};
43
use cb_common::pbs::{
@@ -45,7 +44,7 @@ pub trait BuilderApi<S: BuilderApiState>: 'static {
4544

4645
/// https://ethereum.github.io/builder-specs/#/Builder/registerValidator
4746
async fn register_validator(
48-
registrations: Vec<ValidatorRegistration>,
47+
registrations: Vec<serde_json::Value>,
4948
req_headers: HeaderMap,
5049
state: PbsState<S>,
5150
) -> eyre::Result<()> {

crates/pbs/src/mev_boost/register_validator.rs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use std::time::{Duration, Instant};
22

3-
use alloy::rpc::types::beacon::relay::ValidatorRegistration;
3+
use alloy::primitives::Bytes;
44
use axum::http::{HeaderMap, HeaderValue};
55
use cb_common::{
66
pbs::{HEADER_START_TIME_UNIX_MS, RelayClient, error::PbsError},
77
utils::{get_user_agent_with_version, read_chunked_body_with_max, utcnow_ms},
88
};
99
use eyre::bail;
1010
use futures::future::{join_all, select_ok};
11-
use reqwest::header::USER_AGENT;
11+
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
1212
use tracing::{Instrument, debug, error};
1313
use url::Url;
1414

@@ -21,7 +21,7 @@ use crate::{
2121
/// Implements https://ethereum.github.io/builder-specs/#/Builder/registerValidator
2222
/// Returns 200 if at least one relay returns 200, else 503
2323
pub async fn register_validator<S: BuilderApiState>(
24-
registrations: Vec<ValidatorRegistration>,
24+
registrations: Vec<serde_json::Value>,
2525
req_headers: HeaderMap,
2626
state: PbsState<S>,
2727
) -> eyre::Result<()> {
@@ -31,27 +31,29 @@ pub async fn register_validator<S: BuilderApiState>(
3131
.insert(HEADER_START_TIME_UNIX_MS, HeaderValue::from_str(&utcnow_ms().to_string())?);
3232
send_headers.insert(USER_AGENT, get_user_agent_with_version(&req_headers)?);
3333

34-
let relays = state.all_relays().to_vec();
35-
let mut handles = Vec::with_capacity(relays.len());
36-
for relay in relays.clone() {
37-
if let Some(batch_size) = relay.config.validator_registration_batch_size {
38-
for batch in registrations.chunks(batch_size) {
39-
handles.push(tokio::spawn(
40-
send_register_validator_with_timeout(
41-
batch.to_vec(),
42-
relay.clone(),
43-
send_headers.clone(),
44-
state.pbs_config().timeout_register_validator_ms,
45-
state.pbs_config().register_validator_retry_limit,
46-
)
47-
.in_current_span(),
48-
));
49-
}
34+
// prepare the body in advance, ugly dyn
35+
let bodies: Box<dyn Iterator<Item = (usize, Bytes)>> =
36+
if let Some(batch_size) = state.config.pbs_config.validator_registration_batch_size {
37+
Box::new(registrations.chunks(batch_size).map(|batch| {
38+
// SAFETY: unwrap is ok because we're serializing a &[serde_json::Value]
39+
let body = serde_json::to_vec(batch).unwrap();
40+
(batch.len(), Bytes::from(body))
41+
}))
5042
} else {
43+
let body = serde_json::to_vec(&registrations).unwrap();
44+
Box::new(std::iter::once((registrations.len(), Bytes::from(body))))
45+
};
46+
send_headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
47+
48+
let mut handles = Vec::with_capacity(state.all_relays().len());
49+
50+
for (n_regs, body) in bodies {
51+
for relay in state.all_relays().iter().cloned() {
5152
handles.push(tokio::spawn(
5253
send_register_validator_with_timeout(
53-
registrations.clone(),
54-
relay.clone(),
54+
n_regs,
55+
body.clone(),
56+
relay,
5557
send_headers.clone(),
5658
state.pbs_config().timeout_register_validator_ms,
5759
state.pbs_config().register_validator_retry_limit,
@@ -82,7 +84,8 @@ pub async fn register_validator<S: BuilderApiState>(
8284
/// Register validator to relay, retry connection errors until the
8385
/// given timeout has passed
8486
async fn send_register_validator_with_timeout(
85-
registrations: Vec<ValidatorRegistration>,
87+
n_regs: usize,
88+
body: Bytes,
8689
relay: RelayClient,
8790
headers: HeaderMap,
8891
timeout_ms: u64,
@@ -97,7 +100,8 @@ async fn send_register_validator_with_timeout(
97100
let start_request = Instant::now();
98101
match send_register_validator(
99102
url.clone(),
100-
&registrations,
103+
n_regs,
104+
body.clone(),
101105
&relay,
102106
headers.clone(),
103107
remaining_timeout_ms,
@@ -134,7 +138,8 @@ async fn send_register_validator_with_timeout(
134138

135139
async fn send_register_validator(
136140
url: Url,
137-
registrations: &[ValidatorRegistration],
141+
n_regs: usize,
142+
body: Bytes,
138143
relay: &RelayClient,
139144
headers: HeaderMap,
140145
timeout_ms: u64,
@@ -146,7 +151,7 @@ async fn send_register_validator(
146151
.post(url)
147152
.timeout(Duration::from_millis(timeout_ms))
148153
.headers(headers)
149-
.json(&registrations)
154+
.body(body.0)
150155
.send()
151156
.await
152157
{
@@ -189,7 +194,7 @@ async fn send_register_validator(
189194
retry,
190195
?code,
191196
latency = ?request_latency,
192-
num_registrations = registrations.len(),
197+
num_registrations = n_regs,
193198
"registration successful"
194199
);
195200

crates/pbs/src/routes/register_validator.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use alloy::rpc::types::beacon::relay::ValidatorRegistration;
21
use axum::{Json, extract::State, http::HeaderMap, response::IntoResponse};
32
use cb_common::utils::get_user_agent;
43
use reqwest::StatusCode;
@@ -15,7 +14,7 @@ use crate::{
1514
pub async fn handle_register_validator<S: BuilderApiState, A: BuilderApi<S>>(
1615
State(state): State<PbsStateGuard<S>>,
1716
req_headers: HeaderMap,
18-
Json(registrations): Json<Vec<ValidatorRegistration>>,
17+
Json(registrations): Json<Vec<serde_json::Value>>,
1918
) -> Result<impl IntoResponse, PbsClientError> {
2019
let state = state.read().clone();
2120

tests/src/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub fn get_pbs_static_config(port: u16) -> PbsConfig {
8181
rpc_url: None,
8282
http_timeout_seconds: 10,
8383
register_validator_retry_limit: u32::MAX,
84+
validator_registration_batch_size: None,
8485
}
8586
}
8687

tests/tests/pbs_post_validators.rs

Lines changed: 0 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -61,146 +61,6 @@ async fn test_register_validators() -> Result<()> {
6161
Ok(())
6262
}
6363

64-
#[tokio::test]
65-
async fn test_register_validators_returns_422_if_request_is_malformed() -> Result<()> {
66-
setup_test_env();
67-
let signer = random_secret();
68-
let pubkey: BlsPublicKey = signer.public_key();
69-
70-
let chain = Chain::Holesky;
71-
let pbs_port = 4100;
72-
73-
// Run a mock relay
74-
let relays = vec![generate_mock_relay(pbs_port + 1, pubkey)?];
75-
let mock_state = Arc::new(MockRelayState::new(chain, signer));
76-
tokio::spawn(start_mock_relay_service(mock_state.clone(), pbs_port + 1));
77-
78-
// Run the PBS service
79-
let config = to_pbs_config(chain, get_pbs_static_config(pbs_port), relays);
80-
let state = PbsState::new(config);
81-
tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state));
82-
83-
// leave some time to start servers
84-
tokio::time::sleep(Duration::from_millis(100)).await;
85-
86-
let mock_validator = MockValidator::new(pbs_port)?;
87-
let url = mock_validator.comm_boost.register_validator_url().unwrap();
88-
info!("Sending register validator");
89-
90-
// Bad fee recipient
91-
let bad_json = r#"[{
92-
"message": {
93-
"fee_recipient": "0xaa",
94-
"gas_limit": "100000",
95-
"timestamp": "1000000",
96-
"pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
97-
},
98-
"signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
99-
}]"#;
100-
101-
let res = mock_validator
102-
.comm_boost
103-
.client
104-
.post(url.clone())
105-
.header("Content-Type", "application/json")
106-
.body(bad_json)
107-
.send()
108-
.await?;
109-
110-
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
111-
112-
// Bad pubkey
113-
let bad_json = r#"[{
114-
"message": {
115-
"fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
116-
"gas_limit": "100000",
117-
"timestamp": "1000000",
118-
"pubkey": "0xbbb"
119-
},
120-
"signature": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
121-
}]"#;
122-
123-
let res = mock_validator
124-
.comm_boost
125-
.client
126-
.post(url.clone())
127-
.header("Content-Type", "application/json")
128-
.body(bad_json)
129-
.send()
130-
.await?;
131-
132-
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
133-
134-
// Bad signature
135-
let bad_json = r#"[{
136-
"message": {
137-
"fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
138-
"gas_limit": "100000",
139-
"timestamp": "1000000",
140-
"pubkey": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
141-
},
142-
"signature": "0xcccc"
143-
}]"#;
144-
145-
let res = mock_validator
146-
.comm_boost
147-
.client
148-
.post(url.clone())
149-
.header("Content-Type", "application/json")
150-
.body(bad_json)
151-
.send()
152-
.await?;
153-
154-
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
155-
156-
// gas limit too high
157-
let bad_json = r#"[{
158-
"message": {
159-
"fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
160-
"gas_limit": "10000000000000000000000000000000000000000000000000000000",
161-
"timestamp": "1000000",
162-
"pubkey": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
163-
},
164-
"signature": "0xcccc"
165-
}]"#;
166-
167-
let res = mock_validator
168-
.comm_boost
169-
.client
170-
.post(url.clone())
171-
.header("Content-Type", "application/json")
172-
.body(bad_json)
173-
.send()
174-
.await?;
175-
176-
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
177-
178-
// timestamp too high
179-
let bad_json = r#"[{
180-
"message": {
181-
"fee_recipient": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
182-
"gas_limit": "1000000",
183-
"timestamp": "10000000000000000000000000000000000000000000000000000000",
184-
"pubkey": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
185-
},
186-
"signature": "0xcccc"
187-
}]"#;
188-
189-
let res = mock_validator
190-
.comm_boost
191-
.client
192-
.post(url.clone())
193-
.header("Content-Type", "application/json")
194-
.body(bad_json)
195-
.send()
196-
.await?;
197-
198-
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
199-
200-
assert_eq!(mock_state.received_register_validator(), 0);
201-
Ok(())
202-
}
203-
20464
#[tokio::test]
20565
async fn test_register_validators_does_not_retry_on_429() -> Result<()> {
20666
setup_test_env();

0 commit comments

Comments
 (0)