Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable random port on --port 0 #666

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
339 changes: 247 additions & 92 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ parking_lot = "0.12.3"
serial_test = "3.1.1"
hex = "0.4.3"
lazy_static = { version = "1.4.0" }
netstat2 = "0.11.1"

# Benchmarking
criterion = { version = "0.3.4", features = ["async_tokio"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/starknet-devnet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ usc = { workspace = true }
reqwest = { workspace = true }
criterion = { workspace = true }
serial_test = { workspace = true }

netstat2 = { workspace = true }

[[bench]]
name = "mint_bench"
Expand Down
2 changes: 1 addition & 1 deletion crates/starknet-devnet/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub(crate) struct Args {
#[arg(env = "PORT")]
#[arg(value_name = "PORT")]
#[arg(default_value_t = DEVNET_DEFAULT_PORT)]
#[arg(help = "Specify the port to listen at;")]
#[arg(help = "Specify the port to listen at; If 0, acquires a random free port and prints it;")]
port: u16,

// Set start time in seconds
Expand Down
15 changes: 13 additions & 2 deletions crates/starknet-devnet/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::future::IntoFuture;
use std::net::IpAddr;
use std::result::Result::Ok;
use std::time::Duration;

Expand Down Expand Up @@ -240,6 +241,17 @@ pub async fn set_and_log_fork_config(
Ok(())
}

async fn bind_port(
host: IpAddr,
specified_port: u16,
) -> Result<(String, TcpListener), anyhow::Error> {
let binding_address = format!("{host}:{specified_port}");
let listener = TcpListener::bind(binding_address.clone()).await?;

let acquired_port = listener.local_addr()?.port();
Ok((format!("{host}:{acquired_port}"), listener))
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
configure_tracing();
Expand All @@ -261,8 +273,7 @@ async fn main() -> Result<(), anyhow::Error> {
starknet_config.chain_id = json_rpc_client.chain_id().await?.into();
}

let address = format!("{}:{}", server_config.host, server_config.port);
let listener = TcpListener::bind(address.clone()).await?;
let (address, listener) = bind_port(server_config.host, server_config.port).await?;

let starknet = Starknet::new(&starknet_config)?;
let api = Api::new(starknet);
Expand Down
7 changes: 3 additions & 4 deletions crates/starknet-devnet/tests/common/background_anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use rand::Rng;
use reqwest::StatusCode;
use starknet_core::messaging::ethereum::ETH_ACCOUNT_DEFAULT;

use super::constants::HOST;
use super::errors::TestError;

pub struct BackgroundAnvil {
Expand Down Expand Up @@ -50,15 +51,13 @@ impl BackgroundAnvil {
.spawn()
.expect("Could not start background Anvil");

let address = "127.0.0.1";
let anvil_url = format!("http://{address}:{port}");

let anvil_url = format!("http://{HOST}:{port}");
let client = reqwest::Client::new();
let max_retries = 30;
for _ in 0..max_retries {
if let Ok(anvil_block_rsp) = send_dummy_request(&client, &anvil_url).await {
assert_eq!(anvil_block_rsp.status(), StatusCode::OK);
println!("Spawned background anvil at port {port} ({address})");
println!("Spawned background anvil at {anvil_url}");

let (provider, provider_signer) = setup_ethereum_provider(&anvil_url).await?;

Expand Down
97 changes: 55 additions & 42 deletions crates/starknet-devnet/tests/common/background_devnet.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::collections::HashMap;
use std::fmt::LowerHex;
use std::net::TcpListener;
use std::process::{Child, Command, Stdio};
use std::time;

use lazy_static::lazy_static;
use netstat2::{
get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo, TcpSocketInfo,
TcpState,
};
use reqwest::{Client, StatusCode};
use serde_json::json;
use server::rpc_core::error::{ErrorCode, RpcError};
Expand All @@ -24,8 +27,7 @@ use tokio::sync::Mutex;
use url::Url;

use super::constants::{
ACCOUNTS, HEALTHCHECK_PATH, HOST, MAX_PORT, MIN_PORT, PREDEPLOYED_ACCOUNT_INITIAL_BALANCE,
RPC_PATH, SEED,
ACCOUNTS, HEALTHCHECK_PATH, HOST, PREDEPLOYED_ACCOUNT_INITIAL_BALANCE, RPC_PATH, SEED,
};
use super::errors::TestError;
use super::reqwest_client::{PostReqwestSender, ReqwestClient};
Expand All @@ -50,21 +52,30 @@ pub struct BackgroundDevnet {
rpc_url: Url,
}

fn get_free_port() -> Result<u16, TestError> {
for port in MIN_PORT..=MAX_PORT {
if let Ok(listener) = TcpListener::bind(("127.0.0.1", port)) {
return Ok(listener.local_addr().expect("No local addr").port());
}
// otherwise port is occupied
}
Err(TestError::NoFreePorts)
fn is_socket_tcp_listener(info: &ProtocolSocketInfo) -> bool {
matches!(info, ProtocolSocketInfo::Tcp(TcpSocketInfo { state: TcpState::Listen, .. }))
}

/// Returns the ports used by process identified by `pid`.
fn get_ports_by_pid(pid: u32) -> Result<Vec<u16>, anyhow::Error> {
let af_flags = AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
let sockets = get_sockets_info(af_flags, ProtocolFlags::TCP)?;

let ports = sockets
.into_iter()
.filter(|socket| socket.associated_pids.contains(&pid))
.filter(|socket| is_socket_tcp_listener(&socket.protocol_socket_info))
.map(|socket| socket.local_port())
.collect();
Ok(ports)
}

lazy_static! {
static ref DEFAULT_CLI_MAP: HashMap<&'static str, String> = HashMap::from([
("--seed", SEED.to_string()),
("--accounts", ACCOUNTS.to_string()),
("--initial-balance", PREDEPLOYED_ACCOUNT_INITIAL_BALANCE.to_string()),
("--port", 0.to_string()) // random port by default
]);
}

Expand Down Expand Up @@ -114,39 +125,44 @@ impl BackgroundDevnet {
}

pub(crate) async fn spawn_with_additional_args(args: &[&str]) -> Result<Self, TestError> {
// we keep the reference, otherwise the mutex unlocks immediately
let _mutex_guard = BACKGROUND_DEVNET_MUTEX.lock().await;

let free_port = get_free_port().expect("No free ports");

let devnet_url = format!("http://{HOST}:{free_port}");
let devnet_rpc_url = Url::parse(format!("{}{RPC_PATH}", devnet_url.as_str()).as_str())?;
let json_rpc_client = JsonRpcClient::new(HttpTransport::new(devnet_rpc_url.clone()));

let process = Command::new("cargo")
.arg("run")
.arg("--release")
.arg("--")
.arg("--port")
.arg(free_port.to_string())
.args(Self::add_default_args(args))
.stdout(Stdio::piped()) // comment this out for complete devnet stdout
.spawn()
.expect("Could not start background devnet");
.expect("Devnet subprocess should be startable");

let healthcheck_uri = format!("{}{HEALTHCHECK_PATH}", devnet_url.as_str()).to_string();
let reqwest_client = Client::new();

let max_retries = 30;
let max_retries = 60;
for _ in 0..max_retries {
// attempt to get ports used by PID of the spawned subprocess
let port = match get_ports_by_pid(process.id()) {
Ok(ports) => match ports.len() {
0 => continue, // if no ports, wait a bit more
1 => ports[0],
_ => return Err(TestError::TooManyPorts(ports)),
},
Err(e) => return Err(TestError::DevnetNotStartable(e.to_string())),
};

// now we know the port; check if it can be used to poll Devnet's endpoint
let devnet_url = format!("http://{HOST}:{port}");
let devnet_rpc_url = Url::parse(format!("{devnet_url}{RPC_PATH}").as_str())?;

let healthcheck_uri = format!("{devnet_url}{HEALTHCHECK_PATH}").to_string();

if let Ok(alive_resp) = reqwest_client.get(&healthcheck_uri).send().await {
assert_eq!(alive_resp.status(), StatusCode::OK);
println!("Spawned background devnet at port {free_port}");
println!("Spawned background devnet at {devnet_url}");
return Ok(BackgroundDevnet {
reqwest_client: ReqwestClient::new(devnet_url.clone(), reqwest_client),
json_rpc_client,
json_rpc_client: JsonRpcClient::new(HttpTransport::new(devnet_rpc_url.clone())),
process,
port: free_port,
port,
url: devnet_url,
rpc_url: devnet_rpc_url,
});
Expand All @@ -157,28 +173,25 @@ impl BackgroundDevnet {
tokio::time::sleep(time::Duration::from_millis(500)).await;
}

Err(TestError::DevnetNotStartable)
Err(TestError::DevnetNotStartable(
"Before testing, make sure you build Devnet with: `cargo build --release`".into(),
))
}

pub async fn send_custom_rpc(
&self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, RpcError> {
let body_json = if params.is_null() {
json!({
"jsonrpc": "2.0",
"id": 0,
"method": method
})
} else {
json!({
"jsonrpc": "2.0",
"id": 0,
"method": method,
"params": params
})
};
let mut body_json = json!({
"jsonrpc": "2.0",
"id": 0,
"method": method,
});

if !params.is_null() {
body_json["params"] = params;
}

let json_rpc_result: serde_json::Value =
self.reqwest_client().post_json_async(RPC_PATH, body_json).await.map_err(|err| {
Expand Down
4 changes: 1 addition & 3 deletions crates/starknet-devnet/tests/common/constants.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use starknet_core::constants::DEVNET_DEFAULT_INITIAL_BALANCE;
use starknet_rs_core::types::Felt;

pub const HOST: &str = "localhost";
pub const MIN_PORT: u16 = 1025;
pub const MAX_PORT: u16 = 65_535;
pub const HOST: &str = "127.0.0.1";
pub const SEED: usize = 42;
pub const ACCOUNTS: usize = 3;
pub const CHAIN_ID: Felt = starknet_rs_core::chain_id::SEPOLIA;
Expand Down
10 changes: 5 additions & 5 deletions crates/starknet-devnet/tests/common/errors.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TestError {
#[error("No free ports")]
NoFreePorts,

#[error("Could not parse URL")]
UrlParseError(#[from] url::ParseError),

#[error("Invalid URI")]
InvalidUri(#[from] axum::http::uri::InvalidUri),

#[error("Could not start Devnet. Make sure you've built it with: `cargo build --release`")]
DevnetNotStartable,
#[error("Could not start Devnet: {0}")]
DevnetNotStartable(String),

#[error("Too many ports occupied: {0:?}")]
TooManyPorts(Vec<u16>),

#[error("Could not start Anvil")]
AnvilNotStartable,
Expand Down
18 changes: 12 additions & 6 deletions crates/starknet-devnet/tests/general_integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@ mod general_integration_tests {
use crate::common::utils::{to_hex_felt, UniqueAutoDeletableFile};

#[tokio::test]
/// Asserts that a background instance can be spawned
async fn spawnable() {
async fn background_devnet_can_be_spawned() {
BackgroundDevnet::spawn().await.expect("Could not start Devnet");
}

#[tokio::test]
async fn background_devnets_at_different_ports_with_random_acquisition() {
let devnet_args = ["--port", "0"];
let devnet1 = BackgroundDevnet::spawn_with_additional_args(&devnet_args).await.unwrap();
let devnet2 = BackgroundDevnet::spawn_with_additional_args(&devnet_args).await.unwrap();

assert_ne!(devnet1.url, devnet2.url);
}

#[tokio::test]
async fn too_big_request_rejected_via_non_rpc() {
let limit = 1_000;
Expand Down Expand Up @@ -92,7 +100,7 @@ mod general_integration_tests {
async fn test_config() {
// random values
let dump_file = UniqueAutoDeletableFile::new("dummy");
let mut expected_config = json!({
let expected_config = json!({
"seed": 1,
"total_accounts": 2,
"account_contract_class_hash": "0x61dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f",
Expand All @@ -112,7 +120,7 @@ mod general_integration_tests {
},
"server_config": {
"host": "0.0.0.0",
// expected port added after spawning; determined by port-acquiring logic
"port": 0, // default value in tests, config not modified upon finding a free port
"timeout": 121,
"request_body_size_limit": 1000,
"restricted_methods": null,
Expand Down Expand Up @@ -161,8 +169,6 @@ mod general_integration_tests {
.await
.unwrap();

expected_config["server_config"]["port"] = devnet.port.into();

let fetched_config = devnet.get_config().await;
assert_eq!(fetched_config, expected_config);
}
Expand Down