From aca10be1f826bf8537ddf008d268a9c4a48d7507 Mon Sep 17 00:00:00 2001 From: Diego Prats Date: Tue, 18 Feb 2025 14:59:51 -0800 Subject: [PATCH] fix: bug when users install CLI (#1269) --- clients/cli/Cargo.lock | 2 +- clients/cli/Cargo.toml | 2 +- clients/cli/src/config.rs | 39 +-- clients/cli/src/main.rs | 9 +- clients/cli/src/node_id_manager.rs | 88 ++++++ clients/cli/src/prover_id_manager.rs | 383 --------------------------- clients/cli/src/setup.rs | 132 +++++---- 7 files changed, 168 insertions(+), 487 deletions(-) create mode 100644 clients/cli/src/node_id_manager.rs delete mode 100644 clients/cli/src/prover_id_manager.rs diff --git a/clients/cli/Cargo.lock b/clients/cli/Cargo.lock index 461a46b..ccfabb8 100644 --- a/clients/cli/Cargo.lock +++ b/clients/cli/Cargo.lock @@ -1610,7 +1610,7 @@ dependencies = [ [[package]] name = "nexus-network" -version = "0.4.6" +version = "0.5.0" dependencies = [ "async-stream", "blake3", diff --git a/clients/cli/Cargo.toml b/clients/cli/Cargo.toml index e504f26..d1b76ae 100644 --- a/clients/cli/Cargo.toml +++ b/clients/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nexus-network" -version = "0.4.6" +version = "0.5.0" edition = "2021" rust-version = "1.75" build = "build.rs" diff --git a/clients/cli/src/config.rs b/clients/cli/src/config.rs index 39989ba..09f18a1 100644 --- a/clients/cli/src/config.rs +++ b/clients/cli/src/config.rs @@ -9,13 +9,12 @@ pub enum Environment { impl Environment { pub fn orchestrator_url(&self) -> String { - let url = match self { + match self { Environment::Local => "http://localhost:8080".to_string(), Environment::Dev => "https://dev.orchestrator.nexus.xyz".to_string(), Environment::Staging => "https://staging.orchestrator.nexus.xyz".to_string(), Environment::Beta => "https://beta.orchestrator.nexus.xyz".to_string(), - }; - url + } } pub fn from_args(local: bool, dev: bool, staging: bool, beta: bool) -> Self { @@ -33,37 +32,3 @@ impl Environment { } } } - -// // the firebase APP IDS by environment -// mod firebase { -// pub const DEV_APP_ID: &str = "1:954530464230:web:f0a14de14ef7bcdaa99627"; -// pub const STAGING_APP_ID: &str = "1:222794630996:web:1758d64a85eba687eaaac1"; -// pub const BETA_APP_ID: &str = "1:279395003658:web:04ee2c524474d683d75ef3"; - -// // Analytics keys for the different environments -// // These are keys that allow the measurement protocol to write to the analytics database -// // They are not sensitive. Worst case, if a malicious actor obtains the secret, they could potentially send false or misleading data to your GA4 property -// pub const DEV_API_SECRET: &str = "8ySxiKrtT8a76zClqqO8IQ"; -// pub const STAGING_API_SECRET: &str = "OI7H53soRMSDWfJf1ittHQ"; -// pub const BETA_API_SECRET: &str = "gxxzKAQLSl-uYI0eKbIi_Q"; -// } - -// // Release versions (existing code) -// pub fn analytics_id(env: &Environment) -> String { -// // Return the appropriate Firebase App ID based on the environment -// match env { -// Environment::Dev => firebase::DEV_APP_ID.to_string(), -// Environment::Staging => firebase::STAGING_APP_ID.to_string(), -// Environment::Beta => firebase::BETA_APP_ID.to_string(), -// _ => String::new(), -// } -// } - -// pub fn analytics_api_key(env: &Environment) -> String { -// match env { -// Environment::Dev => firebase::DEV_API_SECRET.to_string(), -// Environment::Staging => firebase::STAGING_API_SECRET.to_string(), -// Environment::Beta => firebase::BETA_API_SECRET.to_string(), -// _ => String::new(), -// } -// } diff --git a/clients/cli/src/main.rs b/clients/cli/src/main.rs index 36cbd2a..3025113 100644 --- a/clients/cli/src/main.rs +++ b/clients/cli/src/main.rs @@ -6,6 +6,7 @@ mod config; mod flops; #[path = "proto/nexus.orchestrator.rs"] mod nexus_orchestrator; +mod node_id_manager; mod orchestrator_client; mod setup; mod utils; @@ -24,13 +25,7 @@ use orchestrator_client::OrchestratorClient; use clap::Parser; use colored::Colorize; -// use serde_json; -use directories::ProjectDirs; -use rand::Rng; -use serde_json::json; use sha3::{Digest, Keccak256}; -use std::fs::File; -use std::io::Write; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -179,7 +174,7 @@ async fn authenticated_proving( println!("\tProof size: {} bytes", proof_bytes.len()); println!("4. Submitting proof..."); client - .submit_proof(&node_id, &proof_hash, proof_bytes) + .submit_proof(node_id, &proof_hash, proof_bytes) .await?; println!("{}", "5. Proof successfully submitted".green()); diff --git a/clients/cli/src/node_id_manager.rs b/clients/cli/src/node_id_manager.rs new file mode 100644 index 0000000..1a7dee5 --- /dev/null +++ b/clients/cli/src/node_id_manager.rs @@ -0,0 +1,88 @@ +use colored::Colorize; +// use rand::RngCore; +// use random_word::Lang; +use std::{fs, path::Path, path::PathBuf}; + +pub fn get_home_directory() -> Result { + match home::home_dir() { + Some(path) if !path.as_os_str().is_empty() => Ok(path), + _ => { + println!("Could not determine home directory"); + Err("No home directory found") + } + } +} + +pub fn create_nexus_directory(nexus_dir: &Path) -> std::io::Result<()> { + println!("Attempting to create .nexus directory"); + if let Err(e) = fs::create_dir(nexus_dir) { + eprintln!( + "{}: {}", + "Warning: Failed to create .nexus directory" + .to_string() + .yellow(), + e + ); + return Err(e); + } + + Ok(()) +} + +pub fn read_existing_node_id(node_id_path: &Path) -> Result { + let buf = fs::read(node_id_path)?; + let id = String::from_utf8(buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? + .trim() + .to_string(); + + if id.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Node ID file is empty", + )); + } + + Ok(id) +} + +fn save_node_id(path: &Path, id: &str) { + if let Err(e) = fs::write(path, id) { + println!("Failed to save node-id to file: {}", e); + } else { + println!("Successfully saved new node-id to file: {}", id); + } +} + +pub fn handle_read_error(e: std::io::Error, path: &Path, default_id: &str) { + match e.kind() { + std::io::ErrorKind::NotFound => { + save_node_id(path, default_id); + } + std::io::ErrorKind::PermissionDenied => { + eprintln!( + "{}: {}", + "Error: Permission denied when accessing node-id file" + .to_string() + .yellow(), + e + ); + } + std::io::ErrorKind::InvalidData => { + eprintln!( + "{}: {}", + "Error: node-id file is corrupted".to_string().yellow(), + e + ); + } + _ => { + eprintln!( + "{}: {}", + "Error: Unexpected IO error when reading node-id file" + .to_string() + .yellow(), + e + ); + } + } +} diff --git a/clients/cli/src/prover_id_manager.rs b/clients/cli/src/prover_id_manager.rs deleted file mode 100644 index 894c4fa..0000000 --- a/clients/cli/src/prover_id_manager.rs +++ /dev/null @@ -1,383 +0,0 @@ -use colored::Colorize; -use rand::RngCore; -use random_word::Lang; -use std::{fs, path::Path, path::PathBuf}; - -/// Gets an existing prover ID from the filesystem or generates a new one -/// This is the main entry point for getting a prover ID -pub fn get_or_generate_prover_id() -> String { - let default_prover_id = generate_default_id(); - - let home_path = match get_home_directory() { - Ok(path) => path, - Err(_) => return default_prover_id, - }; - - let nexus_dir = home_path.join(".nexus"); - let prover_id_path = nexus_dir.join("prover-id"); - - // 1. If the .nexus directory doesn't exist, we need to create it - if !nexus_dir.exists() { - return handle_first_time_setup(&nexus_dir, &prover_id_path, &default_prover_id); - } - - // 2. If the .nexus directory exists, we need to read the prover-id file - match read_existing_prover_id(&prover_id_path) { - // 2.1 Happy path - we successfully read the prover-id file - Ok(id) => { - println!("Successfully read existing prover-id from file: {}", id); - id - } - // 2.2 We couldn't read the prover-id file, so we may need to create a new one - Err(e) => { - eprintln!( - "{}: {}", - "Warning: Could not read prover-id file" - .to_string() - .yellow(), - e - ); - handle_read_error(e, &prover_id_path, &default_prover_id); - default_prover_id - } - } -} - -fn generate_default_id() -> String { - format!( - "{}-{}-{}", - random_word::gen(Lang::En), - random_word::gen(Lang::En), - rand::thread_rng().next_u32() % 100, - ) -} - -fn get_home_directory() -> Result { - match home::home_dir() { - Some(path) if !path.as_os_str().is_empty() => Ok(path), - _ => { - println!("Could not determine home directory"); - Err("No home directory found") - } - } -} - -fn handle_first_time_setup( - nexus_dir: &Path, - prover_id_path: &Path, - default_prover_id: &str, -) -> String { - println!("Attempting to create .nexus directory"); - if let Err(e) = fs::create_dir(nexus_dir) { - eprintln!( - "{}: {}", - "Warning: Failed to create .nexus directory" - .to_string() - .yellow(), - e - ); - return default_prover_id.to_string(); - } - - save_prover_id(prover_id_path, default_prover_id); - default_prover_id.to_string() -} - -fn read_existing_prover_id(prover_id_path: &Path) -> Result { - let buf = fs::read(prover_id_path)?; - let id = String::from_utf8(buf) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))? - .trim() - .to_string(); - - if id.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Prover ID file is empty", - )); - } - - Ok(id) -} - -fn save_prover_id(path: &Path, id: &str) { - if let Err(e) = fs::write(path, id) { - println!("Failed to save prover-id to file: {}", e); - } else { - println!("Successfully saved new prover-id to file: {}", id); - } -} - -fn handle_read_error(e: std::io::Error, path: &Path, default_id: &str) { - match e.kind() { - std::io::ErrorKind::NotFound => { - save_prover_id(path, default_id); - } - std::io::ErrorKind::PermissionDenied => { - eprintln!( - "{}: {}", - "Error: Permission denied when accessing prover-id file" - .to_string() - .yellow(), - e - ); - } - std::io::ErrorKind::InvalidData => { - eprintln!( - "{}: {}", - "Error: Prover-id file is corrupted".to_string().yellow(), - e - ); - } - _ => { - eprintln!( - "{}: {}", - "Error: Unexpected IO error when reading prover-id file" - .to_string() - .yellow(), - e - ); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serial_test::serial; - use std::env; - use tempfile::TempDir; // This is needed to run tests serially, to remove flakiness - - /// Tests the behavior for a first-time user with no existing configuration. - /// This simulates a new user scenario where: - /// 1. No .nexus directory exists yet - /// 2. No prover-id file exists yet - /// 3. The program needs to create both directory and file - #[test] - #[serial] - fn test_new_user() { - let temp_path = { - let temp_dir = TempDir::new().unwrap(); - let path = temp_dir.path().to_path_buf(); - println!("Directory at start: {:?}", path); - assert!(path.exists(), "Directory should exist during test"); - - // Setup - create temporary home directory - let original_home = env::var("HOME").ok(); - env::set_var("HOME", temp_dir.path()); - std::thread::sleep(std::time::Duration::from_millis(10)); // Give env time to update - - // Verify .nexus directory doesn't exist yet - let nexus_dir = temp_dir.path().join(".nexus"); - assert!(!nexus_dir.exists(), "Nexus directory should not exist yet"); - - // Get prover ID - should create directory and file - let id1 = get_or_generate_prover_id(); - println!("Generated ID: {}", id1); - - // Verify ID format (word-word-number) - let parts: Vec<&str> = id1.split('-').collect(); - assert_eq!(parts.len(), 3, "ID should be in format word-word-number"); - assert!( - parts[2].parse::().is_ok(), - "Last part should be a number" - ); - - // Verify directory and file were created - assert!( - nexus_dir.exists(), - "Nexus directory should have been created" - ); - let id_path = nexus_dir.join("prover-id"); - assert!(id_path.exists(), "Prover ID file should have been created"); - - // Verify saved ID matches what we got - let saved_id = - fs::read_to_string(&id_path).expect("Should be able to read prover-id file"); - assert_eq!(saved_id, id1, "Saved ID should match generated ID"); - - // Get ID again - should return same ID - let id2 = get_or_generate_prover_id(); - assert_eq!(id2, id1, "Second call should return same ID"); - - // Cleanup - match original_home { - None => env::remove_var("HOME"), - Some(home) => env::set_var("HOME", home), - } - std::thread::sleep(std::time::Duration::from_millis(10)); // Give env time to update - - path // Return the path for checking later - }; // temp_dir is dropped here, cleaning up - - // Verify cleanup happened - println!("Directory after test: {:?}", temp_path); - assert!(!temp_path.exists(), "Directory should be cleaned up"); - } - - /// Tests that the function can properly read an existing prover ID configuration. - /// This simulates a scenario where: - /// 1. User already has a .nexus directory - /// 2. User already has a prover-id file with valid content - /// 3. Function should read and use the existing ID without modification - #[test] - #[serial] - fn test_read_existing_prover_id() { - // Setup - create temporary home directory - let original_home = env::var("HOME").ok(); - let temp_dir = TempDir::new().unwrap(); - println!("Created temp dir: {:?}", temp_dir.path()); - env::set_var("HOME", temp_dir.path()); - std::thread::sleep(std::time::Duration::from_millis(10)); - - // Create pre-existing configuration - let nexus_dir = temp_dir.path().join(".nexus"); - fs::create_dir(&nexus_dir).expect("Failed to create .nexus directory"); - - let pre_existing_id = "happy-prover-42"; - let id_path = nexus_dir.join("prover-id"); - fs::write(&id_path, pre_existing_id).expect("Failed to create prover-id file"); - - // Verify our setup worked - assert!(nexus_dir.exists(), "Setup: .nexus directory should exist"); - assert!(id_path.exists(), "Setup: prover-id file should exist"); - assert_eq!( - fs::read_to_string(&id_path).expect("Setup: should be able to read file"), - pre_existing_id, - "Setup: file should contain our ID" - ); - - // Test that the function reads the existing ID - let read_id = get_or_generate_prover_id(); - assert_eq!( - read_id, pre_existing_id, - "Should return the pre-existing ID" - ); - - // Verify nothing was modified - assert!(nexus_dir.exists(), "Directory should still exist"); - assert!(id_path.exists(), "File should still exist"); - assert_eq!( - fs::read_to_string(&id_path).expect("Should still be able to read file"), - pre_existing_id, - "File content should be unchanged" - ); - - // Cleanup - match original_home { - None => env::remove_var("HOME"), - Some(home) => env::set_var("HOME", home), - } - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - /// Tests handling of corrupted prover-id file - #[test] - #[serial] - fn test_corrupted_prover_id_file() { - let temp_dir = TempDir::new().unwrap(); - env::set_var("HOME", temp_dir.path()); - - // Create .nexus directory and corrupted prover-id file - let nexus_dir = temp_dir.path().join(".nexus"); - fs::create_dir(&nexus_dir).unwrap(); - - let id_path = nexus_dir.join("prover-id"); - fs::write(&id_path, vec![0xFF, 0xFE, 0xFF]).unwrap(); // Invalid UTF-8 - - let id = get_or_generate_prover_id(); - assert!(id.contains('-'), "Should generate new valid ID"); - } - - /// Tests handling of permission denied scenarios - #[test] - #[serial] - #[cfg(unix)] // This test only works on Unix-like systems - fn test_permission_denied() { - use std::os::unix::fs::PermissionsExt; - - let temp_dir = TempDir::new().unwrap(); - env::set_var("HOME", temp_dir.path()); - - // Create .nexus directory with read-only permissions - let nexus_dir = temp_dir.path().join(".nexus"); - fs::create_dir(&nexus_dir).unwrap(); - - let metadata = fs::metadata(&nexus_dir).unwrap(); - let mut perms = metadata.permissions(); - perms.set_mode(0o444); // read-only - fs::set_permissions(&nexus_dir, perms).unwrap(); - - let id = get_or_generate_prover_id(); - assert!( - id.contains('-'), - "Should generate new ID when permissions denied" - ); - } - - /// Tests that IDs are properly formatted - #[test] - #[serial] - fn test_id_format() { - let id = get_or_generate_prover_id(); - let parts: Vec<&str> = id.split('-').collect(); - - assert_eq!(parts.len(), 3, "ID should have three parts"); - assert!( - parts[0].chars().all(|c| c.is_ascii_alphabetic()), - "First word should be alphabetic" - ); - assert!( - parts[1].chars().all(|c| c.is_ascii_alphabetic()), - "Second word should be alphabetic" - ); - assert!( - parts[2].parse::().is_ok(), - "Last part should be a number" - ); - assert!( - parts[2].parse::().unwrap() < 100, - "Number should be less than 100" - ); - } - - /// Tests behavior with empty prover-id file - #[test] - #[serial] - fn test_empty_prover_id_file() { - let temp_dir = TempDir::new().unwrap(); - env::set_var("HOME", temp_dir.path()); - - let nexus_dir = temp_dir.path().join(".nexus"); - fs::create_dir(&nexus_dir).unwrap(); - - let id_path = nexus_dir.join("prover-id"); - fs::write(&id_path, "").unwrap(); - - let id = get_or_generate_prover_id(); - assert!(id.contains('-'), "Should generate new ID for empty file"); - } - - /// Tests behavior when home directory is not available - #[test] - #[serial] - fn test_no_home_directory() { - env::remove_var("HOME"); - - let id = get_or_generate_prover_id(); - assert!( - id.contains('-'), - "Should generate temporary ID without home dir" - ); - } - - /// Tests that generated IDs are unique - #[test] - fn test_id_uniqueness() { - let mut ids = std::collections::HashSet::new(); - for _ in 0..100 { - let id = generate_default_id(); - assert!(ids.insert(id), "Generated IDs should be unique"); - } - } -} diff --git a/clients/cli/src/setup.rs b/clients/cli/src/setup.rs index ff16e46..00e713f 100644 --- a/clients/cli/src/setup.rs +++ b/clients/cli/src/setup.rs @@ -1,15 +1,15 @@ use colored::Colorize; -use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::fs; // Update the import path to use the proto module -// use crate::config; -// use crate::orchestrator_client::OrchestratorClient; +use crate::node_id_manager::{ + create_nexus_directory, get_home_directory, handle_read_error, read_existing_node_id, +}; pub enum SetupResult { Anonymous, - Connected(String), // String could be the public key or other connection info + Connected(String), Invalid, } @@ -19,51 +19,70 @@ pub struct UserConfig { pub user_id: Option, } -// fn save_user_config(user_id: &str, node_id: &str) -> std::io::Result<()> { -// let proj_dirs = -// ProjectDirs::from("xyz", "nexus", "cli").expect("Failed to determine config directory"); - -// let config_dir = proj_dirs.config_dir(); -// fs::create_dir_all(config_dir)?; - -// let config_path = config_dir.join("user.json"); -// let config = UserConfig { -// user_id: Some(user_id.to_string()), -// node_id: node_id.to_string(), -// }; - -// fs::write(&config_path, serde_json::to_string_pretty(&config)?)?; - -// //print the user config was saved properly -// println!("User ID: {}", user_id); -// println!("Node ID: {}", node_id); -// println!("User config saved to: {}", config_path.to_string_lossy()); - -// Ok(()) -// } - //function that takes a node_id and saves it to the user config fn save_node_id(node_id: &str) -> std::io::Result<()> { - let proj_dirs = - ProjectDirs::from("xyz", "nexus", "cli").expect("Failed to determine config directory"); - let config_path = proj_dirs.config_dir().join("user.json"); - let config = UserConfig { - node_id: node_id.to_string(), - user_id: None, + //get the home directory + let home_path = match get_home_directory() { + Ok(path) => path, + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to determine home directory", + )) + } }; - fs::write(&config_path, serde_json::to_string_pretty(&config)?)?; + let nexus_dir = home_path.join(".nexus"); + let node_id_path = nexus_dir.join("node-id"); - Ok(()) + //print how to find the node-id file + println!("Node ID file: {}", node_id_path.to_string_lossy()); + //write the node_id to the node-id file + fs::write(&node_id_path, node_id).unwrap(); + + // 2. If the .nexus directory exists, we need to read the node-id file + match read_existing_node_id(&node_id_path) { + // 2.1 Happy path - we successfully read the node-id file + Ok(id) => { + println!( + "Successfully read existing node-id '{}' from file: {}", + id, + node_id_path.to_string_lossy() + ); + Ok(()) + } + // 2.2 We couldn't read the node-id file, so we may need to create a new one + Err(e) => { + eprintln!( + "{}: {}", + "Warning: Could not read node-id file".to_string().yellow(), + e + ); + handle_read_error(e, &node_id_path, node_id); + Ok(()) + } + } } pub async fn run_initial_setup() -> SetupResult { - //check if there is a user config file - let proj_dirs = - ProjectDirs::from("xyz", "nexus", "cli").expect("Failed to determine config directory"); - let config_path = proj_dirs.config_dir().join("user.json"); - if config_path.exists() { - println!("\nThis node is already connected to an account"); + // Get home directory and check for prover-id file + let home_path = home::home_dir().expect("Failed to determine home directory"); + + //If the .nexus directory doesn't exist, we need to create it + let nexus_dir = home_path.join(".nexus"); + if !nexus_dir.exists() { + create_nexus_directory(&nexus_dir).expect("Failed to create .nexus directory"); + } + + //Check if the node-id file exists, use it. If not, create a new one. + let node_id_path = home_path.join(".nexus").join("node-id"); + let node_id = fs::read_to_string(&node_id_path).unwrap_or_default(); + + if node_id_path.exists() { + println!( + "\nThis node is already connected to an account using node id: {}", + node_id + ); //ask the user if they want to use the existing config println!("Do you want to use the existing user account? (y/n)"); @@ -73,19 +92,13 @@ pub async fn run_initial_setup() -> SetupResult { .unwrap(); let use_existing_config = use_existing_config.trim(); if use_existing_config == "y" { - match fs::read_to_string(&config_path) { - Ok(content) => match serde_json::from_str::(&content) { - Ok(user_config) => { - println!("\nUsing existing node ID: {}", user_config.node_id); - return SetupResult::Connected(user_config.node_id); - } - Err(e) => { - println!("{}", format!("Failed to parse config file: {}", e).red()); - return SetupResult::Invalid; - } - }, + match fs::read_to_string(&node_id_path) { + Ok(content) => { + println!("\nUsing existing node ID: {}", content.trim()); + return SetupResult::Connected(content.trim().to_string()); + } Err(e) => { - println!("{}", format!("Failed to read config file: {}", e).red()); + println!("{}", format!("Failed to read node-id file: {}", e).red()); return SetupResult::Invalid; } } @@ -142,12 +155,15 @@ pub async fn run_initial_setup() -> SetupResult { } pub fn clear_user_config() -> std::io::Result<()> { - let proj_dirs = - ProjectDirs::from("xyz", "nexus", "cli").expect("Failed to determine config directory"); - let config_path = proj_dirs.config_dir().join("user.json"); - if config_path.exists() { - fs::remove_file(config_path)?; + // Clear prover-id file + let home_path = home::home_dir().expect("Failed to determine home directory"); + let prover_id_path = home_path.join(".nexus").join("prover-id"); + if prover_id_path.exists() { + fs::remove_file(prover_id_path)?; + println!("Cleared prover ID configuration"); } + + println!("User configuration cleared"); Ok(()) }