diff --git a/Cargo.lock b/Cargo.lock index 379f0c7b611..a631d1d8b6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2499,13 +2499,13 @@ dependencies = [ "hotshot-state-prover", "hotshot-types", "jf-signature 0.4.0 (git+https://github.com/EspressoSystems/jellyfish?tag=jf-signature-v0.4.0)", - "portpicker", "rand 0.8.5", "sequencer", "sequencer-utils", "surf-disco", "tempfile", "test-log", + "test-utils", "tide-disco", "tokio", "tracing", @@ -2988,12 +2988,12 @@ dependencies = [ "hotshot-types", "nohash-hasher", "parking_lot", - "portpicker", "quickcheck", "rand 0.9.2", "serde", "serde_bytes", "snow", + "test-utils", "thiserror 2.0.17", "tokio", "tracing", @@ -4434,7 +4434,6 @@ dependencies = [ "hotshot-types", "itertools 0.12.1", "jf-merkle-tree-compat", - "portpicker", "rand 0.8.5", "rstest", "sequencer", @@ -4445,6 +4444,7 @@ dependencies = [ "surf-disco", "tempfile", "test-log", + "test-utils", "tide-disco", "tokio", "toml", @@ -4513,7 +4513,6 @@ dependencies = [ "num-traits", "parking_lot", "paste", - "portpicker", "pretty_assertions", "rand 0.8.5", "rstest", @@ -4527,6 +4526,7 @@ dependencies = [ "surf-disco", "tagged-base64", "test-log", + "test-utils", "thiserror 2.0.17", "tide-disco", "time 0.3.47", @@ -5533,10 +5533,10 @@ dependencies = [ "lru 0.12.5", "num_enum", "parking_lot", - "portpicker", "rand 0.8.5", "serde", "sha2 0.10.9", + "test-utils", "time 0.3.47", "tokio", "tracing", @@ -5588,10 +5588,10 @@ dependencies = [ "hotshot-types", "lru 0.12.5", "num_cpus", - "portpicker", "serde", "sha2 0.10.9", "tagged-base64", + "test-utils", "tide-disco", "tokio", "tracing", @@ -5627,11 +5627,11 @@ dependencies = [ "jf-advz", "lru 0.12.5", "num_cpus", - "portpicker", "serde", "sha2 0.10.9", "tagged-base64", "test-log", + "test-utils", "tide-disco", "tokio", "tracing", @@ -5665,12 +5665,12 @@ dependencies = [ "hotshot-types", "jf-advz", "nonempty-collections", - "portpicker", "quick_cache", "rand 0.8.5", "serde", "sha2 0.10.9", "surf-disco", + "test-utils", "thiserror 2.0.17", "tide-disco", "tokio", @@ -5726,12 +5726,12 @@ dependencies = [ "futures", "hotshot-example-types", "hotshot-types", - "portpicker", "rand 0.8.5", "semver 1.0.27", "serde", "snafu 0.8.9", "surf-disco", + "test-utils", "tide-disco", "tokio", "toml", @@ -5789,13 +5789,13 @@ dependencies = [ "hotshot-testing", "hotshot-types", "local-ip-address", - "portpicker", "rand 0.8.5", "reqwest", "sequencer", "serde", "sha2 0.10.9", "surf-disco", + "test-utils", "time 0.3.47", "tokio", "toml", @@ -5902,7 +5902,6 @@ dependencies = [ "jf-merkle-tree-compat", "lazy_static", "log", - "portpicker", "prometheus", "rand 0.8.5", "refinery", @@ -5917,6 +5916,7 @@ dependencies = [ "tagged-base64", "tempfile", "test-log", + "test-utils", "tide-disco", "time 0.3.47", "tokio", @@ -6053,13 +6053,13 @@ dependencies = [ "itertools 0.14.0", "jf-advz", "lru 0.12.5", - "portpicker", "rand 0.8.5", "reqwest", "serde", "sha2 0.10.9", "tagged-base64", "test-log", + "test-utils", "thiserror 2.0.17", "tide-disco", "tokio", @@ -7797,7 +7797,6 @@ dependencies = [ "jf-advz", "jf-merkle-tree-compat", "light-client", - "portpicker", "pretty_assertions", "rand 0.8.5", "sequencer", @@ -7807,6 +7806,7 @@ dependencies = [ "static_assertions", "surf-disco", "test-log", + "test-utils", "tokio", "tracing", "vbs", @@ -10688,7 +10688,6 @@ dependencies = [ "moka", "num_enum", "parking_lot", - "portpicker", "pretty_assertions", "priority-queue", "rand 0.8.5", @@ -10713,6 +10712,7 @@ dependencies = [ "tagged-base64", "tempfile", "test-log", + "test-utils", "thiserror 2.0.17", "tide-disco", "time 0.3.47", @@ -10750,7 +10750,6 @@ dependencies = [ "hotshot", "hotshot-example-types", "log-panics", - "portpicker", "serde", "serde_json", "surf", @@ -11139,7 +11138,6 @@ dependencies = [ "hotshot-types", "itertools 0.12.1", "jf-merkle-tree-compat", - "portpicker", "rand 0.8.5", "reqwest", "rstest", @@ -11150,6 +11148,7 @@ dependencies = [ "surf-disco", "tempfile", "test-log", + "test-utils", "tide-disco", "tokio", "tracing", @@ -11564,7 +11563,6 @@ dependencies = [ "hotshot-types", "jf-merkle-tree-compat", "jf-signature 0.4.0 (git+https://github.com/EspressoSystems/jellyfish?tag=jf-signature-v0.4.0)", - "portpicker", "predicates", "pretty_assertions", "prometheus-parse", @@ -11582,6 +11580,7 @@ dependencies = [ "tagged-base64", "tempfile", "test-log", + "test-utils", "thiserror 2.0.17", "tokio", "toml", @@ -12009,6 +12008,10 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "test-utils" +version = "0.1.0" + [[package]] name = "tests" version = "0.1.0" @@ -12025,7 +12028,6 @@ dependencies = [ "hotshot-state-prover", "hotshot-types", "jf-signature 0.4.0 (git+https://github.com/EspressoSystems/jellyfish?tag=jf-signature-v0.4.0)", - "portpicker", "rand 0.8.5", "reqwest", "sequencer", @@ -12036,6 +12038,7 @@ dependencies = [ "tagged-base64", "tempfile", "test-log", + "test-utils", "tokio", "tracing", "tracing-subscriber 0.3.22", diff --git a/Cargo.toml b/Cargo.toml index 9a0b22310a9..0ed125e7e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "sequencer-sqlite", "slow-tests", "staking-cli", + "test-utils", "tests", "types", "utils", @@ -83,6 +84,7 @@ default-members = [ "sequencer", "slow-tests", "staking-cli", + "test-utils", "tests", "types", "utils", @@ -194,6 +196,7 @@ hotshot-utils = { path = "crates/hotshot/utils" } espresso-types = { path = "types" } light-client = { path = "light-client" } +test-utils = { path = "test-utils" } # VID import vid = { path = "vid", features = ["parallel", "keccak256"] } @@ -248,7 +251,6 @@ log-panics = { version = "2.0", features = ["with-backtrace"] } lru = "0.12" num-traits = "0.2" paste = "1.0" -portpicker = "0.1.1" pretty_assertions = { version = "1.4", features = ["unstable"] } priority-queue = "2" rand = "0.8.5" diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index 15fafcd49d1..803f9ce0945 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -29,11 +29,11 @@ hotshot-events-service = { workspace = true } hotshot-example-types = { workspace = true } hotshot-state-prover = { workspace = true } hotshot-types = { workspace = true } -portpicker = { workspace = true } rand = { workspace = true } sequencer = { path = "../../sequencer", default-features = false } sequencer-utils = { path = "../../utils" } surf-disco = { workspace = true } +test-utils = { workspace = true } tide-disco = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index b8351d7b525..2d0ff3d394a 100755 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -239,18 +239,6 @@ pub mod testing { } } - // url for the hotshot event streaming api - pub fn hotshot_event_streaming_api_url() -> Url { - // spawn the event streaming api - let port = portpicker::pick_unused_port() - .expect("Could not find an open port for hotshot event streaming api"); - - let hotshot_events_streaming_api_url = - Url::parse(format!("http://localhost:{port}").as_str()).unwrap(); - - hotshot_events_streaming_api_url - } - // start the server for the hotshot event streaming api pub fn run_hotshot_event_streaming_api( url: Url, @@ -406,12 +394,9 @@ pub mod testing { pub fn hotshot_builder_url() -> Url { // spawn the builder api let port = - portpicker::pick_unused_port().expect("Could not find an open port for builder api"); - - let hotshot_builder_api_url = - Url::parse(format!("http://localhost:{port}").as_str()).unwrap(); + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); - hotshot_builder_api_url + Url::parse(format!("http://localhost:{port}").as_str()).unwrap() } pub async fn test_builder_impl( diff --git a/crates/builder/src/non_permissioned.rs b/crates/builder/src/non_permissioned.rs index 517e2f67579..765ddb7ae9f 100644 --- a/crates/builder/src/non_permissioned.rs +++ b/crates/builder/src/non_permissioned.rs @@ -246,7 +246,6 @@ impl BuilderConfig { mod test { use espresso_types::MockSequencerVersions; use futures::StreamExt; - use portpicker::pick_unused_port; use sequencer::{ api::{ options::HotshotEvents, @@ -267,11 +266,13 @@ mod test { /// Builder subscrived to this api, and server the hotshot client request and the private mempool tx submission #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_non_permissioned_builder() { - let query_port = pick_unused_port().expect("No ports free"); + let query_port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let event_service_url: Url = format!("http://localhost:{query_port}").parse().unwrap(); - let builder_port = pick_unused_port().expect("No ports free"); + let builder_port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let builder_api_url: Url = format!("http://localhost:{builder_port}").parse().unwrap(); let network_config = TestConfigBuilder::default().build(); diff --git a/crates/cliquenet/Cargo.toml b/crates/cliquenet/Cargo.toml index 13b9d1db382..8d2f8f84d27 100644 --- a/crates/cliquenet/Cargo.toml +++ b/crates/cliquenet/Cargo.toml @@ -28,8 +28,8 @@ hotshot-types = { workspace = true, optional = true } [dev-dependencies] criterion = "0.8.1" -portpicker = { workspace = true } quickcheck = { workspace = true } +test-utils = { workspace = true } tracing-subscriber = { workspace = true } [[bench]] diff --git a/crates/cliquenet/benches/bench1.rs b/crates/cliquenet/benches/bench1.rs index 6a739d50b4d..8f72407ffbc 100644 --- a/crates/cliquenet/benches/bench1.rs +++ b/crates/cliquenet/benches/bench1.rs @@ -5,6 +5,7 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m #[cfg(feature = "metrics")] use hotshot_types::traits::metrics::NoMetrics; use rand::RngCore; +use test_utils::reserve_tcp_port; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream}, @@ -47,22 +48,19 @@ async fn setup_cliquenet() -> (Retry, Retry) { let a = Keypair::generate().unwrap(); let b = Keypair::generate().unwrap(); + let port_a = reserve_tcp_port().expect("OS should have ephemeral ports available"); + let port_b = reserve_tcp_port().expect("OS should have ephemeral ports available"); + let all: [(u8, PublicKey, Address); 2] = [ ( A, a.public_key(), - Address::from(( - Ipv4Addr::from([127, 0, 0, 1]), - portpicker::pick_unused_port().unwrap(), - )), + Address::from((Ipv4Addr::from([127, 0, 0, 1]), port_a)), ), ( B, b.public_key(), - Address::from(( - Ipv4Addr::from([127, 0, 0, 1]), - portpicker::pick_unused_port().unwrap(), - )), + Address::from((Ipv4Addr::from([127, 0, 0, 1]), port_b)), ), ]; diff --git a/crates/hotshot-builder/legacy/Cargo.toml b/crates/hotshot-builder/legacy/Cargo.toml index c8abf6dfdbb..00812186419 100644 --- a/crates/hotshot-builder/legacy/Cargo.toml +++ b/crates/hotshot-builder/legacy/Cargo.toml @@ -37,7 +37,7 @@ hotshot-macros = { workspace = true } hotshot-task-impls = { workspace = true } hotshot-testing = { workspace = true } num_cpus = { workspace = true } -portpicker = { workspace = true } +test-utils = { workspace = true } tracing-test = { workspace = true } url = { workspace = true } diff --git a/crates/hotshot-builder/refactored/Cargo.toml b/crates/hotshot-builder/refactored/Cargo.toml index ab44ff061f6..86676e10c1d 100644 --- a/crates/hotshot-builder/refactored/Cargo.toml +++ b/crates/hotshot-builder/refactored/Cargo.toml @@ -38,8 +38,8 @@ hotshot-macros = { workspace = true } hotshot-task-impls = { workspace = true } hotshot-testing = { workspace = true } num_cpus = { workspace = true } -portpicker = { workspace = true } test-log = { workspace = true } +test-utils = { workspace = true } tracing-test = { workspace = true } url = { workspace = true } diff --git a/crates/hotshot-builder/refactored/src/testing/mod.rs b/crates/hotshot-builder/refactored/src/testing/mod.rs index 0d90be98328..b1032d8dea6 100644 --- a/crates/hotshot-builder/refactored/src/testing/mod.rs +++ b/crates/hotshot-builder/refactored/src/testing/mod.rs @@ -73,7 +73,7 @@ impl TestServiceWrapper { global_state: Arc>, event_stream_sender: Sender>, ) -> Self { - let port = portpicker::pick_unused_port().unwrap(); + let port = test_utils::reserve_tcp_port().unwrap(); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let app = Arc::clone(&global_state).into_app().unwrap(); spawn(app.serve(url.clone(), StaticVersion::<0, 1> {})); diff --git a/crates/hotshot-builder/shared/Cargo.toml b/crates/hotshot-builder/shared/Cargo.toml index f0a0adaf9c4..ff20763f8b4 100644 --- a/crates/hotshot-builder/shared/Cargo.toml +++ b/crates/hotshot-builder/shared/Cargo.toml @@ -39,6 +39,6 @@ vbs = { workspace = true } vec1 = { workspace = true } [dev-dependencies] -portpicker = { workspace = true } +test-utils = { workspace = true } tide-disco = { workspace = true } tracing-test = { workspace = true } diff --git a/crates/hotshot-builder/shared/src/utils/event_service_wrapper.rs b/crates/hotshot-builder/shared/src/utils/event_service_wrapper.rs index 34b8ede7780..aa261b3ff15 100644 --- a/crates/hotshot-builder/shared/src/utils/event_service_wrapper.rs +++ b/crates/hotshot-builder/shared/src/utils/event_service_wrapper.rs @@ -31,7 +31,7 @@ pub struct EventServiceStream { impl EventServiceStream { /// Maximum period between events, once it elapsed we assume - /// udnerlying connection silently went down and attempt to reconnect + /// underlying connection silently went down and attempt to reconnect const MAX_WAIT_PERIOD: Duration = Duration::from_secs(10); const RETRY_PERIOD: Duration = Duration::from_secs(1); const CONNECTION_TIMEOUT: Duration = Duration::from_secs(60); @@ -251,12 +251,8 @@ mod tests { async fn test_event_stream_wrapper() { const TIMEOUT: Duration = Duration::from_secs(3); - let url: Url = format!( - "http://localhost:{}", - portpicker::pick_unused_port().unwrap() - ) - .parse() - .unwrap(); + let port = test_utils::reserve_tcp_port().unwrap(); + let url: Url = format!("http://localhost:{port}").parse().unwrap(); let app_handle = run_app("hotshot-events", url.clone()); @@ -296,12 +292,8 @@ mod tests { async fn test_event_stream_wrapper_with_idle_timeout() { const TIMEOUT: Duration = Duration::from_secs(3); - let url: Url = format!( - "http://localhost:{}", - portpicker::pick_unused_port().unwrap() - ) - .parse() - .unwrap(); + let port = test_utils::reserve_tcp_port().unwrap(); + let url: Url = format!("http://localhost:{port}").parse().unwrap(); let app_handle = run_app("hotshot-events", url.clone()); diff --git a/crates/hotshot/examples/Cargo.toml b/crates/hotshot/examples/Cargo.toml index 6606b490063..31b5fcce6e5 100644 --- a/crates/hotshot/examples/Cargo.toml +++ b/crates/hotshot/examples/Cargo.toml @@ -33,13 +33,13 @@ hotshot-orchestrator = { workspace = true } hotshot-testing = { workspace = true } hotshot-types = { workspace = true } local-ip-address = "0.6" -portpicker = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } sequencer = { path = "../../../sequencer", default-features = false } serde = { workspace = true, features = ["rc"] } sha2 = { workspace = true } surf-disco = { workspace = true } +test-utils = { workspace = true } time = { workspace = true } tokio = { workspace = true } diff --git a/crates/hotshot/examples/combined/all.rs b/crates/hotshot/examples/combined/all.rs index 89864af1d1b..200ff5d2948 100644 --- a/crates/hotshot/examples/combined/all.rs +++ b/crates/hotshot/examples/combined/all.rs @@ -61,9 +61,11 @@ async fn main() { // 2 brokers for _ in 0..2 { - // Get the ports to bind to - let private_port = portpicker::pick_unused_port().expect("could not find an open port"); - let public_port = portpicker::pick_unused_port().expect("could not find an open port"); + // Atomically bind to available ports + let private_port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); + let public_port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); // Extrapolate addresses let private_address = format!("127.0.0.1:{private_port}"); diff --git a/crates/hotshot/examples/infra/mod.rs b/crates/hotshot/examples/infra/mod.rs index 1b5cb07cc88..6de3148003e 100755 --- a/crates/hotshot/examples/infra/mod.rs +++ b/crates/hotshot/examples/infra/mod.rs @@ -1061,7 +1061,8 @@ where match args.builder_address { None => { - let port = portpicker::pick_unused_port().expect("Failed to pick an unused port"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); advertise_urls = local_ip_address::list_afinet_netifas() .expect("Couldn't get list of local IP addresses") .into_iter() diff --git a/crates/hotshot/examples/push-cdn/all.rs b/crates/hotshot/examples/push-cdn/all.rs index 444edc60fe7..92f0ce5f250 100644 --- a/crates/hotshot/examples/push-cdn/all.rs +++ b/crates/hotshot/examples/push-cdn/all.rs @@ -72,9 +72,11 @@ async fn main() { // 2 brokers for _ in 0..2 { - // Get the ports to bind to - let private_port = portpicker::pick_unused_port().expect("could not find an open port"); - let public_port = portpicker::pick_unused_port().expect("could not find an open port"); + // Atomically bind to available ports + let private_port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); + let public_port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); // Extrapolate addresses let private_address = format!("127.0.0.1:{private_port}"); diff --git a/crates/hotshot/hotshot/Cargo.toml b/crates/hotshot/hotshot/Cargo.toml index 57157129c47..6b4c3a29762 100644 --- a/crates/hotshot/hotshot/Cargo.toml +++ b/crates/hotshot/hotshot/Cargo.toml @@ -47,10 +47,10 @@ libp2p-identity = { workspace = true } lru = { workspace = true } num_enum = "0.7" parking_lot = { workspace = true } -portpicker = "0.1" rand = { workspace = true } serde = { workspace = true, features = ["rc"] } sha2 = { workspace = true } +test-utils = { workspace = true } time = { workspace = true } tokio = { workspace = true } diff --git a/crates/hotshot/hotshot/src/traits/networking/cliquenet_network.rs b/crates/hotshot/hotshot/src/traits/networking/cliquenet_network.rs index d3659a2fe10..58fdd2a9e73 100644 --- a/crates/hotshot/hotshot/src/traits/networking/cliquenet_network.rs +++ b/crates/hotshot/hotshot/src/traits/networking/cliquenet_network.rs @@ -151,16 +151,17 @@ impl TestableNetworkingImplementation for Cliquenet { _reliability_config: Option>, _secondary_network_delay: Duration, ) -> AsyncGenerator> { - let mut parties = Vec::new(); - for i in 0..expected_node_count { - use std::net::Ipv4Addr; + use std::net::Ipv4Addr; - use cliquenet::Address; + use cliquenet::Address; + let mut parties: Vec<(Keypair, T::SignatureKey, Address)> = Vec::new(); + for i in 0..expected_node_count { let secret = T::SignatureKey::generated_from_seed_indexed([0u8; 32], i as u64).1; let public = T::SignatureKey::from_private(&secret); let kpair = derive_keypair::<::SignatureKey>(&secret); - let port = portpicker::pick_unused_port().expect("an unused port is available"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let addr = Address::Inet(Ipv4Addr::LOCALHOST.into(), port); parties.push((kpair, public, addr)); diff --git a/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs b/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs index 7ef8a5bdcce..cb4e2d2dbf0 100644 --- a/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs +++ b/crates/hotshot/hotshot/src/traits/networking/libp2p_network.rs @@ -224,8 +224,12 @@ impl TestableNetworkingImplementation for Libp2pNetwork { node_id < num_bootstrap as u64 ); - // pick a free, unused UDP port for testing - let port = portpicker::pick_unused_port().expect("Could not find an open port"); + // UDP has no TIME_WAIT, so there's a tiny race before libp2p binds. + let port = std::net::UdpSocket::bind("127.0.0.1:0") + .expect("UDP socket should bind") + .local_addr() + .expect("UDP socket should have local addr") + .port(); let addr = Multiaddr::from_str(&format!("/ip4/127.0.0.1/udp/{port}/quic-v1")).unwrap(); diff --git a/crates/hotshot/hotshot/src/traits/networking/push_cdn_network.rs b/crates/hotshot/hotshot/src/traits/networking/push_cdn_network.rs index 61e9efa5f49..44036bda20a 100644 --- a/crates/hotshot/hotshot/src/traits/networking/push_cdn_network.rs +++ b/crates/hotshot/hotshot/src/traits/networking/push_cdn_network.rs @@ -48,6 +48,8 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use parking_lot::Mutex; #[cfg(feature = "hotshot-testing")] use rand::{rngs::StdRng, RngCore, SeedableRng}; +#[cfg(feature = "hotshot-testing")] +use test_utils::reserve_tcp_port; use tokio::sync::mpsc::error::TrySendError; #[cfg(feature = "hotshot-testing")] use tokio::{spawn, time::sleep}; @@ -339,20 +341,17 @@ impl TestableNetworkingImplementation .to_string_lossy() .into_owned(); - // Pick some unused public ports - let public_address_1 = format!( - "127.0.0.1:{}", - portpicker::pick_unused_port().expect("could not find an open port") - ); - let public_address_2 = format!( - "127.0.0.1:{}", - portpicker::pick_unused_port().expect("could not find an open port") - ); + // Atomically bind to unused public ports + let public_port_1 = reserve_tcp_port().expect("OS should have ephemeral ports available"); + let public_port_2 = reserve_tcp_port().expect("OS should have ephemeral ports available"); + let public_address_1 = format!("127.0.0.1:{public_port_1}"); + let public_address_2 = format!("127.0.0.1:{public_port_2}"); // 2 brokers for i in 0..2 { - // Get the ports to bind to - let private_port = portpicker::pick_unused_port().expect("could not find an open port"); + // Atomically bind to a private port + let private_port = + reserve_tcp_port().expect("OS should have ephemeral ports available"); // Extrapolate addresses let private_address = format!("127.0.0.1:{private_port}"); @@ -406,8 +405,8 @@ impl TestableNetworkingImplementation }); } - // Get the port to use for the marshal - let marshal_port = portpicker::pick_unused_port().expect("could not find an open port"); + // Atomically bind to an available port for the marshal + let marshal_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); // Configure the marshal let marshal_endpoint = format!("127.0.0.1:{marshal_port}"); diff --git a/crates/hotshot/testing/Cargo.toml b/crates/hotshot/testing/Cargo.toml index 9a6d68050f2..2584092078e 100644 --- a/crates/hotshot/testing/Cargo.toml +++ b/crates/hotshot/testing/Cargo.toml @@ -36,12 +36,12 @@ hotshot-utils = { workspace = true } itertools = "0.14.0" jf-advz = { workspace = true } lru = { workspace = true } -portpicker = { workspace = true } rand = { workspace = true, features = ["small_rng"] } reqwest = { workspace = true } serde = { workspace = true } sha2 = { workspace = true } tagged-base64 = { workspace = true } +test-utils = { workspace = true } thiserror = { workspace = true } tide-disco = { workspace = true } tokio = { workspace = true } diff --git a/crates/hotshot/testing/src/test_runner.rs b/crates/hotshot/testing/src/test_runner.rs index 54df0a9b567..5f7a6f3d01c 100644 --- a/crates/hotshot/testing/src/test_runner.rs +++ b/crates/hotshot/testing/src/test_runner.rs @@ -315,8 +315,15 @@ where ) -> (Vec>>, Vec) { let mut builder_tasks = Vec::new(); let mut builder_urls = Vec::new(); - for metadata in &self.launcher.metadata.builders { - let builder_port = portpicker::pick_unused_port().expect("No free ports"); + + let mut ports = Vec::new(); + for _ in &self.launcher.metadata.builders { + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); + ports.push(port); + } + + for (metadata, builder_port) in self.launcher.metadata.builders.iter().zip(&ports) { let builder_url = Url::parse(&format!("http://localhost:{builder_port}")).expect("Invalid URL"); let builder_task = B::start( diff --git a/crates/hotshot/testing/tests/tests_1/block_builder.rs b/crates/hotshot/testing/tests/tests_1/block_builder.rs index f7ffe81080d..f9a7a3269b9 100644 --- a/crates/hotshot/testing/tests/tests_1/block_builder.rs +++ b/crates/hotshot/testing/tests/tests_1/block_builder.rs @@ -23,6 +23,7 @@ use hotshot_types::{ network::RandomBuilderConfig, traits::{node_implementation::NodeType, signature_key::SignatureKey, BlockPayload}, }; +use test_utils::reserve_tcp_port; use tide_disco::Url; use tokio::time::sleep; use vbs::version::StaticVersion; @@ -34,7 +35,7 @@ async fn test_random_block_builder() { use hotshot_example_types::node_types::TestVersions; use vbs::version::Version; - let port = portpicker::pick_unused_port().expect("No free ports"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let api_url = Url::parse(&format!("http://localhost:{port}")).expect("Valid URL"); let task: Box> = RandomBuilderImplementation::start( 1, diff --git a/espresso-dev-node/Cargo.toml b/espresso-dev-node/Cargo.toml index cc930c0d132..03b2c8d646e 100644 --- a/espresso-dev-node/Cargo.toml +++ b/espresso-dev-node/Cargo.toml @@ -22,12 +22,12 @@ hotshot-contract-adapter = { path = "../contracts/rust/adapter" } hotshot-state-prover = { workspace = true } hotshot-types = { workspace = true } itertools = { workspace = true } -portpicker = { workspace = true } sequencer-utils = { path = "../utils" } serde = { workspace = true } serde_json = { workspace = true } staking-cli = { workspace = true } tempfile = { workspace = true } +test-utils = { workspace = true } tide-disco = { workspace = true } tokio = { workspace = true } toml = { workspace = true } @@ -40,7 +40,6 @@ committable = { workspace = true } escargot = "0.5.10" hotshot-query-service = { workspace = true } jf-merkle-tree-compat = { workspace = true } -portpicker = { workspace = true } rand = { workspace = true } rstest = { workspace = true } surf-disco = { workspace = true } diff --git a/espresso-dev-node/src/main.rs b/espresso-dev-node/src/main.rs index c8c94fe99ac..49264b055de 100644 --- a/espresso-dev-node/src/main.rs +++ b/espresso-dev-node/src/main.rs @@ -9,7 +9,7 @@ use alloy::{ network::EthereumWallet, node_bindings::Anvil, primitives::{Address, Bytes, U256}, - providers::{Provider, ProviderBuilder, WalletProvider}, + providers::{layers::AnvilLayer, Provider, ProviderBuilder, WalletProvider}, rpc::client::RpcClient, signers::{ k256::ecdsa::SigningKey, @@ -38,7 +38,6 @@ use hotshot_types::{ utils::epoch_from_block_number, }; use itertools::izip; -use portpicker::pick_unused_port; use sequencer::{ api::{ options, @@ -55,6 +54,7 @@ use sequencer_utils::logging; use serde::{Deserialize, Serialize}; use staking_cli::demo::{DelegationConfig, StakingTransactions}; use tempfile::NamedTempFile; +use test_utils::reserve_tcp_port; use tide_disco::{error::ServerError, method::ReadState, Api, Error, StatusCode}; use tokio::spawn; use url::Url; @@ -283,8 +283,8 @@ async fn main() -> anyhow::Result<()> { (url, None) } else { tracing::warn!("L1 url is not provided. running an anvil node"); - let instance = Anvil::new() - .args([ + let anvil_layer = AnvilLayer::from( + Anvil::new().args([ "--slots-in-an-epoch", "0", "--mnemonic", @@ -300,14 +300,15 @@ async fn main() -> anyhow::Result<()> { .context("Failed to convert path to string")?, "--state-interval", "1", - ]) - .spawn(); - let url = instance.endpoint_url(); + ]), + ); + let url = anvil_layer.endpoint_url(); tracing::info!("l1 url: {}", url); + let instance = anvil_layer.instance().clone(); (url, Some(instance)) }; - let relay_server_port = pick_unused_port().unwrap(); + let relay_server_port = reserve_tcp_port().unwrap(); let relay_server_url: Url = format!("http://localhost:{relay_server_port}") .parse() .unwrap(); @@ -549,8 +550,8 @@ async fn main() -> anyhow::Result<()> { client_states.provider_urls.insert(chain_id, url.clone()); let lc_proxy_addr = client_states.lc_proxy_addr.get(&chain_id).unwrap(); - // init the prover config - let prover_port = prover_port.unwrap_or_else(|| pick_unused_port().unwrap()); + // init the prover config - use port 0 to let the OS assign an available port + let prover_port = prover_port.unwrap_or(0); prover_ports.push(prover_port); let l1_rpc_client = RpcClient::new_http(url); let prover_config = StateProverConfig { diff --git a/espresso-dev-node/tests/dev_node_tests.rs b/espresso-dev-node/tests/dev_node_tests.rs index 36be1bde495..9ff99e41038 100644 --- a/espresso-dev-node/tests/dev_node_tests.rs +++ b/espresso-dev-node/tests/dev_node_tests.rs @@ -20,10 +20,10 @@ use hotshot_query_service::{ explorer::TransactionDetailResponse, }; use jf_merkle_tree_compat::MerkleTreeScheme; -use portpicker::pick_unused_port; use rand::Rng; use sequencer::SequencerApiVersion; use surf_disco::Client; +use test_utils::reserve_tcp_port; use tide_disco::error::ServerError; use tokio::time::sleep; use url::Url; @@ -49,9 +49,10 @@ impl Drop for BackgroundProcess { async fn slow_dev_node_test( #[values(DevNodeVersion::V0_3, DevNodeVersion::V0_4)] version: DevNodeVersion, ) { - let builder_port = pick_unused_port().unwrap(); - let api_port = pick_unused_port().unwrap(); - let dev_node_port = pick_unused_port().unwrap(); + let builder_port = reserve_tcp_port().unwrap(); + let api_port = reserve_tcp_port().unwrap(); + let dev_node_port = reserve_tcp_port().unwrap(); + let instance = Anvil::new().spawn(); let l1_url = instance.endpoint_url(); @@ -366,9 +367,9 @@ async fn alt_chain_providers() -> (Vec, Vec) { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn slow_dev_node_multiple_lc_providers_test() { - let builder_port = pick_unused_port().unwrap(); - let api_port = pick_unused_port().unwrap(); - let dev_node_port = pick_unused_port().unwrap(); + let builder_port = reserve_tcp_port().unwrap(); + let api_port = reserve_tcp_port().unwrap(); + let dev_node_port = reserve_tcp_port().unwrap(); let instance = Anvil::new().chain_id(1).spawn(); let l1_url = instance.endpoint_url(); diff --git a/hotshot-events-service/Cargo.toml b/hotshot-events-service/Cargo.toml index b0ab4a79368..090bdc60c36 100644 --- a/hotshot-events-service/Cargo.toml +++ b/hotshot-events-service/Cargo.toml @@ -27,8 +27,8 @@ vbs = { workspace = true } [dev-dependencies] hotshot-example-types = { workspace = true } -portpicker = "0.1.1" surf-disco = { workspace = true } +test-utils = { workspace = true } [lints] workspace = true diff --git a/hotshot-events-service/src/test.rs b/hotshot-events-service/src/test.rs index 893377edc36..8aefb1f2232 100644 --- a/hotshot-events-service/src/test.rs +++ b/hotshot-events-service/src/test.rs @@ -18,6 +18,7 @@ mod tests { PeerConfig, }; use surf_disco::Client; + use test_utils::reserve_tcp_port; use tide_disco::{App, Url}; use tokio::spawn; use tracing_test::traced_test; @@ -41,7 +42,7 @@ mod tests { #[traced_test] async fn test_no_active_receiver() { tracing::info!("Starting test_no_active_receiver"); - let port = portpicker::pick_unused_port().expect("Could not find an open port"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let api_url = Url::parse(format!("http://localhost:{port}").as_str()).unwrap(); let known_nodes_with_stake = vec![]; @@ -90,7 +91,7 @@ mod tests { #[tokio::test] #[traced_test] async fn test_startup_info_endpoint() { - let port = portpicker::pick_unused_port().expect("Could not find an open port"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let api_url = Url::parse(format!("http://localhost:{port}").as_str()).unwrap(); let private_key = @@ -146,7 +147,7 @@ mod tests { async fn test_event_stream() { tracing::info!("Starting test_event_stream"); - let port = portpicker::pick_unused_port().expect("Could not find an open port"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let api_url = Url::parse(format!("http://localhost:{port}").as_str()).unwrap(); let known_nodes_with_stake = vec![]; diff --git a/hotshot-query-service/Cargo.toml b/hotshot-query-service/Cargo.toml index 741aad03644..374e9403f03 100644 --- a/hotshot-query-service/Cargo.toml +++ b/hotshot-query-service/Cargo.toml @@ -37,9 +37,9 @@ sql-data-source = ["include_dir", "refinery", "refinery-core", "sqlx", "log"] # Enable extra features useful for writing tests with a query service. testing = [ "espresso-macros", - "portpicker", "rand", "tempfile", + "test-utils", "sql-data-source", "file-system-data-source", ] @@ -80,7 +80,6 @@ jf-merkle-tree-compat = { workspace = true, features = [ ] } lazy_static = "1" log = { version = "0.4", optional = true } -portpicker = { version = "0.1", optional = true } prometheus = { version = "0.13", default-features = false } rand = { workspace = true, optional = true } refinery = { version = "0.8", features = ["tokio-postgres"], optional = true } @@ -98,6 +97,7 @@ sqlx = { version = "0.8", features = [ surf-disco = "0.9" tagged-base64 = { workspace = true } tempfile = { version = "3.10", optional = true } +test-utils = { workspace = true, optional = true } tide-disco = "0.9" time = "0.3" tokio = { version = "1", default-features = false, features = [ @@ -129,11 +129,11 @@ backtrace-on-stack-overflow = { version = "0.3", optional = true } clap = { version = "4.5", features = ["derive", "env"] } espresso-macros = { git = "https://github.com/EspressoSystems/espresso-macros.git", tag = "0.1.0" } generic-array = "0.14" -portpicker = "0.1" rand = { workspace = true } reqwest = { workspace = true } tempfile = "3.10" test-log = { workspace = true } +test-utils = { workspace = true } [[example]] name = "simple-server" diff --git a/hotshot-query-service/examples/simple-server.rs b/hotshot-query-service/examples/simple-server.rs index e6e8aa478f7..551d54de61c 100644 --- a/hotshot-query-service/examples/simple-server.rs +++ b/hotshot-query-service/examples/simple-server.rs @@ -49,6 +49,7 @@ use hotshot_types::{ traits::{election::Membership, network::Topic}, HotShotConfig, PeerConfig, }; +use test_utils::reserve_tcp_port; use tracing_subscriber::EnvFilter; use url::Url; use vbs::version::StaticVersionType; @@ -172,7 +173,7 @@ async fn init_consensus( let num_nodes_with_stake = NonZeroUsize::new(pub_keys.len()).unwrap(); // Pick a random, unused port for the builder server - let builder_port = portpicker::pick_unused_port().expect("No ports available"); + let builder_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let builder_url = Url::parse(&format!("http://0.0.0.0:{builder_port}")).expect("Failed to parse URL"); diff --git a/hotshot-query-service/src/availability.rs b/hotshot-query-service/src/availability.rs index 3ed854646c4..cdf9e6e3451 100644 --- a/hotshot-query-service/src/availability.rs +++ b/hotshot-query-service/src/availability.rs @@ -802,10 +802,10 @@ mod test { use futures::future::FutureExt; use hotshot_example_types::node_types::EpochsTestVersions; use hotshot_types::{data::Leaf2, simple_certificate::QuorumCertificate2}; - use portpicker::pick_unused_port; use serde::de::DeserializeOwned; use surf_disco::{Client, Error as _}; use tempfile::TempDir; + use test_utils::reserve_tcp_port; use tide_disco::App; use toml::toml; @@ -861,7 +861,7 @@ mod test { // Check the consistency of every block/leaf pair. for i in 0..height { // Limit the number of blocks we validate in order to - // speeed up the tests. + // speed up the tests. if ![0, 1, height / 2, height - 1].contains(&i) { continue; } @@ -1115,7 +1115,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); let options = Options { small_object_range_limit: 500, @@ -1206,7 +1206,7 @@ mod test { // Check the consistency of every block/leaf pair. for i in 0..height { // Limit the number of blocks we validate in order to - // speeed up the tests. + // speed up the tests. if ![0, 1, height / 2, height - 1].contains(&i) { continue; } @@ -1460,7 +1460,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1513,7 +1513,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let options = Options { small_object_range_limit: 500, @@ -1674,7 +1674,7 @@ mod test { let mut app = App::<_, Error>::with_state(RwLock::new(data_source)); app.register_module("availability", api).unwrap(); - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let _server = BackgroundTask::spawn( "server", app.serve(format!("0.0.0.0:{port}"), MockBase::instance()), @@ -1713,7 +1713,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1803,7 +1803,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1843,7 +1843,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", diff --git a/hotshot-query-service/src/data_source/storage/sql.rs b/hotshot-query-service/src/data_source/storage/sql.rs index 308c984e258..8c39d9508be 100644 --- a/hotshot-query-service/src/data_source/storage/sql.rs +++ b/hotshot-query-service/src/data_source/storage/sql.rs @@ -1148,8 +1148,8 @@ pub mod testing { time::Duration, }; - use portpicker::pick_unused_port; use refinery::Migration; + use test_utils::reserve_tcp_port; use tokio::{net::TcpStream, time::timeout}; use super::Config; @@ -1207,7 +1207,7 @@ pub mod testing { // "free" port on that system. // We *might* be able to get away with this as any remote docker // host should hopefully be pretty open with it's port space. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let host = docker_hostname.unwrap_or("localhost".to_string()); let mut cmd = Command::new("docker"); diff --git a/hotshot-query-service/src/explorer.rs b/hotshot-query-service/src/explorer.rs index 98b26d4333a..7f92d8cf020 100644 --- a/hotshot-query-service/src/explorer.rs +++ b/hotshot-query-service/src/explorer.rs @@ -398,8 +398,8 @@ mod test { use std::{cmp::min, time::Duration}; use futures::StreamExt; - use portpicker::pick_unused_port; use surf_disco::Client; + use test_utils::reserve_tcp_port; use tide_disco::App; use super::*; @@ -867,7 +867,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "explorer", diff --git a/hotshot-query-service/src/fetching/provider/any.rs b/hotshot-query-service/src/fetching/provider/any.rs index 699e744e43c..a3552f8e358 100644 --- a/hotshot-query-service/src/fetching/provider/any.rs +++ b/hotshot-query-service/src/fetching/provider/any.rs @@ -208,7 +208,7 @@ where #[cfg(all(test, not(target_os = "windows")))] mod test { use futures::stream::StreamExt; - use portpicker::pick_unused_port; + use test_utils::reserve_tcp_port; use tide_disco::App; use vbs::version::StaticVersionType; @@ -234,7 +234,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", diff --git a/hotshot-query-service/src/fetching/provider/query_service.rs b/hotshot-query-service/src/fetching/provider/query_service.rs index 4521d194238..115749c11ab 100644 --- a/hotshot-query-service/src/fetching/provider/query_service.rs +++ b/hotshot-query-service/src/fetching/provider/query_service.rs @@ -548,8 +548,8 @@ mod test { use generic_array::GenericArray; use hotshot_example_types::node_types::{EpochsTestVersions, TestVersions}; use hotshot_types::traits::node_implementation::Versions; - use portpicker::pick_unused_port; use rand::RngCore; + use test_utils::reserve_tcp_port; use tide_disco::{error::ServerError, App}; use vbs::version::StaticVersion; @@ -615,7 +615,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -845,7 +845,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1074,7 +1074,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1135,7 +1135,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1200,7 +1200,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1262,7 +1262,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1321,7 +1321,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1400,7 +1400,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1518,7 +1518,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_fetch_from_malicious_server() { - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let _server = BackgroundTask::spawn("malicious server", malicious_server(port)); let provider = QueryServiceProvider::new( @@ -1548,7 +1548,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1719,7 +1719,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1825,7 +1825,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1924,7 +1924,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -1991,7 +1991,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -2056,7 +2056,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -2139,7 +2139,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -2244,7 +2244,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -2318,7 +2318,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -2397,7 +2397,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start a web server that the non-consensus node can use to fetch blocks. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "availability", @@ -2618,7 +2618,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_fallback_deserialization_for_fetch_requests_v0() { - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); // This run will call v0 availalbilty api for fetch requests. // The fetch initially attempts deserialization with new types, @@ -2630,7 +2630,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_fallback_deserialization_for_fetch_requests_v1() { - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); // Fetch from the v1 availability API using MockVersions. // this one fetches from the v1 provider. @@ -2640,7 +2640,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_fallback_deserialization_for_fetch_requests_pos() { - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); // Fetch Proof of Stake (PoS) data using the v1 availability API // with proof of stake version @@ -2655,7 +2655,7 @@ mod test { let mut network = MockNetwork::::init().await; - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( diff --git a/hotshot-query-service/src/lib.rs b/hotshot-query-service/src/lib.rs index 3b1a281552d..42c9b32ea31 100644 --- a/hotshot-query-service/src/lib.rs +++ b/hotshot-query-service/src/lib.rs @@ -598,9 +598,9 @@ mod test { use atomic_store::{load_store::BincodeLoadStore, AtomicStore, AtomicStoreLoader, RollingLog}; use futures::future::FutureExt; use hotshot_types::{data::VidShare, simple_certificate::QuorumCertificate2}; - use portpicker::pick_unused_port; use surf_disco::Client; use tempfile::TempDir; + use test_utils::reserve_tcp_port; use testing::mocks::MockBase; use tide_disco::App; use toml::toml; @@ -936,7 +936,7 @@ mod test { }) .unwrap(); - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let _server = BackgroundTask::spawn( "server", app.serve(format!("0.0.0.0:{port}"), MockBase::instance()), diff --git a/hotshot-query-service/src/node.rs b/hotshot-query-service/src/node.rs index 070aa80e5ca..862b7fc3660 100644 --- a/hotshot-query-service/src/node.rs +++ b/hotshot-query-service/src/node.rs @@ -230,9 +230,9 @@ mod test { EncodeBytes, }, }; - use portpicker::pick_unused_port; use surf_disco::Client; use tempfile::TempDir; + use test_utils::reserve_tcp_port; use tide_disco::{App, Error as _}; use tokio::time::sleep; use toml::toml; @@ -258,7 +258,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "node", @@ -436,7 +436,7 @@ mod test { network.start().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "node", @@ -638,7 +638,7 @@ mod test { let mut app = App::<_, Error>::with_state(RwLock::new(data_source)); app.register_module("node", api).unwrap(); - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let _server = BackgroundTask::spawn( "server", app.serve(format!("0.0.0.0:{port}"), MockBase::instance()), diff --git a/hotshot-query-service/src/status.rs b/hotshot-query-service/src/status.rs index a9aa2bd0435..3162854afa7 100644 --- a/hotshot-query-service/src/status.rs +++ b/hotshot-query-service/src/status.rs @@ -112,10 +112,10 @@ mod test { use async_lock::RwLock; use futures::FutureExt; - use portpicker::pick_unused_port; use reqwest::redirect::Policy; use surf_disco::Client; use tempfile::TempDir; + use test_utils::reserve_tcp_port; use tide_disco::{App, Url}; use toml::toml; @@ -137,7 +137,7 @@ mod test { let mut network = MockNetwork::::init().await; // Start the web server. - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source())); app.register_module( "status", @@ -251,7 +251,7 @@ mod test { let mut app = App::<_, Error>::with_state(RwLock::new(data_source)); app.register_module("status", api).unwrap(); - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().unwrap(); let _server = BackgroundTask::spawn( "server", app.serve(format!("0.0.0.0:{port}"), MockBase::instance()), diff --git a/hotshot-query-service/src/testing/consensus.rs b/hotshot-query-service/src/testing/consensus.rs index 647c12a8fc2..b24a9f63319 100644 --- a/hotshot-query-service/src/testing/consensus.rs +++ b/hotshot-query-service/src/testing/consensus.rs @@ -42,6 +42,7 @@ use hotshot_types::{ }, HotShotConfig, PeerConfig, }; +use test_utils::reserve_tcp_port; use tokio::{ runtime::Handle, task::{block_in_place, yield_now}, @@ -111,7 +112,7 @@ impl MockNetwork { .collect::>(); // Pick a random, unused port for the builder server - let builder_port = portpicker::pick_unused_port().expect("No ports available"); + let builder_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); // Create the bind URL from the random port let builder_url = diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index 450d9ecb4b1..767397bc900 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -47,10 +47,10 @@ rand = { workspace = true, optional = true } # Add test dependencies when building tests. light-client = { path = ".", features = ["testing"] } -portpicker = { workspace = true } pretty_assertions = { workspace = true } sequencer = { path = "../sequencer", default-features = false, features = ["testing"] } test-log = { workspace = true } +test-utils = { workspace = true } [lints] workspace = true diff --git a/light-client/src/client.rs b/light-client/src/client.rs index e4a275c33dd..8f235166f5f 100644 --- a/light-client/src/client.rs +++ b/light-client/src/client.rs @@ -203,7 +203,6 @@ mod test { availability::{BlockQueryData, LeafQueryData}, Resolvable, }; - use portpicker::pick_unused_port; use pretty_assertions::assert_eq; use rand::RngCore; use sequencer::{ @@ -215,6 +214,7 @@ mod test { }, testing::{wait_for_decide_on_handle, TestConfigBuilder}, }; + use test_utils; use tokio::time::sleep; use super::*; @@ -226,7 +226,8 @@ mod test { #[tokio::test] #[test_log::test] async fn test_block_height() { - let port = pick_unused_port().expect("No ports free"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default().build(); @@ -268,7 +269,8 @@ mod test { #[tokio::test] #[test_log::test] async fn test_leaf_proof() { - let port = pick_unused_port().expect("No ports free"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default().build(); @@ -352,7 +354,8 @@ mod test { #[tokio::test] #[test_log::test] async fn test_header_proof() { - let port = pick_unused_port().expect("No ports free"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default().build(); @@ -437,7 +440,8 @@ mod test { #[tokio::test] #[test_log::test] async fn test_payload_proof() { - let port = pick_unused_port().expect("No ports free"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default().build(); @@ -486,7 +490,8 @@ mod test { #[tokio::test] #[test_log::test] async fn test_namespace_proof() { - let port = pick_unused_port().expect("No ports free"); + let port = + test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default().build(); diff --git a/sequencer/Cargo.toml b/sequencer/Cargo.toml index b1ebbf2e054..daa8e3badb0 100644 --- a/sequencer/Cargo.toml +++ b/sequencer/Cargo.toml @@ -85,7 +85,6 @@ libp2p = { workspace = true } moka = { workspace = true } num_enum = "0.7" parking_lot = "0.12" -portpicker = { workspace = true } priority-queue = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } @@ -112,6 +111,7 @@ strum = { workspace = true } surf-disco = { workspace = true } tagged-base64 = { workspace = true } tempfile = { workspace = true } +test-utils = { workspace = true } thiserror = { workspace = true } tide-disco = { workspace = true } time = { workspace = true } diff --git a/sequencer/src/api.rs b/sequencer/src/api.rs index 1e1f85917d3..3f90bf8f78c 100644 --- a/sequencer/src/api.rs +++ b/sequencer/src/api.rs @@ -1363,10 +1363,10 @@ pub mod test_helpers { }; use itertools::izip; use jf_merkle_tree_compat::{MerkleCommitment, MerkleTreeScheme}; - use portpicker::pick_unused_port; use staking_cli::demo::{DelegationConfig, StakingTransactions}; use surf_disco::Client; use tempfile::TempDir; + use test_utils::reserve_tcp_port; use tide_disco::{error::ServerError, Api, App, Error, StatusCode}; use tokio::{spawn, task::JoinHandle, time::sleep}; use url::Url; @@ -1795,7 +1795,7 @@ pub mod test_helpers { /// to test a different initialization path) but should not remove or modify the existing /// functionality (e.g. removing the status module or changing the port). pub async fn status_test_helper(opt: impl FnOnce(Options) -> Options) { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); let client: Client> = Client::new(url); @@ -1842,7 +1842,7 @@ pub mod test_helpers { pub async fn submit_test_helper(opt: impl FnOnce(Options) -> Options) { let txn = Transaction::new(NamespaceId::from(1_u32), vec![1, 2, 3, 4]); - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); let client: Client> = Client::new(url); @@ -1873,7 +1873,7 @@ pub mod test_helpers { /// Test the state signature API. pub async fn state_signature_test_helper(opt: impl FnOnce(Options) -> Options) { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); @@ -1913,7 +1913,7 @@ pub mod test_helpers { /// to test a different initialization path) but should not remove or modify the existing /// functionality (e.g. removing the catchup module or changing the port). pub async fn catchup_test_helper(opt: impl FnOnce(Options) -> Options) { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); let client: Client> = Client::new(url); @@ -2047,7 +2047,7 @@ pub mod test_helpers { app.register_module::<_, _>("catchup", api).unwrap(); - let port = pick_unused_port().expect("no free port"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = Url::parse(&format!("http://localhost:{port}")).unwrap(); let handle = spawn({ @@ -2088,12 +2088,12 @@ mod api_tests { utils::EpochTransitionIndicator, vid::avidm::{init_avidm_param, AvidMScheme}, }; - use portpicker::pick_unused_port; use surf_disco::Client; use test_helpers::{ catchup_test_helper, state_signature_test_helper, status_test_helper, submit_test_helper, TestNetwork, TestNetworkConfigBuilder, }; + use test_utils::reserve_tcp_port; use tide_disco::error::ServerError; use vbs::version::StaticVersion; @@ -2145,7 +2145,7 @@ mod api_tests { let txn = Transaction::new(ns_id, vec![1, 2, 3, 4]); // Start query service. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let storage = D::create_storage().await; let network_config = TestConfigBuilder::default().build(); let config = TestNetworkConfigBuilder::default() @@ -2690,7 +2690,6 @@ mod test { prelude::{MerkleProof, Sha3Node}, MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme, }; - use portpicker::pick_unused_port; use pretty_assertions::assert_matches; use rand::seq::SliceRandom; use rstest::rstest; @@ -2703,6 +2702,7 @@ mod test { catchup_test_helper, state_signature_test_helper, status_test_helper, submit_test_helper, TestNetwork, TestNetworkConfigBuilder, }; + use test_utils::reserve_tcp_port; use tide_disco::{ app::AppHealth, error::ServerError, healthcheck::HealthStatus, Error, StatusCode, Url, }; @@ -2754,7 +2754,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_healthcheck() { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); let client: Client> = Client::new(url); let options = Options::with_port(port); @@ -2792,7 +2792,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_leaf_only_data_source() { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let storage = SqlDataSource::create_storage().await; let options = @@ -2879,7 +2879,7 @@ mod test { async fn run_catchup_test(url_suffix: &str) { // Start a sequencer network, using the query service for catchup. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 5; let url: url::Url = format!("http://localhost:{port}{url_suffix}") @@ -2995,7 +2995,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_catchup_no_state_peers() { // Start a sequencer network, using the query service for catchup. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 5; let config = TestNetworkConfigBuilder::::with_num_nodes() .api_config(Options::with_port(port)) @@ -3081,7 +3081,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_catchup_epochs_no_state_peers() { // Start a sequencer network, using the query service for catchup. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const EPOCH_HEIGHT: u64 = 5; let network_config = TestConfigBuilder::default() .epoch_height(EPOCH_HEIGHT) @@ -3177,7 +3177,7 @@ mod test { // Both chain config commitments will match, so the ValidatedState should have the // full chain config after a non-genesis block is decided. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let chain_config: ChainConfig = ChainConfig::default(); @@ -3230,7 +3230,7 @@ mod test { // However, for this test to work, at least one node should have a full chain config // to allow other nodes to catch up. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let cf = ChainConfig { max_block_size: 300.into(), @@ -3307,7 +3307,7 @@ mod test { // Number of nodes running in the test network. const NUM_NODES: usize = 5; let upgrade_version = ::Upgrade::VERSION; - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let test_config = TestConfigBuilder::default() .epoch_height(200) @@ -3411,7 +3411,7 @@ mod test { .collect::>() .try_into() .unwrap(); - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let config = TestNetworkConfigBuilder::default() .api_config(SqlDataSource::options( &storage[0], @@ -3471,7 +3471,7 @@ mod test { drop(network); // Start up again, resuming from the last decided leaf. - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let config = TestNetworkConfigBuilder::default() .api_config(SqlDataSource::options( @@ -3529,7 +3529,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_fetch_config() { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: surf_disco::Url = format!("http://localhost:{port}").parse().unwrap(); let client: Client> = Client::new(url.clone()); @@ -3570,7 +3570,8 @@ mod test { } async fn run_hotshot_event_streaming_test(url_suffix: &str) { - let query_service_port = pick_unused_port().expect("No ports free for query service"); + let query_service_port = + reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{query_service_port}{url_suffix}") .parse() @@ -3634,7 +3635,8 @@ mod test { .epoch_height(epoch_height) .build(); - let query_service_port = pick_unused_port().expect("No ports free for query service"); + let query_service_port = + reserve_tcp_port().expect("OS should have ephemeral ports available"); let hotshot_url = format!("http://localhost:{query_service_port}") .parse() @@ -3716,7 +3718,7 @@ mod test { .epoch_height(epoch_height) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 1; // Initialize nodes. @@ -3813,7 +3815,7 @@ mod test { .epoch_height(epoch_height) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 5; // Initialize nodes. @@ -3934,7 +3936,7 @@ mod test { .epoch_height(epoch_height) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 5; // Initialize nodes. @@ -4044,7 +4046,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 7; @@ -4271,7 +4273,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 5; @@ -4533,7 +4535,7 @@ mod test { .epoch_height(epoch_height) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 2; // Initialize nodes. @@ -4604,7 +4606,7 @@ mod test { const EPOCH_HEIGHT: u64 = 10; const NUM_NODES: usize = 6; - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let network_config = TestConfigBuilder::default() .epoch_height(EPOCH_HEIGHT) @@ -4736,7 +4738,7 @@ mod test { const EPOCH_HEIGHT: u64 = 10; const NUM_NODES: usize = 6; - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let network_config = TestConfigBuilder::default() .epoch_height(EPOCH_HEIGHT) @@ -4896,7 +4898,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("API PORT = {api_port}"); const NUM_NODES: usize = 5; @@ -4941,7 +4943,7 @@ mod test { network.peers.remove(0); let node_0_storage = &storage[1]; let node_0_persistence = persistence[1].clone(); - let node_0_port = pick_unused_port().expect("No ports free for query service"); + let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("node_0_port {node_0_port}"); // enable query module with api peers let opt = Options::with_port(node_0_port).query_sql( @@ -5122,7 +5124,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("API PORT = {api_port}"); const NUM_NODES: usize = 5; @@ -5166,7 +5168,7 @@ mod test { let node_0_storage = &storage[1]; let node_0_persistence = persistence[1].clone(); - let node_0_port = pick_unused_port().expect("No ports free for query service"); + let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("node_0_port {node_0_port}"); let opt = Options::with_port(node_0_port).query_sql( Query { @@ -5431,7 +5433,7 @@ mod test { .epoch_height(epoch_height) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 1; // Initialize nodes. @@ -5499,7 +5501,7 @@ mod test { .epoch_height(epoch_height) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 1; // Initialize nodes. @@ -5678,7 +5680,7 @@ mod test { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_tx_metadata() { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); let client: Client> = Client::new(url); @@ -5760,7 +5762,7 @@ mod test { async fn test_aggregator_namespace_endpoints() { let mut rng = thread_rng(); - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); tracing::info!("Sequencer URL = {url}"); @@ -5939,7 +5941,7 @@ mod test { let mut rng = thread_rng(); - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url = format!("http://localhost:{port}").parse().unwrap(); tracing::info!("Sequencer URL = {url}"); @@ -6071,7 +6073,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("API PORT = {api_port}"); const NUM_NODES: usize = 5; @@ -6148,7 +6150,7 @@ mod test { .epoch_height(TEST_EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("API PORT = {api_port}"); const NUM_NODES: usize = 2; @@ -6276,7 +6278,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("API PORT = {api_port}"); const NUM_NODES: usize = 5; @@ -6328,7 +6330,7 @@ mod test { let new_persistence: persistence::sql::Options = ::persistence_options(&new_storage); - let node_0_port = pick_unused_port().expect("No ports free for query service"); + let node_0_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); tracing::info!("node_0_port {node_0_port}"); let opt = Options::with_port(node_0_port).query_sql( Query { @@ -6395,7 +6397,7 @@ mod test { // Use version that supports epochs (V3 or V4) let versions = PosVersionV4::new(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); // Initialize storage for nodes let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await; @@ -6551,7 +6553,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); println!("API PORT = {api_port}"); let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await; @@ -6689,7 +6691,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); const NUM_NODES: usize = 5; @@ -6778,7 +6780,7 @@ mod test { .epoch_height(EPOCH_HEIGHT) .build(); - let api_port = pick_unused_port().expect("No ports free for query service"); + let api_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); println!("API PORT = {api_port}"); let storage = join_all((0..NUM_NODES).map(|_| SqlDataSource::create_storage())).await; @@ -7117,7 +7119,7 @@ mod test { // Number of nodes running in the test network. const NUM_NODES: usize = 5; - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default().build(); @@ -7324,7 +7326,7 @@ mod test { const EPOCH_HEIGHT: u64 = 200; let upgrade_version = EpochVersion::version(); - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let url: Url = format!("http://localhost:{port}").parse().unwrap(); let test_config = TestConfigBuilder::default() diff --git a/sequencer/src/bin/dev-cdn.rs b/sequencer/src/bin/dev-cdn.rs index bc58ab39ef8..941112e092e 100644 --- a/sequencer/src/bin/dev-cdn.rs +++ b/sequencer/src/bin/dev-cdn.rs @@ -12,9 +12,9 @@ use cdn_marshal::{Config as MarshalConfig, Marshal}; use clap::Parser; use espresso_types::SeqTypes; use hotshot_types::traits::{node_implementation::NodeType, signature_key::SignatureKey}; -use portpicker::pick_unused_port; use rand::{rngs::StdRng, RngCore, SeedableRng}; use sequencer::network::cdn::{TestingDef, WrappedSignatureKey}; +use test_utils::reserve_tcp_port; use tokio::spawn; #[derive(Parser, Debug)] @@ -55,8 +55,8 @@ async fn main() -> Result<()> { .into_owned(); // Acquire unused ports for the broker to use - let broker_public_port = pick_unused_port().expect("failed to find free port for broker"); - let broker_private_port = pick_unused_port().expect("failed to find free port for broker"); + let broker_public_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); + let broker_private_port = reserve_tcp_port().expect("OS should have ephemeral ports available"); // Configure the broker let broker_config: BrokerConfig> = BrokerConfig { diff --git a/sequencer/src/lib.rs b/sequencer/src/lib.rs index 9b4a74f9401..2a0de3a29e6 100644 --- a/sequencer/src/lib.rs +++ b/sequencer/src/lib.rs @@ -772,10 +772,10 @@ pub mod testing { }, HotShotConfig, PeerConfig, }; - use portpicker::pick_unused_port; use rand::SeedableRng as _; use rand_chacha::ChaCha20Rng; use staking_cli::demo::{DelegationConfig, StakingTransactions}; + use test_utils::reserve_tcp_port; use tokio::spawn; use vbs::version::Version; @@ -822,7 +822,10 @@ pub mod testing { max_block_size: Option, ) -> (Box>, Url) { let builder_key_pair = TestConfig::<0>::builder_key(); - let port = port.unwrap_or_else(|| pick_unused_port().expect("No ports available")); + let port = match port { + Some(p) => p, + None => reserve_tcp_port().expect("OS should have ephemeral ports available"), + }; // This should never fail. let url: Url = format!("http://localhost:{port}") @@ -851,14 +854,15 @@ pub mod testing { .into_app() .expect("Failed to create builder tide-disco app"); - spawn( + spawn(async move { app.serve( format!("http://0.0.0.0:{port}") .parse::() .expect("Failed to parse builder listener"), EpochVersion::instance(), - ), - ); + ) + .await + }); // Pass on the builder task to be injected in the testing harness (Box::new(LegacyBuilderImplementation { global_state }), url) @@ -867,7 +871,10 @@ pub mod testing { pub async fn run_test_builder( port: Option, ) -> (Box>, Url) { - let port = port.unwrap_or_else(|| pick_unused_port().expect("No ports available")); + let port = match port { + Some(p) => p, + None => reserve_tcp_port().expect("OS should have ephemeral ports available"), + }; // This should never fail. let url: Url = format!("http://localhost:{port}") @@ -875,18 +882,17 @@ pub mod testing { .expect("Failed to parse builder URL"); tracing::info!("Starting test builder on {url}"); - ( - >::start( - NUM_NODES, - format!("http://0.0.0.0:{port}") - .parse() - .expect("Failed to parse builder listener"), - (), - HashMap::new(), - ) - .await, - url, + let task = >::start( + NUM_NODES, + format!("http://0.0.0.0:{port}") + .parse() + .expect("Failed to parse builder listener"), + (), + HashMap::new(), ) + .await; + + (task, url) } pub struct TestConfigBuilder { @@ -1104,6 +1110,8 @@ pub mod testing { let master_map = MasterMap::new(); + let builder_port = reserve_tcp_port().unwrap(); + let config: HotShotConfig = HotShotConfig { fixed_leader_for_gpuvid: 0, num_nodes_with_stake: num_nodes.try_into().unwrap(), @@ -1115,11 +1123,9 @@ pub mod testing { da_staked_committee_size: num_nodes, view_sync_timeout: Duration::from_secs(1), data_request_delay: Duration::from_secs(1), - builder_urls: vec1::vec1![Url::parse(&format!( - "http://127.0.0.1:{}", - pick_unused_port().unwrap() - )) - .unwrap()], + builder_urls: vec1::vec1![ + Url::parse(&format!("http://127.0.0.1:{builder_port}")).unwrap() + ], builder_timeout: Duration::from_secs(1), start_threshold: ( known_nodes_with_stake.clone().len() as u64, diff --git a/sequencer/src/persistence.rs b/sequencer/src/persistence.rs index 1ec22e28a64..5af61f66a99 100644 --- a/sequencer/src/persistence.rs +++ b/sequencer/src/persistence.rs @@ -154,9 +154,9 @@ mod tests { vote::HasViewNumber, }; use indexmap::IndexMap; - use portpicker::pick_unused_port; use staking_cli::demo::{DelegationConfig, StakingTransactions}; use surf_disco::Client; + use test_utils::reserve_tcp_port; use tide_disco::error::ServerError; use tokio::{spawn, time::sleep}; use vbs::version::{StaticVersion, StaticVersionType, Version}; @@ -1395,7 +1395,8 @@ mod tests { let anvil_provider = network_config.anvil().unwrap(); - let query_service_port = pick_unused_port().expect("No ports free for query service"); + let query_service_port = + reserve_tcp_port().expect("OS should have ephemeral ports available"); let query_api_options = Options::with_port(query_service_port); const NODE_COUNT: usize = 2; diff --git a/sequencer/src/run.rs b/sequencer/src/run.rs index 2d274054438..18818be2678 100644 --- a/sequencer/src/run.rs +++ b/sequencer/src/run.rs @@ -340,9 +340,9 @@ mod test { use espresso_types::{MockSequencerVersions, PubKey}; use hotshot_types::{light_client::StateKeyPair, traits::signature_key::SignatureKey}; - use portpicker::pick_unused_port; use surf_disco::{error::ClientError, Client, Url}; use tempfile::TempDir; + use test_utils::reserve_tcp_port; use tokio::spawn; use vbs::version::Version; @@ -359,7 +359,7 @@ mod test { let (pub_key, priv_key) = PubKey::generated_from_seed_indexed([0; 32], 0); let state_key = StateKeyPair::generate_from_seed_indexed([0; 32], 0); - let port = pick_unused_port().unwrap(); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let tmp = TempDir::new().unwrap(); let genesis_file = tmp.path().join("genesis.toml"); diff --git a/slow-tests/Cargo.toml b/slow-tests/Cargo.toml index 8ac44117834..bddac98b7f3 100644 --- a/slow-tests/Cargo.toml +++ b/slow-tests/Cargo.toml @@ -29,7 +29,6 @@ hotshot-testing = { workspace = true } hotshot-types = { workspace = true } itertools = { workspace = true } jf-merkle-tree-compat = { workspace = true } -portpicker = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } rstest = { workspace = true } @@ -39,6 +38,7 @@ staking-cli = { workspace = true } surf-disco = { workspace = true } tempfile = { workspace = true } test-log = { workspace = true } +test-utils = { workspace = true } tide-disco = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/slow-tests/tests/restart_tests.rs b/slow-tests/tests/restart_tests.rs index c72716697dd..ff7eca1bf31 100644 --- a/slow-tests/tests/restart_tests.rs +++ b/slow-tests/tests/restart_tests.rs @@ -1,9 +1,4 @@ -use std::{ - collections::{BTreeMap, HashSet}, - path::Path, - sync::Arc, - time::Duration, -}; +use std::{collections::BTreeMap, path::Path, sync::Arc, time::Duration}; use alloy::{ network::EthereumWallet, @@ -56,7 +51,6 @@ use hotshot_types::{ PeerConfig, }; use itertools::Itertools; -use portpicker::pick_unused_port; use sequencer::{ api::{ self, data_source::testing::TestableSequencerDataSource, options::Query, @@ -76,6 +70,7 @@ use sequencer::{ use staking_cli::demo::{DelegationConfig, StakingTransactions}; use surf_disco::{error::ClientError, Url}; use tempfile::TempDir; +use test_utils::reserve_tcp_port; use tokio::{ task::{spawn, JoinHandle}, time::{sleep, timeout}, @@ -242,14 +237,14 @@ struct NodeParams { } impl NodeParams { - fn new(ports: &mut PortPicker, i: u64, is_da: bool) -> Self { - Self { - api_port: ports.pick(), - libp2p_port: ports.pick(), + fn new(i: u64, is_da: bool) -> anyhow::Result { + Ok(Self { + api_port: reserve_tcp_port()?, + libp2p_port: reserve_tcp_port()?, staking_key: PubKey::generated_from_seed_indexed([0; 32], i).1, state_key: StateKeyPair::generate_from_seed_indexed([0; 32], i), is_da, - } + }) } } @@ -647,8 +642,6 @@ impl Drop for TestNetwork { impl TestNetwork { async fn new(da_nodes: usize, regular_nodes: usize, cdn: bool) -> Self { - let mut ports = PortPicker::default(); - let tmp = TempDir::new().unwrap(); let genesis_file_path = tmp.path().join("genesis.toml"); @@ -678,11 +671,12 @@ impl TestNetwork { }; let node_params = (0..da_nodes + regular_nodes) - .map(|i| NodeParams::new(&mut ports, i as u64, i < da_nodes)) - .collect::>(); + .map(|i| NodeParams::new(i as u64, i < da_nodes)) + .collect::, _>>() + .unwrap(); - let orchestrator_port = ports.pick(); - let builder_port = ports.pick(); + let orchestrator_port = reserve_tcp_port().unwrap(); + let builder_port = reserve_tcp_port().unwrap(); let orchestrator_task = Some(start_orchestrator( orchestrator_port, &node_params, @@ -690,9 +684,9 @@ impl TestNetwork { )); let cdn_dir = tmp.path().join("cdn"); - let cdn_port = ports.pick(); + let cdn_port = reserve_tcp_port().unwrap(); let broker_task = if cdn { - Some(start_broker(&mut ports, &cdn_dir).await) + Some(start_broker(&cdn_dir).await) } else { None }; @@ -702,7 +696,7 @@ impl TestNetwork { None }; - let anvil_port = ports.pick(); + let anvil_port = reserve_tcp_port().unwrap(); let anvil = Anvil::new() .args(["--slots-in-an-epoch", "1"]) .port(anvil_port) @@ -1176,10 +1170,10 @@ fn start_orchestrator(port: u16, nodes: &[NodeParams], builder_port: u16) -> Joi }) } -async fn start_broker(ports: &mut PortPicker, dir: &Path) -> JoinHandle<()> { +async fn start_broker(dir: &Path) -> JoinHandle<()> { let (public_key, private_key) = PubKey::generated_from_seed_indexed([0; 32], 1337); - let public_port = ports.pick(); - let private_port = ports.pick(); + let public_port = reserve_tcp_port().unwrap(); + let private_port = reserve_tcp_port().unwrap(); let broker_config: BrokerConfig> = BrokerConfig { public_advertise_endpoint: format!("127.0.0.1:{public_port}"), public_bind_endpoint: format!("127.0.0.1:{public_port}"), @@ -1232,38 +1226,6 @@ async fn start_marshal(dir: &Path, port: u16) -> JoinHandle<()> { }) } -/// Allocator for unused ports. -/// -/// While portpicker is able to pick ports that are currently unused by the OS, its allocation is -/// random, and it may return the same port twice if that port is still unused by the OS the second -/// time. This test suite allocates many ports, and it is often convenient to allocate many in a -/// batch, before starting the services that listen on them, so that the first port selected is not -/// "in use" when we select later ports in the same batch. -/// -/// This object keeps track not only of ports in use by the OS, but also ports it has already given -/// out, for which there may not yet be any listener. Thus, it is safe to use this to allocate many -/// ports at once, without a collision. -#[derive(Debug, Default)] -struct PortPicker { - allocated: HashSet, -} - -impl PortPicker { - fn pick(&mut self) -> u16 { - loop { - let port = pick_unused_port().unwrap(); - if self.allocated.insert(port) { - break port; - } - tracing::warn!( - port, - "picked port which is already allocated, will try again. If this error persists, \ - try reducing the number of ports being used." - ); - } - } -} - fn builder_key_pair() -> EthKeyPair { use hotshot_types::traits::signature_key::BuilderSignatureKey; FeeAccount::generated_from_seed_indexed([1; 32], 0).1 diff --git a/slow-tests/tests/state.rs b/slow-tests/tests/state.rs index 57371f13607..80180e6bb45 100644 --- a/slow-tests/tests/state.rs +++ b/slow-tests/tests/state.rs @@ -24,7 +24,6 @@ use jf_merkle_tree_compat::{ prelude::{MerkleProof, Sha3Node}, LookupResult, MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme, }; -use portpicker::pick_unused_port; use sequencer::{ api::{ data_source::testing::TestableSequencerDataSource, @@ -36,6 +35,7 @@ use sequencer::{ SequencerApiVersion, }; use surf_disco::Client; +use test_utils::reserve_tcp_port; use tide_disco::error::ServerError; use tokio::time::sleep; @@ -44,7 +44,7 @@ type MockSequencerVersions = #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn slow_test_merklized_state_api() { - let port = pick_unused_port().expect("No ports free"); + let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); let storage = SqlDataSource::create_storage().await; diff --git a/staking-cli/Cargo.toml b/staking-cli/Cargo.toml index 7193a7dfad5..15b5ef13fa1 100644 --- a/staking-cli/Cargo.toml +++ b/staking-cli/Cargo.toml @@ -27,7 +27,6 @@ hotshot-state-prover = { workspace = true } hotshot-types = { workspace = true } jf-merkle-tree-compat = { workspace = true } jf-signature = { workspace = true, features = ["bls", "schnorr"] } -portpicker = { workspace = true } prometheus-parse = "0.2" rand = { workspace = true } rand_chacha = { workspace = true } @@ -55,6 +54,7 @@ pretty_assertions = { workspace = true } staking-cli = { path = ".", features = ["testing"] } tempfile = { workspace = true } test-log = { workspace = true } +test-utils = { workspace = true } [lints] workspace = true diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml new file mode 100644 index 00000000000..ad31bf2118f --- /dev/null +++ b/test-utils/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-utils" +version.workspace = true +authors.workspace = true +edition.workspace = true +description = "Test utilities for Espresso Network" +license = "MIT" + +[dependencies] + +[lints] +workspace = true diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs new file mode 100644 index 00000000000..0de39ce578c --- /dev/null +++ b/test-utils/src/lib.rs @@ -0,0 +1,126 @@ +//! Test utilities for Espresso Network +//! +//! This crate provides self-contained test utilities with no workspace internal dependencies. +//! Use `test-utils` for utilities that only need standard library functionality. +//! Use crate-specific `utils` modules for utilities that need workspace dependencies. +//! +//! # Port Allocation +//! +//! The `reserve_tcp_port()` function uses the TIME_WAIT trick to provide race-free port +//! allocation, returning a port protected from ephemeral allocation for ~60s. + +use std::net::{TcpListener, TcpStream}; + +/// Reserve a TCP port using the TIME_WAIT trick. +/// +/// This function allocates an ephemeral port and forces it into TCP TIME_WAIT state +/// by completing a TCP handshake. The port is then: +/// - Protected from OS ephemeral allocation for ~60 seconds (TIME_WAIT duration) +/// - Still available for explicit binds (TIME_WAIT prevents collision with ephemeral +/// allocation, not intentional rebinding) +/// +/// This pattern is based on Yelp's ephemeral-port-reserve approach and is useful +/// when you need to know a port number before starting a service, but the service +/// needs to bind to the port itself (not use a pre-bound listener). +/// +/// # Errors +/// +/// Returns an error if unable to bind, connect, or accept on 127.0.0.1. +/// +/// # Example +/// +/// ``` +/// use test_utils::reserve_tcp_port; +/// +/// let port = reserve_tcp_port().expect("OS should have ephemeral ports available"); +/// // Port is now in TIME_WAIT state - protected from ephemeral allocation +/// // Services can still bind to it explicitly +/// ``` +pub fn reserve_tcp_port() -> std::io::Result { + let server = TcpListener::bind("127.0.0.1:0")?; + let addr = server.local_addr()?; + + // Force TIME_WAIT by completing TCP handshake + let _client = TcpStream::connect(addr)?; + let (_accepted, _) = server.accept()?; + // All sockets drop here - port enters TIME_WAIT + + Ok(addr.port()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_reserve_tcp_port_returns_nonzero() { + let port = reserve_tcp_port().unwrap(); + assert_ne!(port, 0); + } + + #[test] + fn test_reserve_tcp_port_unique() { + let ports: Vec = (0..10).map(|_| reserve_tcp_port().unwrap()).collect(); + let unique: std::collections::HashSet<_> = ports.iter().collect(); + assert_eq!(unique.len(), 10, "All ports should be unique"); + } + + #[test] + fn test_reserve_tcp_port_service_can_bind() { + let port = reserve_tcp_port().unwrap(); + + // Service should be able to bind (TIME_WAIT allows explicit binds) + let listener = TcpListener::bind(format!("127.0.0.1:{}", port)) + .expect("Service should bind to TIME_WAIT port"); + + assert_eq!(listener.local_addr().unwrap().port(), port); + } + + #[test] + fn test_reserve_tcp_port_no_ephemeral_collision() { + let reserved_port = reserve_tcp_port().unwrap(); + + // Create many ephemeral connections - none should get our reserved port + let mut ephemeral_ports = Vec::new(); + for _ in 0..100 { + let socket = TcpListener::bind("127.0.0.1:0").unwrap(); + ephemeral_ports.push(socket.local_addr().unwrap().port()); + } + + assert!( + !ephemeral_ports.contains(&reserved_port), + "OS assigned reserved port {} to ephemeral allocation", + reserved_port + ); + } + + #[test] + fn test_reserve_tcp_port_concurrent() { + use std::{ + collections::HashSet, + sync::{Arc, Mutex}, + thread, + }; + + let ports = Arc::new(Mutex::new(HashSet::new())); + let mut handles = vec![]; + + for _ in 0..10 { + let ports = Arc::clone(&ports); + handles.push(thread::spawn(move || { + let port = reserve_tcp_port().unwrap(); + ports.lock().unwrap().insert(port); + })); + } + + for h in handles { + h.join().unwrap(); + } + + assert_eq!( + ports.lock().unwrap().len(), + 10, + "All ports should be unique" + ); + } +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index d4cdbeb85fb..92aa85870d0 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -28,7 +28,6 @@ hotshot-contract-adapter = { path = "../contracts/rust/adapter" } hotshot-state-prover = { path = "../hotshot-state-prover" } hotshot-types = { path = "../crates/hotshot/types" } jf-signature = { workspace = true } -portpicker = { workspace = true } rand = { workspace = true } reqwest = { workspace = true, features = ["json"] } sequencer = { path = "../sequencer", default-features = false, features = ["testing"] } @@ -38,6 +37,7 @@ surf-disco = { workspace = true } tagged-base64 = { workspace = true } tempfile = { workspace = true } test-log = { workspace = true } +test-utils = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/tests/reward_claims_e2e.rs b/tests/reward_claims_e2e.rs index 8f045f261f5..6063cae5a26 100644 --- a/tests/reward_claims_e2e.rs +++ b/tests/reward_claims_e2e.rs @@ -4,7 +4,7 @@ use alloy::{ network::EthereumWallet, node_bindings::Anvil, primitives::U256, - providers::{Provider, ProviderBuilder, WalletProvider}, + providers::{layers::AnvilLayer, Provider, ProviderBuilder, WalletProvider}, rpc::client::RpcClient, }; use espresso_contract_deployer::{build_signer, Contract}; @@ -20,7 +20,6 @@ use hotshot_types::{ stake_table::{one_honest_threshold, HSStakeTable}, utils::epoch_from_block_number, }; -use portpicker::pick_unused_port; use sequencer::{ api::{ data_source::testing::TestableSequencerDataSource, @@ -32,6 +31,7 @@ use sequencer::{ SequencerApiVersion, }; use staking_cli::demo::DelegationConfig; +use test_utils::reserve_tcp_port; use tokio::spawn; use url::Url; use vbs::version::StaticVersionType; @@ -45,15 +45,16 @@ const RETRY_INTERVAL: Duration = Duration::from_secs(2); #[test_log::test(tokio::test)] async fn test_reward_claims_e2e() -> anyhow::Result<()> { + // Use AnvilLayer to keep Anvil alive and prevent port race conditions. // Finalize blocks immediately to ensure we have a finalized stake table on L1 for consensus. - let anvil = Anvil::new().args(["--slots-in-an-epoch", "0"]).spawn(); - let l1_url = anvil.endpoint_url(); + let anvil_layer = AnvilLayer::from(Anvil::new().args(["--slots-in-an-epoch", "0"])); + let l1_url = anvil_layer.endpoint_url(); - let relay_server_port = pick_unused_port().unwrap(); + let relay_server_port = reserve_tcp_port().unwrap(); let relay_server_url: Url = format!("http://localhost:{relay_server_port}") .parse() .unwrap(); - let sequencer_api_port = pick_unused_port().unwrap(); + let sequencer_api_port = reserve_tcp_port().unwrap(); let network_config = TestConfigBuilder::default() .epoch_height(BLOCKS_PER_EPOCH) diff --git a/types/Cargo.toml b/types/Cargo.toml index e65b818f3f1..4ae853c9fbc 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -78,10 +78,10 @@ vid = { workspace = true } [dev-dependencies] espresso-contract-deployer = { path = "../contracts/rust/deployer" } espresso-types = { path = ".", features = [ "testing" ] } -portpicker = { workspace = true } rstest = { workspace = true } rstest_reuse = { workspace = true } test-log = { workspace = true } +test-utils = { workspace = true } [lints] workspace = true diff --git a/types/src/v0/impls/l1.rs b/types/src/v0/impls/l1.rs index 6394ca91fb5..dfa57a1ee06 100644 --- a/types/src/v0/impls/l1.rs +++ b/types/src/v0/impls/l1.rs @@ -1118,7 +1118,6 @@ mod test { providers::layers::AnvilProvider, }; use espresso_contract_deployer::{deploy_fee_contract_proxy, Contracts}; - use portpicker::pick_unused_port; use time::OffsetDateTime; use super::*; @@ -1421,8 +1420,9 @@ mod test { } async fn test_reconnect_update_task_helper(ws: bool) { - let port = pick_unused_port().unwrap(); - let anvil = Arc::new(Anvil::new().block_time(1).port(port).spawn()); + // Use port 0 to let OS assign a port, avoiding race conditions + let anvil = Arc::new(Anvil::new().block_time(1).port(0u16).spawn()); + let port = anvil.port(); let client = new_l1_client(&anvil, ws).await; let initial_state = client.snapshot().await; @@ -1681,7 +1681,8 @@ mod test { // don't require an L1 that is continuously mining blocks. #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_update_loop_initializes_l1_state() { - let anvil = Arc::new(Anvil::new().port(9988u16).spawn()); + // Use port 0 to let OS assign a port, avoiding race conditions + let anvil = Arc::new(Anvil::new().port(0u16).spawn()); let l1_client = new_l1_client(&anvil, true).await; for _try in 0..10 { diff --git a/utils/Cargo.toml b/utils/Cargo.toml index c974924c92d..14333eee4bd 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -18,7 +18,6 @@ committable = { workspace = true } hotshot = { workspace = true } hotshot-example-types = { workspace = true } log-panics = { workspace = true } -portpicker = { workspace = true } serde = { workspace = true } serde_json = "^1.0.113" surf = "2.3.2"