diff --git a/.gitignore b/.gitignore index d0c37afe0..9df00c72a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ hyperdrive/packages/settings/pkg/ui/* hyperdrive/src/register-ui/build/ hyperdrive/src/register-ui/dist/ hyperdrive/packages/docs/pkg/ui +/.idea diff --git a/hyperdrive/src/main.rs b/hyperdrive/src/main.rs index 17c946186..c440d5d7f 100644 --- a/hyperdrive/src/main.rs +++ b/hyperdrive/src/main.rs @@ -6,7 +6,6 @@ use lib::types::core::{ NetworkErrorSender, NodeRouting, PrintReceiver, PrintSender, ProcessId, ProcessVerbosity, Request, KERNEL_PROCESS_ID, }; -use lib::types::eth::RpcUrlConfigInput; #[cfg(feature = "simulation-mode")] use ring::{rand::SystemRandom, signature, signature::KeyPair}; use std::collections::HashMap; @@ -88,6 +87,12 @@ async fn main() { let rpc_config = matches.get_one::("rpc-config").map(|p| { std::fs::canonicalize(&p).expect(&format!("specified rpc-config path {p} not found")) }); + + // Prevent using both --rpc and --rpc-config flags simultaneously + if rpc.is_some() && rpc_config.is_some() { + panic!("Cannot use both --rpc and --rpc-config flags simultaneously. Please use either --rpc for a single RPC URL or --rpc-config for multiple URLs and/or advanced configs (e.g., auth), but not both."); + } + let password = matches.get_one::("password"); // logging mode is toggled at runtime by CTRL+L @@ -129,38 +134,114 @@ async fn main() { is_eth_provider_config_updated = true; serde_json::from_str(DEFAULT_ETH_PROVIDERS).unwrap() }; + + // TODO: Remove debug logging before merging - before modification + eprintln!( + "[DEBUG-AUTH] eth_provider_config before --rpc/--rpc-config processing: {:#?}", + eth_provider_config + ); + if let Some(rpc) = rpc { - eth_provider_config.insert( - 0, - lib::eth::ProviderConfig { - chain_id: CHAIN_ID, - trusted: true, - provider: lib::eth::NodeOrRpcUrl::RpcUrl { - url: rpc.to_string(), - auth: None, - }, + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Processing --rpc flag with URL: {}", rpc); + + let new_provider = lib::eth::ProviderConfig { + chain_id: CHAIN_ID, + trusted: true, + provider: lib::eth::NodeOrRpcUrl::RpcUrl { + url: rpc.to_string(), + auth: None, }, - ); + }; + + add_provider_to_config(&mut eth_provider_config, new_provider); is_eth_provider_config_updated = true; + + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Added --rpc provider (with deduplication)"); } if let Some(rpc_config) = rpc_config { - let rpc_config = tokio::fs::read_to_string(rpc_config) - .await - .expect("cant read rpc-config"); - let rpc_config: Vec = - serde_json::from_str(&rpc_config).expect("rpc-config had invalid format"); - for RpcUrlConfigInput { url, auth } in rpc_config { - eth_provider_config.insert( - 0, - lib::eth::ProviderConfig { - chain_id: CHAIN_ID, - trusted: true, - provider: lib::eth::NodeOrRpcUrl::RpcUrl { url, auth }, - }, - ); + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] Processing --rpc-config flag with file: {}", + rpc_config.display() + ); + + match std::fs::read_to_string(&rpc_config) { + Ok(contents) => { + match serde_json::from_str::>(&contents) { + Ok(rpc_configs) => { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] Loaded {} providers from config file", + rpc_configs.len() + ); + + // Store the length before consuming the vector + let total_configs = rpc_configs.len(); + + // Process in reverse order so the first entry in the file becomes highest priority + for (reverse_index, rpc_url_config) in + rpc_configs.into_iter().rev().enumerate() + { + let original_index = total_configs - 1 - reverse_index; + + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Processing config provider {} (original position {}): {}", + reverse_index + 1, original_index + 1, rpc_url_config.url); + if let Some(ref auth) = rpc_url_config.auth { + match auth { + lib::eth::Authorization::Basic(creds) => { + eprintln!("[DEBUG-AUTH] Config provider has Basic auth (length: {})", creds.len()); + } + lib::eth::Authorization::Bearer(token) => { + eprintln!("[DEBUG-AUTH] Config provider has Bearer auth (length: {})", token.len()); + } + lib::eth::Authorization::Raw(raw) => { + eprintln!("[DEBUG-AUTH] Config provider has Raw auth (length: {})", raw.len()); + } + } + } else { + eprintln!("[DEBUG-AUTH] Config provider has no auth"); + } + + let new_provider = lib::eth::ProviderConfig { + chain_id: CHAIN_ID, + trusted: true, + provider: lib::eth::NodeOrRpcUrl::RpcUrl { + url: rpc_url_config.url, + auth: rpc_url_config.auth, + }, + }; + + add_provider_to_config(&mut eth_provider_config, new_provider); + } + is_eth_provider_config_updated = true; + + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Added all --rpc-config providers (with deduplication, order preserved)"); + } + Err(e) => { + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Failed to parse RPC config file: {}", e); + eprintln!("Failed to parse RPC config file: {e}"); + } + } + } + Err(e) => { + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Failed to read RPC config file: {}", e); + eprintln!("Failed to read RPC config file: {e}"); + } } - is_eth_provider_config_updated = true; } + + // TODO: Remove debug logging before merging - after modification + eprintln!( + "[DEBUG-AUTH] eth_provider_config after --rpc/--rpc-config processing: {:#?}", + eth_provider_config + ); + if is_eth_provider_config_updated { // save the new provider config tokio::fs::write( @@ -274,7 +355,7 @@ async fn main() { (ws_tcp_handle, ws_flag_used), (tcp_tcp_handle, tcp_flag_used), http_server_port, - rpc.cloned(), + eth_provider_config.clone(), detached, ) .await @@ -285,7 +366,7 @@ async fn main() { our_ip.to_string(), (ws_tcp_handle, ws_flag_used), (tcp_tcp_handle, tcp_flag_used), - rpc.cloned(), + eth_provider_config.clone(), password, ) .await @@ -842,7 +923,7 @@ async fn serve_register_fe( ws_networking: (Option, bool), tcp_networking: (Option, bool), http_server_port: u16, - maybe_rpc: Option, + eth_provider_config: lib::eth::SavedConfigs, detached: bool, ) -> (Identity, Vec, Keyfile) { let (kill_tx, kill_rx) = tokio::sync::oneshot::channel::(); @@ -861,7 +942,7 @@ async fn serve_register_fe( (tcp_networking.0.as_ref(), tcp_networking.1), http_server_port, disk_keyfile, - maybe_rpc, + eth_provider_config, detached) => { panic!("registration failed") } @@ -888,7 +969,7 @@ async fn login_with_password( our_ip: String, ws_networking: (Option, bool), tcp_networking: (Option, bool), - maybe_rpc: Option, + eth_provider_config: lib::eth::SavedConfigs, password: &str, ) -> (Identity, Vec, Keyfile) { use argon2::Argon2; @@ -962,7 +1043,7 @@ async fn login_with_password( }, }; - let provider = Arc::new(register::connect_to_provider(maybe_rpc).await); + let provider = Arc::new(register::connect_to_provider_from_config(ð_provider_config).await); register::assign_routing( &mut our, @@ -986,6 +1067,45 @@ async fn login_with_password( (our, disk_keyfile, k) } +/// Add a provider config with deduplication logic (same as runtime system) +fn add_provider_to_config( + eth_provider_config: &mut lib::eth::SavedConfigs, + new_provider: lib::eth::ProviderConfig, +) { + match &new_provider.provider { + lib::eth::NodeOrRpcUrl::RpcUrl { url, .. } => { + // Remove any existing provider with this URL + eth_provider_config.0.retain(|config| { + if let lib::eth::NodeOrRpcUrl::RpcUrl { + url: existing_url, .. + } = &config.provider + { + existing_url != url + } else { + true + } + }); + } + lib::eth::NodeOrRpcUrl::Node { hns_update, .. } => { + // Remove any existing provider with this node name + eth_provider_config.0.retain(|config| { + if let lib::eth::NodeOrRpcUrl::Node { + hns_update: existing_update, + .. + } = &config.provider + { + existing_update.name != hns_update.name + } else { + true + } + }); + } + } + + // Insert the new provider at the front (position 0) + eth_provider_config.0.insert(0, new_provider); +} + fn make_remote_link(url: &str, text: &str) -> String { format!("\x1B]8;;{}\x1B\\{}\x1B]8;;\x1B\\", url, text) } diff --git a/hyperdrive/src/register.rs b/hyperdrive/src/register.rs index 138ac7b84..db7b504b6 100644 --- a/hyperdrive/src/register.rs +++ b/hyperdrive/src/register.rs @@ -39,7 +39,7 @@ pub async fn register( tcp_networking: (Option<&tokio::net::TcpListener>, bool), http_port: u16, keyfile: Option>, - maybe_rpc: Option, + eth_provider_config: lib::eth::SavedConfigs, detached: bool, ) { // Networking info is generated and passed to the UI, but not used until confirmed @@ -88,7 +88,7 @@ pub async fn register( }, }); - let provider = Arc::new(connect_to_provider(maybe_rpc).await); + let provider = Arc::new(connect_to_provider_from_config(ð_provider_config).await); let keyfile = warp::any().map(move || keyfile.clone()); let our_temp_id = warp::any().map(move || our_temp_id.clone()); @@ -245,38 +245,139 @@ pub async fn register( .await; } -/// Connect to given provider or one of two public RPC providers as fallbacks. -/// TODO: add more fallbacks -pub async fn connect_to_provider(maybe_rpc: Option) -> RootProvider { - let url = if let Some(ref rpc_url) = maybe_rpc { - rpc_url - } else { - "wss://base-rpc.publicnode.com" - }; +/// Connect to provider using the saved configuration with fallbacks +pub async fn connect_to_provider_from_config( + eth_provider_config: &lib::eth::SavedConfigs, +) -> RootProvider { + let saved_configs = ð_provider_config.0; + + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Number of configured providers (including Node providers which will not be used): {}", saved_configs.len()); + + // Try each configured provider first + for (index, provider_config) in saved_configs.iter().enumerate() { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] Trying configured provider {}/{}", + index + 1, + saved_configs.len() + ); + + match &provider_config.provider { + lib::eth::NodeOrRpcUrl::RpcUrl { url, auth } => { + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] RPC URL provider: {}", url); + eprintln!("[DEBUG-AUTH] Auth present: {}", auth.is_some()); + if let Some(auth_ref) = auth { + match auth_ref { + lib::eth::Authorization::Basic(creds) => { + eprintln!( + "[DEBUG-AUTH] Auth type: Basic (credentials length: {})", + creds.len() + ); + eprintln!( + "[DEBUG-AUTH] Basic auth format valid (contains ':'): {}", + creds.contains(':') + ); + } + lib::eth::Authorization::Bearer(token) => { + eprintln!( + "[DEBUG-AUTH] Auth type: Bearer (token length: {})", + token.len() + ); + } + lib::eth::Authorization::Raw(raw) => { + eprintln!("[DEBUG-AUTH] Auth type: Raw (value length: {})", raw.len()); + } + } + } - let rpc_urls = [ - url, - "wss://base.llamarpc.com", - "wss://base-rpc.publicnode.com", - //"wss://1rpc.io/base", - //"wss://base.blockpi.network/v1/rpc/public", - ]; + let ws_connect = WsConnect { + url: url.clone(), + auth: auth.clone().map(|a| a.into()), + config: None, + }; - for rpc_url in rpc_urls { - if let Ok(client) = ProviderBuilder::new() - .on_ws(WsConnect::new(rpc_url.to_string())) - .await - { - println!("Connected to {rpc_url}\r"); + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] Attempting connection to provider: {}", url); + + if let Ok(client) = ProviderBuilder::new().on_ws(ws_connect).await { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] ✅ Successfully connected to configured provider: {}", + url + ); + println!("Connected to configured provider: {url}\r"); + return client; + } else { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] ❌ Failed to connect to configured provider: {}", + url + ); + println!("Failed to connect to provider: {url}\r"); + } + } + lib::eth::NodeOrRpcUrl::Node { + hns_update, + use_as_provider, + } => { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] Node provider: {} (use_as_provider: {})", + hns_update.name, use_as_provider + ); + eprintln!("[DEBUG-AUTH] Skipping node provider (need RPC URL)"); + continue; + } + } + } + + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] All configured providers failed, falling back to defaults"); + + // Fall back to default providers if configured ones fail + let default_rpc_urls = ["wss://base.llamarpc.com", "wss://base-rpc.publicnode.com"]; + + for (index, rpc_url) in default_rpc_urls.iter().enumerate() { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] Trying fallback provider {}/{}: {}", + index + 1, + default_rpc_urls.len(), + rpc_url + ); + + let ws_connect = WsConnect { + url: rpc_url.to_string(), + auth: None, + config: None, + }; + + if let Ok(client) = ProviderBuilder::new().on_ws(ws_connect).await { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] ✅ Successfully connected to fallback provider: {}", + rpc_url + ); + println!("Connected to fallback provider: {rpc_url}\r"); return client; + } else { + // TODO: Remove debug logging before merging + eprintln!( + "[DEBUG-AUTH] ❌ Failed to connect to fallback provider: {}", + rpc_url + ); } } + // TODO: Remove debug logging before merging + eprintln!("[DEBUG-AUTH] ❌ All providers (configured + fallback) failed!"); + panic!( - "Error: runtime could not connect to Base ETH RPCs {rpc_urls:?}\n\ + "Error: runtime could not connect to any ETH RPC providers\n\ This is necessary in order to verify node identity onchain.\n\ - Please make sure you are using a valid WebSockets URL if using \ - the --rpc flag, and you are connected to the internet." + Please check your configured providers and internet connection." ); }