Skip to content

Commit 8edbc5a

Browse files
committed
remove all hardcoded ports from tests
1 parent e562003 commit 8edbc5a

11 files changed

Lines changed: 261 additions & 140 deletions

benches/microbench/src/get_header.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ use axum::http::HeaderMap;
4343
use cb_common::{pbs::GetHeaderParams, signer::random_secret, types::Chain, utils::EncodingType};
4444
use cb_pbs::{PbsState, get_header};
4545
use cb_tests::{
46-
mock_relay::{MockRelayState, start_mock_relay_service},
47-
utils::{generate_mock_relay, get_pbs_config, to_pbs_config},
46+
mock_relay::{MockRelayState, start_mock_relay_service_with_listener},
47+
utils::{generate_mock_relay, get_free_listener, get_pbs_config, to_pbs_config},
4848
};
4949
use criterion::{Criterion, black_box, criterion_group, criterion_main};
5050

51-
// Ports 19201–19205 are reserved for the microbenchmark mock relays.
52-
const BASE_PORT: u16 = 19200;
51+
// Mock relay ports are allocated dynamically via get_free_listener() so that
52+
// parallel test/bench runs don't collide on hardcoded ports.
5353
const CHAIN: Chain = Chain::Hoodi;
5454
const MAX_RELAYS: usize = 5;
5555
const RELAY_COUNTS: [usize; 3] = [1, 3, MAX_RELAYS];
@@ -83,10 +83,23 @@ fn bench_get_header(c: &mut Criterion) {
8383
let pubkey = signer.public_key();
8484
let mock_state = Arc::new(MockRelayState::new(CHAIN, signer));
8585

86-
let relay_clients: Vec<_> = (0..MAX_RELAYS)
87-
.map(|i| {
88-
let port = BASE_PORT + 1 + i as u16;
89-
tokio::spawn(start_mock_relay_service(mock_state.clone(), port));
86+
// Allocate all listeners upfront so each port is reserved until the
87+
// server takes ownership — avoids TOCTOU bind races.
88+
let listeners: Vec<_> = {
89+
let mut v = Vec::with_capacity(MAX_RELAYS);
90+
for _ in 0..MAX_RELAYS {
91+
v.push(get_free_listener().await);
92+
}
93+
v
94+
};
95+
let ports: Vec<u16> = listeners.iter().map(|l| l.local_addr().unwrap().port()).collect();
96+
97+
let relay_clients: Vec<_> = listeners
98+
.into_iter()
99+
.enumerate()
100+
.map(|(i, listener)| {
101+
let port = ports[i];
102+
tokio::spawn(start_mock_relay_service_with_listener(mock_state.clone(), listener));
90103
generate_mock_relay(port, pubkey.clone()).expect("relay client")
91104
})
92105
.collect();

tests/src/mock_relay.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,17 @@ use tracing::{debug, error};
4040
use tree_hash::TreeHash;
4141

4242
pub async fn start_mock_relay_service(state: Arc<MockRelayState>, port: u16) -> eyre::Result<()> {
43-
let app = mock_relay_app_router(state);
44-
4543
let socket = SocketAddr::new("0.0.0.0".parse()?, port);
4644
let listener = TcpListener::bind(socket).await?;
45+
start_mock_relay_service_with_listener(state, listener).await
46+
}
4747

48+
/// Like [`start_mock_relay_service`], but accepts a pre-bound [`TcpListener`].
49+
pub async fn start_mock_relay_service_with_listener(
50+
state: Arc<MockRelayState>,
51+
listener: TcpListener,
52+
) -> eyre::Result<()> {
53+
let app = mock_relay_app_router(state);
4854
axum::serve(listener, app).await?;
4955
Ok(())
5056
}

tests/src/mock_ssv_public.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ pub async fn create_mock_public_ssv_server(
3030
port: u16,
3131
state: Option<PublicSsvMockState>,
3232
) -> Result<JoinHandle<()>, axum::Error> {
33+
let address = SocketAddr::from(([127, 0, 0, 1], port));
34+
let listener = TcpListener::bind(address).await.map_err(axum::Error::new)?;
35+
create_mock_public_ssv_server_with_listener(listener, state).await
36+
}
37+
38+
/// Like [`create_mock_public_ssv_server`], but accepts a pre-bound
39+
/// [`TcpListener`].
40+
pub async fn create_mock_public_ssv_server_with_listener(
41+
listener: TcpListener,
42+
state: Option<PublicSsvMockState>,
43+
) -> Result<JoinHandle<()>, axum::Error> {
44+
let port = listener.local_addr().map(|a| a.port()).unwrap_or(0);
3345
let data = include_str!("../../tests/data/ssv_valid_public.json");
3446
let response =
3547
serde_json::from_str::<SSVPublicResponse>(data).expect("failed to parse test data");
@@ -46,8 +58,6 @@ pub async fn create_mock_public_ssv_server(
4658
.with_state(state)
4759
.into_make_service();
4860

49-
let address = SocketAddr::from(([127, 0, 0, 1], port));
50-
let listener = TcpListener::bind(address).await.map_err(axum::Error::new)?;
5161
let server = axum::serve(listener, router).with_graceful_shutdown(async {
5262
tokio::signal::ctrl_c().await.expect("Failed to listen for shutdown signal");
5363
});

tests/src/utils.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ pub fn get_local_address(port: u16) -> String {
2727
format!("http://0.0.0.0:{port}")
2828
}
2929

30+
/// Bind to port 0 and let the OS assign an unused ephemeral port.
31+
///
32+
/// The returned listener keeps the port reserved. Pass it to
33+
/// [`start_mock_relay_service_with_listener`] so the socket is never released
34+
/// between allocation and use (zero TOCTOU race). For servers that bind
35+
/// internally (e.g. `PbsService::run`), read the port with
36+
/// `listener.local_addr().unwrap().port()`, then `drop` the listener
37+
/// immediately before starting the server.
38+
pub async fn get_free_listener() -> tokio::net::TcpListener {
39+
tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap()
40+
}
41+
3042
static SYNC_SETUP: Once = Once::new();
3143
pub fn setup_test_env() {
3244
SYNC_SETUP.call_once(|| {

tests/tests/pbs_cfg_file_update.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ use cb_common::{
1212
};
1313
use cb_pbs::{DefaultBuilderApi, PbsService, PbsState};
1414
use cb_tests::{
15-
mock_relay::{MockRelayState, start_mock_relay_service},
15+
mock_relay::{MockRelayState, start_mock_relay_service_with_listener},
1616
mock_validator::MockValidator,
17-
utils::{generate_mock_relay, get_pbs_config, setup_test_env, to_pbs_config},
17+
utils::{
18+
generate_mock_relay, get_free_listener, get_pbs_config, setup_test_env, to_pbs_config,
19+
},
1820
};
1921
use eyre::Result;
2022
use lh_types::ForkName;
@@ -32,20 +34,23 @@ async fn test_cfg_file_update() -> Result<()> {
3234
let pubkey = signer.public_key();
3335

3436
let chain = Chain::Hoodi;
35-
let pbs_port = 3730;
37+
let pbs_listener = get_free_listener().await;
38+
let relay1_listener = get_free_listener().await;
39+
let relay2_listener = get_free_listener().await;
40+
let pbs_port = pbs_listener.local_addr().unwrap().port();
41+
let relay1_port = relay1_listener.local_addr().unwrap().port();
42+
let relay2_port = relay2_listener.local_addr().unwrap().port();
3643

3744
// Start relay 1
38-
let relay1_port = pbs_port + 1;
3945
let relay1 = generate_mock_relay(relay1_port, pubkey.clone())?;
4046
let relay1_state = Arc::new(MockRelayState::new(chain, signer.clone()));
41-
tokio::spawn(start_mock_relay_service(relay1_state.clone(), relay1_port));
47+
tokio::spawn(start_mock_relay_service_with_listener(relay1_state.clone(), relay1_listener));
4248

4349
// Start relay 2
44-
let relay2_port = relay1_port + 1;
4550
let relay2 = generate_mock_relay(relay2_port, pubkey.clone())?;
4651
let relay2_id = relay2.id.clone().to_string();
4752
let relay2_state = Arc::new(MockRelayState::new(chain, signer));
48-
tokio::spawn(start_mock_relay_service(relay2_state.clone(), relay2_port));
53+
tokio::spawn(start_mock_relay_service_with_listener(relay2_state.clone(), relay2_listener));
4954

5055
// Make a config with relay 1 only
5156
let pbs_config = PbsConfig {
@@ -109,6 +114,7 @@ async fn test_cfg_file_update() -> Result<()> {
109114
// Run the PBS service
110115
let config = to_pbs_config(chain, get_pbs_config(pbs_port), vec![relay1.clone()]);
111116
let state = PbsState::new(config, config_path.clone());
117+
drop(pbs_listener);
112118
tokio::spawn(PbsService::run::<(), DefaultBuilderApi>(state));
113119

114120
// leave some time to start servers - extra time for the file watcher

0 commit comments

Comments
 (0)