diff --git a/psyche-book/src/enduser/quickstart-compute-provider.md b/psyche-book/src/enduser/quickstart-compute-provider.md index 9ae28317a..86907cdc3 100644 --- a/psyche-book/src/enduser/quickstart-compute-provider.md +++ b/psyche-book/src/enduser/quickstart-compute-provider.md @@ -8,7 +8,7 @@ Before starting, ensure you have: - [ ] Linux operating system (Ubuntu recommended) - [ ] NVIDIA GPU with sufficient VRAM for the model being trained -- [ ] The `run-manager` binary +- [ ] The `run-manager` binary, made executable if needed (`chmod +x ./run-manager`) - [ ] Run ID from the run administrator --- @@ -75,50 +75,23 @@ You should see the same GPU information as running `nvidia-smi` directly. --- -## Step 4: Install Solana CLI and Create Wallet - -### Install Solana CLI - -```bash -sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" -``` - -After installation, add Solana to your PATH by adding this line to your `~/.bashrc` or `~/.zshrc`: - -```bash -export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" -``` - -Then reload your shell: - -```bash -source ~/.bashrc # or source ~/.zshrc -``` - -Verify the installation: - -```bash -solana --version -``` - -For more details, see the [Solana installation docs](https://solana.com/docs/intro/installation). - -### Generate a Keypair +## Step 4: Create Wallet Create a new Solana keypair for your node: ```bash -solana-keygen new --outfile ~/.config/solana/psyche-node.json +./run-manager solana new-key ~/.config/solana/psyche-node.json ``` -You'll be prompted to set an optional passphrase. The keypair file will be created at the specified path. +The keypair file will be created at the specified path. **Important:** Back up this keypair file securely. If you lose it, you lose access to any rewards earned. -Get your public key (you'll need this): +This command will also output the public key for the keypair (you'll need this). +If you need to access the public key again later, you can run: ```bash -solana-keygen pubkey ~/.config/solana/psyche-node.json +./run-manager solana pubkey ~/.config/solana/psyche-node.json ``` --- @@ -133,33 +106,7 @@ NousNet runs are permissioned. To join, you need the run administrator to author --- -## Step 6: Fund Your Wallet (Devnet) - -Your wallet needs SOL to pay for transaction fees when communicating with the Solana blockchain. - -First, configure Solana CLI to use devnet: - -```bash -solana config set --url https://api.devnet.solana.com -``` - -Then request an airdrop from the devnet faucet: - -```bash -solana airdrop 2 ~/.config/solana/psyche-node.json -``` - -Verify your balance: - -```bash -solana balance ~/.config/solana/psyche-node.json -``` - -> **Note:** If the airdrop fails due to rate limiting, wait a few minutes and try again, or use the [Solana Faucet web interface](https://faucet.solana.com/). - ---- - -## Step 7: Create the Environment File +## Step 6: Create the Environment File Create a `.env` file with your configuration. This file tells the run-manager how to connect and authenticate. @@ -209,14 +156,28 @@ MICRO_BATCH_SIZE=4 --- -## Step 8: Run the Manager +## Step 7: Fund Your Wallet (Devnet) -Make the binary executable if needed: +Your wallet needs SOL to pay for transaction fees when communicating with the Solana blockchain. + +You can request an airdrop from the devnet faucet to your node's wallet: + +```bash +./run-manager solana airdrop 2 --env-file ~/.config/psyche/run.env +``` + +Verify your balance: ```bash -chmod +x ./run-manager +./run-manager solana balance --env-file ~/.config/psyche/run.env ``` +> **Note:** If the airdrop fails due to rate limiting, wait a few minutes and try again, or use the [Solana Faucet web interface](https://faucet.solana.com/). + +--- + +## Step 8: Join the network via the Run Manager + Open and enter a tmux window: ```bash @@ -401,11 +362,11 @@ The `AUTHORIZER` should be your master key's public key (the one authorized by t ## Quick Reference -| Command | Purpose | -| ------------------------------------------------------------- | --------------------------- | -| `nvidia-smi` | Verify GPU and drivers | -| `docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi` | Verify GPU access in Docker | -| `solana-keygen pubkey ~/.config/solana/psyche-node.json` | Get your public key | -| `solana balance ~/.config/solana/psyche-node.json` | Check wallet balance | -| `./run-manager --env-file ~/.config/psyche/run.env` | Start providing compute | -| `Ctrl+C` | Stop the client gracefully | +| Command | Purpose | +| ------------------------------------------------------------------ | --------------------------- | +| `nvidia-smi` | Verify GPU and drivers | +| `docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi` | Verify GPU access in Docker | +| `./run-manager solana pubkey ~/.config/solana/psyche-node.json` | Get your public key | +| `./run-manager solana balance --env-file ~/.config/psyche/run.env` | Check wallet balance | +| `./run-manager --env-file ~/.config/psyche/run.env` | Start providing compute | +| `Ctrl+C` | Stop the client gracefully | diff --git a/tools/rust-tools/run-manager/src/commands/mod.rs b/tools/rust-tools/run-manager/src/commands/mod.rs index 03b5f46c5..3e870bc34 100644 --- a/tools/rust-tools/run-manager/src/commands/mod.rs +++ b/tools/rust-tools/run-manager/src/commands/mod.rs @@ -4,5 +4,6 @@ pub mod authorization; pub mod can_join; pub mod run; pub mod treasury; +pub mod wallet; pub use command::Command; diff --git a/tools/rust-tools/run-manager/src/commands/wallet.rs b/tools/rust-tools/run-manager/src/commands/wallet.rs new file mode 100644 index 000000000..6842c8cfe --- /dev/null +++ b/tools/rust-tools/run-manager/src/commands/wallet.rs @@ -0,0 +1,167 @@ +use anchor_client::solana_sdk::{ + native_token::{lamports_to_sol, sol_to_lamports}, + signature::{EncodableKey, Keypair, Signer}, +}; +use anyhow::{Context, Result, bail}; +use clap::Subcommand; +use solana_client::rpc_client::RpcClient; +use std::path::PathBuf; + +#[derive(Subcommand, Debug)] +pub enum WalletCommands { + /// Display the pubkey from a keypair file + Pubkey { + /// Filepath to a keypair + keypair_path: PathBuf, + }, + /// Generate new keypair from a random seed phrase and write to a file + New { + /// Filepath to write the new keypair to + output_path: PathBuf, + }, + /// Request SOL from a faucet (won't work on mainnet) + Airdrop { + /// Amount of SOL to request + amount: f64, + /// Path to .env file containing RPC and WALLET_FILE + #[arg(long, required = true)] + env_file: PathBuf, + }, + /// Get the balance of the wallet + Balance { + /// Path to .env file containing RPC and WALLET_FILE + #[arg(long, required = true)] + env_file: PathBuf, + }, +} + +pub fn execute(command: WalletCommands) -> Result<()> { + match command { + WalletCommands::Pubkey { keypair_path } => { + let keypair = Keypair::read_from_file(&keypair_path).map_err(|e| { + anyhow::anyhow!("Failed to read keypair from {keypair_path:?}: {e}") + })?; + println!("{}", keypair.pubkey()); + Ok(()) + } + WalletCommands::New { output_path } => { + // this uses OsRng, which is a cryptographically secure RNG, + // so it's safe. + let keypair = Keypair::new(); + + if output_path.exists() { + bail!("keypair output path {output_path:?} exists, refusing to overwrite it.") + } + + keypair + .write_to_file(&output_path) + .map_err(|e| anyhow::anyhow!("Failed to write keypair to {output_path:?}: {e}"))?; + + println!("Wrote keypair to {output_path:?}"); + println!("pubkey: {}", keypair.pubkey()); + Ok(()) + } + WalletCommands::Airdrop { amount, env_file } => airdrop(amount, env_file), + WalletCommands::Balance { env_file } => check_balance(env_file), + } +} + +/// load an env file and extract the RPC URL and keypair +fn load_env_and_keypair(env_file: &PathBuf) -> Result<(String, Keypair)> { + crate::load_and_apply_env_file(env_file)?; + + let rpc_url = crate::get_env_var("RPC")?; + + let wallet_file = crate::get_env_var("WALLET_PRIVATE_KEY_PATH")?; + + // expand tilde in wallet path. we could use a crate like `dirs` to get the home dir more reliably, etc, + // but this will do for now. + let wallet_path = if wallet_file.starts_with("~") { + let home = std::env::var("HOME").map_err(|_| { + anyhow::anyhow!("wallet path contains ~, but HOME environment variable isn't set") + })?; + PathBuf::from(wallet_file.replacen("~", &home, 1)) + } else { + PathBuf::from(wallet_file) + }; + + let keypair = Keypair::read_from_file(&wallet_path) + .map_err(|e| anyhow::anyhow!("Failed to read keypair from {wallet_path:?}: {e}"))?; + + Ok((rpc_url, keypair)) +} + +fn airdrop(amount: f64, env_file: PathBuf) -> Result<()> { + let (rpc_url, keypair) = load_env_and_keypair(&env_file)?; + + let pubkey = keypair.pubkey(); + + let rpc_client = RpcClient::new(&rpc_url); + + let lamports = sol_to_lamports(amount); + + println!("Requesting airdrop of {amount} SOL to {pubkey} via RPC {rpc_url}"); + + let pre_balance = rpc_client + .get_balance(&pubkey) + .context("Failed to get balance before airdrop")?; + + let signature = rpc_client + .request_airdrop(&pubkey, lamports) + .context("Failed to request airdrop")?; + + println!("Airdrop requested. Waiting for confirmation..: {signature}"); + + let mut confirmed = false; + for _ in 0..30 { + print!("."); + std::thread::sleep(std::time::Duration::from_secs(1)); + if let Ok(status) = rpc_client.get_signature_statuses(&[signature]) { + if let Some(status) = &status.value[0] { + if status.err.is_none() { + confirmed = true; + break; + } + } + } + } + println!(); + + if !confirmed { + println!("Warning: Airdrop confirmation timed out"); + println!("Run `solana confirm {signature}` to check status"); + return Ok(()); + } + + let post_balance = rpc_client + .get_balance(&pubkey) + .context("Failed to get balance post-airdrop")?; + + if post_balance < pre_balance + lamports { + bail!("Balance unchanged. Run `solana confirm -v {signature}` to debug"); + } else { + let balance_sol = lamports_to_sol(post_balance); + println!("New balance: {balance_sol} SOL"); + } + + Ok(()) +} + +fn check_balance(env_file: PathBuf) -> Result<()> { + let (rpc_url, keypair) = load_env_and_keypair(&env_file)?; + + let pubkey = keypair.pubkey(); + + let rpc_client = RpcClient::new(rpc_url); + + let balance = rpc_client + .get_balance(&pubkey) + .context("Failed to get balance")?; + + let balance_sol = lamports_to_sol(balance); + + println!("pubkey {pubkey} has sol balance"); + println!("{balance_sol} SOL"); + + Ok(()) +} diff --git a/tools/rust-tools/run-manager/src/main.rs b/tools/rust-tools/run-manager/src/main.rs index 717234d9d..087ad5aab 100644 --- a/tools/rust-tools/run-manager/src/main.rs +++ b/tools/rust-tools/run-manager/src/main.rs @@ -26,6 +26,7 @@ use commands::run::{ CommandSetFutureEpochRates, CommandSetPaused, CommandTick, CommandUpdateConfig, }; use commands::treasury::{CommandTreasurerClaimRewards, CommandTreasurerTopUpRewards}; +use commands::wallet; const VERSION: &str = env!("CARGO_PKG_VERSION"); const GIT_HASH: &str = env!("GIT_HASH"); @@ -214,6 +215,12 @@ enum Commands { params: CommandCanJoin, }, + // Solana key management + Solana { + #[command(subcommand)] + command: wallet::WalletCommands, + }, + // Docs generation #[clap(hide = true)] PrintAllHelp { @@ -370,6 +377,7 @@ async fn async_main() -> Result<()> { Commands::CanJoin { cluster, params } => { params.execute(create_backend_readonly(cluster)?).await } + Commands::Solana { command } => wallet::execute(command), Commands::PrintAllHelp { markdown } => { assert!(markdown); clap_markdown::print_help_markdown::();