From 074cf3c30040ed46ff25f520f335b9bcb3640e1c Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:30:30 -0800 Subject: [PATCH 01/11] Fix issue on Windows where the address to which the client connects cannot be 0.0.0.0 --- crates/sui/src/sui_commands.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 121ed072f276f..4aca98cb67f16 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1223,6 +1223,7 @@ async fn start( if let Some(input) = with_faucet { let faucet_address = parse_host_port(input, DEFAULT_FAUCET_PORT) .map_err(|_| anyhow!("Invalid faucet host and port"))?; + info!("Starting the faucet service at {faucet_address}"); let host_ip = match faucet_address { @@ -1247,12 +1248,18 @@ async fn start( .import(None, SuiKeyPair::Ed25519(kp)) .await .unwrap(); + let localnet_ip = if fullnode_rpc_address.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) + { + format!("http://127.0.0.1:{}", fullnode_rpc_address.port()) + } else { + fullnode_rpc_url + }; SuiClientConfig { keystore, external_keys: None, envs: vec![SuiEnv { alias: "localnet".to_string(), - rpc: fullnode_rpc_url, + rpc: localnet_ip, ws: None, basic_auth: None, chain_id: None, From d243e0f990cd7a1b71103a8ba3b6dea50f6c175e Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:33:59 -0800 Subject: [PATCH 02/11] Cleanup --- crates/sui/src/sui_commands.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 4aca98cb67f16..378bd6a8636cb 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1248,12 +1248,16 @@ async fn start( .import(None, SuiKeyPair::Ed25519(kp)) .await .unwrap(); + + // On windows, using 0.0.0.0 will usually yield in an networking error. This localnet ip + // address must bind to 127.0.0.1 if the default 0.0.0.0 is used. let localnet_ip = if fullnode_rpc_address.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { format!("http://127.0.0.1:{}", fullnode_rpc_address.port()) } else { fullnode_rpc_url }; + SuiClientConfig { keystore, external_keys: None, From 46e41351fc77deff804e3e1c36f2b5ad72b1aeef Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:38:42 -0800 Subject: [PATCH 03/11] Cleanup to avoid clippy warnings on windows --- crates/sui-keys/src/keystore.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/sui-keys/src/keystore.rs b/crates/sui-keys/src/keystore.rs index f16c20c0ee167..96678013f5ee3 100644 --- a/crates/sui-keys/src/keystore.rs +++ b/crates/sui-keys/src/keystore.rs @@ -407,7 +407,7 @@ impl FileBasedKeystore { }); let reader = - BufReader::new(std::fs::File::open(path).with_context(|| { + BufReader::new(fs::File::open(path).with_context(|| { format!("Cannot open the keystore file: {}", path.display()) })?); let kp_strings: Vec = serde_json::from_reader(reader).with_context(|| { @@ -430,7 +430,7 @@ impl FileBasedKeystore { aliases_path.set_extension(ALIASES_FILE_EXTENSION); let aliases = if aliases_path.exists() { - let reader = BufReader::new(std::fs::File::open(&aliases_path).with_context(|| { + let reader = BufReader::new(fs::File::open(&aliases_path).with_context(|| { format!( "Cannot open aliases file in keystore: {}", aliases_path.display() @@ -484,7 +484,7 @@ impl FileBasedKeystore { ) })?; - std::fs::write(aliases_path, aliases_store)?; + fs::write(aliases_path, aliases_store)?; aliases }; @@ -513,7 +513,7 @@ impl FileBasedKeystore { let mut aliases_path = path.clone(); aliases_path.set_extension(ALIASES_FILE_EXTENSION); // no reactor for tokio::fs::write in simtest, so we use spawn_blocking - tokio::task::spawn_blocking(move || std::fs::write(aliases_path, aliases_store)) + tokio::task::spawn_blocking(move || fs::write(aliases_path, aliases_store)) .await? .with_context(|| format!("Cannot write aliases to file: {}", path.display()))?; } @@ -536,7 +536,7 @@ impl FileBasedKeystore { let keystore_path = path.clone(); // no reactor for tokio::fs::write in simtest, so we use spawn_blocking tokio::task::spawn_blocking(move || { - let ret = std::fs::write(&keystore_path, store); + let ret = fs::write(&keystore_path, store); #[cfg(unix)] if ret.is_ok() { let _ = set_reduced_file_permissions(&keystore_path).inspect_err(|error| { From 9be9c25f499f64223e6f8152d232add03db162b8 Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:54:23 -0800 Subject: [PATCH 04/11] Rework the logic for fullnode ip address to transform an unspecified address to a localhost. This is needed for Windows, for clients to correctly connect to services running on 0.0.0.0:port --- crates/sui/src/sui_commands.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 378bd6a8636cb..aa608b2014e14 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1049,7 +1049,16 @@ async fn start( tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; info!("Cluster started"); - let fullnode_rpc_url = format!("http://{fullnode_rpc_address}"); + let fullnode_rpc_ip = match fullnode_rpc_address.ip() { + IpAddr::V4(v4) if v4.is_unspecified() => Ipv4Addr::LOCALHOST, + IpAddr::V6(v6) if v6.is_unspecified() => Ipv4Addr::LOCALHOST, + IpAddr::V4(v4) => v4, + IpAddr::V6(v6) => v6.to_ipv4().ok_or_else(|| { + anyhow!("Fullnode RPC address must be an IPv4 address or unspecified address") + })?, + }; + + let fullnode_rpc_url = format!("http://{fullnode_rpc_ip}:{}", fullnode_rpc_address.port(),); info!("Fullnode RPC URL: {fullnode_rpc_url}"); let prometheus_registry = Registry::new(); @@ -1249,21 +1258,12 @@ async fn start( .await .unwrap(); - // On windows, using 0.0.0.0 will usually yield in an networking error. This localnet ip - // address must bind to 127.0.0.1 if the default 0.0.0.0 is used. - let localnet_ip = if fullnode_rpc_address.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - { - format!("http://127.0.0.1:{}", fullnode_rpc_address.port()) - } else { - fullnode_rpc_url - }; - SuiClientConfig { keystore, external_keys: None, envs: vec![SuiEnv { alias: "localnet".to_string(), - rpc: localnet_ip, + rpc: fullnode_rpc_url.clone(), ws: None, basic_auth: None, chain_id: None, From c777b5ee9e4410b4f8742f7066bd72f80412b1c4 Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:05:13 -0800 Subject: [PATCH 05/11] More fixes to enable windows to run a local network with graphql --- .../src/consistent_reader.rs | 5 +--- .../src/fullnode_client.rs | 4 +--- crates/sui/src/sui_commands.rs | 24 +++++++++++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/sui-indexer-alt-reader/src/consistent_reader.rs b/crates/sui-indexer-alt-reader/src/consistent_reader.rs index f267d59aef54e..f17a763d0778b 100644 --- a/crates/sui-indexer-alt-reader/src/consistent_reader.rs +++ b/crates/sui-indexer-alt-reader/src/consistent_reader.rs @@ -102,10 +102,7 @@ impl ConsistentReader { endpoint = endpoint.timeout(timeout); } - let channel = endpoint - .connect() - .await - .context("Failed to connect to gRPC endpoint")?; + let channel = endpoint.connect_lazy(); Some(ConsistentServiceClient::new(channel)) } else { diff --git a/crates/sui-indexer-alt-reader/src/fullnode_client.rs b/crates/sui-indexer-alt-reader/src/fullnode_client.rs index fa7790f22e4fd..6bf4776dc64d7 100644 --- a/crates/sui-indexer-alt-reader/src/fullnode_client.rs +++ b/crates/sui-indexer-alt-reader/src/fullnode_client.rs @@ -61,9 +61,7 @@ impl FullnodeClient { let execution_client = if let Some(url) = &args.fullnode_rpc_url { let channel = Channel::from_shared(url.clone()) .context("Failed to create channel for gRPC endpoint")? - .connect() - .await - .context("Failed to connect to gRPC endpoint")?; + .connect_lazy(); Some(TransactionExecutionServiceClient::new(channel)) } else { diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index aa608b2014e14..f01de15d51efc 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1176,12 +1176,26 @@ async fn start( no_ide: false, }; + // on Windows, a client needs to connect to 127.0.0.1 if the server is listening on + // 0.0.0.0:port. + let consistent_store_url = consistent_store_url + .as_ref() + .map(|url| Url::parse(url)) + .transpose() + .context("Failed to parse consistent store URL")? + .map(|mut url| { + if url.host_str() == Some("0.0.0.0") { + let port = url.port(); + url.set_host(Some("127.0.0.1")).ok(); + if let Some(p) = port { + url.set_port(Some(p)).ok(); + } + } + url + }); + let consistent_reader_args = ConsistentReaderArgs { - consistent_store_url: consistent_store_url - .as_ref() - .map(|url| Url::parse(url)) - .transpose() - .context("Failed to parse consistent store URL")?, + consistent_store_url, ..Default::default() }; From a451bbc6a08c7a7b2e748b1da1fd023f0f9c15aa Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:23:17 -0800 Subject: [PATCH 06/11] Cleanup --- crates/sui/src/sui_commands.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index f01de15d51efc..6cd9774ab31b9 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1176,23 +1176,20 @@ async fn start( no_ide: false, }; - // on Windows, a client needs to connect to 127.0.0.1 if the server is listening on - // 0.0.0.0:port. let consistent_store_url = consistent_store_url .as_ref() - .map(|url| Url::parse(url)) - .transpose() - .context("Failed to parse consistent store URL")? - .map(|mut url| { - if url.host_str() == Some("0.0.0.0") { - let port = url.port(); - url.set_host(Some("127.0.0.1")).ok(); - if let Some(p) = port { - url.set_port(Some(p)).ok(); - } + .map(|url_str| -> Result { + let mut url = + Url::parse(url_str).context("Failed to parse consistent store URL")?; + if let Some("0.0.0.0") = url.host_str() { + url.set_host(Some("127.0.0.1")) + .context("Failed to set host to 127.0.0.1")?; } - url - }); + + Ok(url) + }) + .transpose() + .context("Failed to parse consistent store URL")?; let consistent_reader_args = ConsistentReaderArgs { consistent_store_url, From 9e24e21fc098752409df1332c851bcc498becb91 Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:18:18 -0800 Subject: [PATCH 07/11] Cleanup --- crates/sui/src/sui_commands.rs | 66 +++++++++++++++------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 6cd9774ab31b9..64da1327f71b6 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -23,7 +23,7 @@ use prometheus::Registry; use rand::rngs::OsRng; use std::collections::BTreeMap; use std::io::{Write, stdout}; -use std::net::{AddrParseError, IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::num::NonZeroUsize; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -1049,16 +1049,8 @@ async fn start( tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; info!("Cluster started"); - let fullnode_rpc_ip = match fullnode_rpc_address.ip() { - IpAddr::V4(v4) if v4.is_unspecified() => Ipv4Addr::LOCALHOST, - IpAddr::V6(v6) if v6.is_unspecified() => Ipv4Addr::LOCALHOST, - IpAddr::V4(v4) => v4, - IpAddr::V6(v6) => v6.to_ipv4().ok_or_else(|| { - anyhow!("Fullnode RPC address must be an IPv4 address or unspecified address") - })?, - }; - - let fullnode_rpc_url = format!("http://{fullnode_rpc_ip}:{}", fullnode_rpc_address.port(),); + let fullnode_rpc_ip = normalize_bind_addr(fullnode_rpc_address); + let fullnode_rpc_url = format!("http://{fullnode_rpc_ip}:{}", fullnode_rpc_address.port()); info!("Fullnode RPC URL: {fullnode_rpc_url}"); let prometheus_registry = Registry::new(); @@ -1160,7 +1152,7 @@ async fn start( rpc_services.push(handle); info!("Consistent Store started at {address}"); - Some(format!("http://{address}")) + Some(address) } else { None }; @@ -1176,21 +1168,7 @@ async fn start( no_ide: false, }; - let consistent_store_url = consistent_store_url - .as_ref() - .map(|url_str| -> Result { - let mut url = - Url::parse(url_str).context("Failed to parse consistent store URL")?; - if let Some("0.0.0.0") = url.host_str() { - url.set_host(Some("127.0.0.1")) - .context("Failed to set host to 127.0.0.1")?; - } - - Ok(url) - }) - .transpose() - .context("Failed to parse consistent store URL")?; - + let consistent_store_url = consistent_store_url.map(socket_addr_to_url).transpose()?; let consistent_reader_args = ConsistentReaderArgs { consistent_store_url, ..Default::default() @@ -1566,19 +1544,15 @@ async fn genesis( // On windows, using 0.0.0.0 will usually yield in an networking error. This localnet ip // address must bind to 127.0.0.1 if the default 0.0.0.0 is used. - let localnet_ip = - if fullnode_config.json_rpc_address.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { - "127.0.0.1".to_string() - } else { - fullnode_config.json_rpc_address.ip().to_string() - }; + let rpc = format!( + "http://{}:{}", + normalize_bind_addr(fullnode_config.json_rpc_address), + fullnode_config.json_rpc_address.port() + ); + client_config.add_env(SuiEnv { alias: "localnet".to_string(), - rpc: format!( - "http://{}:{}", - localnet_ip, - fullnode_config.json_rpc_address.port() - ), + rpc, ws: None, basic_auth: None, chain_id: None, @@ -1890,3 +1864,19 @@ pub async fn get_replay_node(context: &WalletContext) -> Result bail!(err_msg), }) } + +/// Converts a socket address to a Url by setting the scheme to HTTP. +fn socket_addr_to_url(addr: SocketAddr) -> Result { + let ip = normalize_bind_addr(addr); + Url::parse(&format!("http://{ip}:{}", addr.port())) + .with_context(|| format!("Failed to parse {addr} into a Url")) +} + +/// Resolves an unspecified ip address to a localhost IP address. +fn normalize_bind_addr(addr: SocketAddr) -> IpAddr { + match addr.ip() { + IpAddr::V4(v4) if v4.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST), + IpAddr::V6(v6) if v6.is_unspecified() => IpAddr::V6(Ipv6Addr::LOCALHOST), + ip => ip, + } +} From 5f860024e34afdd2a5601aea536a34e1cb00420d Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:26:51 -0800 Subject: [PATCH 08/11] Move LedgerGrpcReader to use connect_lazy too --- crates/sui-indexer-alt-reader/src/ledger_grpc_reader.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/sui-indexer-alt-reader/src/ledger_grpc_reader.rs b/crates/sui-indexer-alt-reader/src/ledger_grpc_reader.rs index e8aa6f627766c..d6072b9e8495a 100644 --- a/crates/sui-indexer-alt-reader/src/ledger_grpc_reader.rs +++ b/crates/sui-indexer-alt-reader/src/ledger_grpc_reader.rs @@ -51,11 +51,7 @@ impl LedgerGrpcReader { if let Some(timeout) = args.statement_timeout() { endpoint = endpoint.timeout(timeout); } - let channel = endpoint - .tls_config(tls_config)? - .connect() - .await - .context("Failed to connect to gRPC endpoint")?; + let channel = endpoint.tls_config(tls_config)?.connect_lazy(); let client = LedgerServiceClient::new(channel.clone()); Ok(Self(client)) From c2d9597e0bab493f2630a9a8046d1a6e920315d0 Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:29:15 -0800 Subject: [PATCH 09/11] Cleanup --- crates/sui/src/sui_commands.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 64da1327f71b6..63fb1b4ad9861 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1542,8 +1542,6 @@ async fn genesis( client_config.active_address = active_address; } - // On windows, using 0.0.0.0 will usually yield in an networking error. This localnet ip - // address must bind to 127.0.0.1 if the default 0.0.0.0 is used. let rpc = format!( "http://{}:{}", normalize_bind_addr(fullnode_config.json_rpc_address), From 8e1aef2256b0fa3e7b60214ee9707c86e4fb7ae1 Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:30:10 -0800 Subject: [PATCH 10/11] Cleanup --- crates/sui/src/sui_commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 63fb1b4ad9861..02c865c8775b0 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1870,7 +1870,8 @@ fn socket_addr_to_url(addr: SocketAddr) -> Result { .with_context(|| format!("Failed to parse {addr} into a Url")) } -/// Resolves an unspecified ip address to a localhost IP address. +/// Resolves an unspecified ip address to a localhost IP address. Particularly on Windows, clients +/// cannot connect to 0.0.0.0 addresses. fn normalize_bind_addr(addr: SocketAddr) -> IpAddr { match addr.ip() { IpAddr::V4(v4) if v4.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST), From cf5a6f440b517acc2dabb9050ed53bb347e6a26c Mon Sep 17 00:00:00 2001 From: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:31:22 -0800 Subject: [PATCH 11/11] Use socket_addr_to_url where possible --- crates/sui/src/sui_commands.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index 02c865c8775b0..3061d274d9c7f 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -1049,8 +1049,7 @@ async fn start( tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; info!("Cluster started"); - let fullnode_rpc_ip = normalize_bind_addr(fullnode_rpc_address); - let fullnode_rpc_url = format!("http://{fullnode_rpc_ip}:{}", fullnode_rpc_address.port()); + let fullnode_rpc_url = socket_addr_to_url(fullnode_rpc_address)?.to_string(); info!("Fullnode RPC URL: {fullnode_rpc_url}"); let prometheus_registry = Registry::new(); @@ -1542,12 +1541,7 @@ async fn genesis( client_config.active_address = active_address; } - let rpc = format!( - "http://{}:{}", - normalize_bind_addr(fullnode_config.json_rpc_address), - fullnode_config.json_rpc_address.port() - ); - + let rpc = socket_addr_to_url(fullnode_config.json_rpc_address)?.to_string(); client_config.add_env(SuiEnv { alias: "localnet".to_string(), rpc,