uhash-cli 0.5.1

UniversalHash proof-of-work miner for Bostrom blockchain
Documentation
mod commands;
use uhash::wallet;

use std::path::PathBuf;

use clap::{Parser, Subcommand, ValueEnum};
use serde::Serialize;

#[derive(Serialize)]
struct JsonError {
    error: String,
}

#[derive(Parser)]
#[command(name = "uhash")]
#[command(author = "Cyberia")]
#[command(version)]
#[command(about = "UniversalHash local compute CLI")]
struct Cli {
    #[command(subcommand)]
    command: Commands,

    /// Output in JSON format
    #[arg(long, global = true)]
    json: bool,

    /// Path to wallet keystore file
    #[arg(long, global = true)]
    wallet: Option<PathBuf>,
}

#[derive(Subcommand)]
enum Commands {
    /// Solve one local challenge and return first valid proof
    Prove {
        /// Challenge bytes encoded as hex
        #[arg(long)]
        challenge: String,
        /// Difficulty threshold in leading zero bits
        #[arg(long)]
        difficulty: u32,
        /// Starting nonce
        #[arg(long, default_value = "0")]
        start_nonce: u64,
        /// Max attempts before giving up
        #[arg(long, default_value = "1000000")]
        max_attempts: u64,
    },

    /// Continuous local mining over challenge
    Mine {
        /// Challenge bytes encoded as hex
        #[arg(long)]
        challenge: String,
        /// Difficulty threshold in leading zero bits
        #[arg(short, long)]
        difficulty: u32,
        /// Mining backend: auto | cpu | cuda | metal | opencl | wgpu
        #[arg(long, value_enum, default_value_t = MiningBackend::Auto)]
        backend: MiningBackend,
        /// CPU threads (cpu backend)
        #[arg(short, long)]
        threads: Option<usize>,
        /// Hashes per batch
        #[arg(long, default_value = "4096")]
        batch_size: usize,
        /// Max batches to run (0 = unlimited)
        #[arg(long, default_value = "0")]
        max_batches: u64,
        /// Stop after first found proof
        #[arg(long, default_value_t = false)]
        stop_on_proof: bool,
        /// Starting nonce
        #[arg(long, default_value = "0")]
        start_nonce: u64,
    },

    /// Verify a local proof tuple against challenge (and optional difficulty)
    Verify {
        /// Challenge bytes encoded as hex
        #[arg(long)]
        challenge: String,
        /// Nonce used for the proof
        #[arg(long)]
        nonce: u64,
        /// Expected 32-byte hash in hex
        #[arg(long)]
        hash: String,
        /// Optional difficulty threshold in leading zero bits
        #[arg(long)]
        difficulty: Option<u32>,
    },

    /// Inspect challenge/hash bytes (local utility)
    Inspect {
        /// Challenge bytes encoded as hex
        #[arg(long)]
        challenge: Option<String>,
        /// 32-byte hash encoded as hex
        #[arg(long)]
        hash: Option<String>,
    },

    /// Run performance benchmark
    Bench {
        /// Number of hashes to compute
        #[arg(short, long, default_value = "100")]
        count: u32,
        /// Benchmark backend: auto | cpu | cuda | metal | opencl | wgpu
        #[arg(long, value_enum, default_value_t = MiningBackend::Auto)]
        backend: MiningBackend,
        /// Batch/threads for benchmark (backend-specific semantics)
        #[arg(short, long)]
        threads: Option<usize>,
    },

    /// Manage Metal tuning cache (show/reset/retune)
    Tune {
        /// Remove cached tuning values for current Metal device
        #[arg(long)]
        reset: bool,
        /// Force runtime retune now (rebuilds cache)
        #[arg(long)]
        retune: bool,
    },

    /// Manage encrypted wallet (create, import, export)
    Wallet {
        #[command(subcommand)]
        command: commands::wallet::WalletCommand,
    },
}

#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
enum MiningBackend {
    Auto,
    Cpu,
    Cuda,
    Metal,
    Opencl,
    Wgpu,
}

impl MiningBackend {
    /// Fallback chain for Auto: Metal (macOS) → CUDA → OpenCL → WGPU → CPU
    fn auto_fallback_chain() -> &'static [MiningBackend] {
        if cfg!(target_os = "macos") {
            &[Self::Metal, Self::Cuda, Self::Opencl, Self::Wgpu, Self::Cpu]
        } else {
            &[Self::Cuda, Self::Opencl, Self::Wgpu, Self::Cpu]
        }
    }

    fn as_str(self) -> &'static str {
        match self {
            Self::Auto => "auto",
            Self::Cpu => "cpu",
            Self::Cuda => "cuda",
            Self::Metal => "metal",
            Self::Opencl => "opencl",
            Self::Wgpu => "wgpu",
        }
    }
}

fn try_init_cuda_backend() -> anyhow::Result<()> {
    #[cfg(feature = "gpu-cuda")]
    {
        let _ = cudarc::driver::CudaContext::new(0)
            .map_err(|e| anyhow::anyhow!("CUDA init failed: {}", e))?;
        Ok(())
    }
    #[cfg(not(feature = "gpu-cuda"))]
    {
        anyhow::bail!("CUDA backend not enabled in this build (rebuild with --features gpu-cuda)")
    }
}

fn try_init_metal_backend() -> anyhow::Result<()> {
    #[cfg(all(feature = "gpu-metal", target_os = "macos"))]
    {
        let _ = metal::Device::system_default()
            .ok_or_else(|| anyhow::anyhow!("No Metal device found"))?;
        Ok(())
    }
    #[cfg(not(all(feature = "gpu-metal", target_os = "macos")))]
    {
        anyhow::bail!(
            "Metal backend not available in this build/platform (macOS + --features gpu-metal required)"
        )
    }
}

fn try_init_opencl_backend() -> anyhow::Result<()> {
    #[cfg(feature = "gpu-opencl")]
    {
        let _ = uhash::opencl_solver::OpenClSolver::new()?;
        Ok(())
    }
    #[cfg(not(feature = "gpu-opencl"))]
    {
        anyhow::bail!(
            "OpenCL backend not enabled in this build (rebuild with --features gpu-opencl)"
        )
    }
}

fn try_init_wgpu_backend() -> anyhow::Result<()> {
    #[cfg(feature = "gpu-wgpu")]
    {
        let _ = uhash::wgpu_solver::WgpuSolver::new()?;
        Ok(())
    }
    #[cfg(not(feature = "gpu-wgpu"))]
    {
        anyhow::bail!("WGPU backend not enabled in this build (rebuild with --features gpu-wgpu)")
    }
}

fn try_init_backend(backend: MiningBackend) -> anyhow::Result<()> {
    match backend {
        MiningBackend::Cpu => Ok(()),
        MiningBackend::Cuda => try_init_cuda_backend(),
        MiningBackend::Metal => try_init_metal_backend(),
        MiningBackend::Opencl => try_init_opencl_backend(),
        MiningBackend::Wgpu => try_init_wgpu_backend(),
        MiningBackend::Auto => Ok(()),
    }
}

fn prepare_backend(
    requested: MiningBackend,
    json: bool,
) -> anyhow::Result<(MiningBackend, Option<String>)> {
    if requested == MiningBackend::Auto {
        // Try each backend in the fallback chain
        for &candidate in MiningBackend::auto_fallback_chain() {
            match try_init_backend(candidate) {
                Ok(()) => return Ok((candidate, None)),
                Err(e) => {
                    if !json {
                        eprintln!(
                            "Auto: {} unavailable ({}), trying next...",
                            candidate.as_str(),
                            e
                        );
                    }
                }
            }
        }
        // If nothing worked, fall back to CPU with a note
        let note = "Auto backend fallback: no GPU backends available, using cpu".to_string();
        if !json {
            eprintln!("Warning: {}", note);
        }
        return Ok((MiningBackend::Cpu, Some(note)));
    }

    // Explicit backend requested
    if requested == MiningBackend::Cpu {
        return Ok((MiningBackend::Cpu, None));
    }

    try_init_backend(requested)?;
    Ok((requested, None))
}

fn cmd_bench(
    count: u32,
    backend: MiningBackend,
    threads: Option<usize>,
    json: bool,
) -> anyhow::Result<()> {
    let (effective_backend, backend_note) = prepare_backend(backend, json)?;
    commands::bench::cmd_benchmark(count, effective_backend, backend_note, threads, json)
}

#[allow(clippy::too_many_arguments)]
fn cmd_mine(
    challenge_hex: &str,
    difficulty: u32,
    backend: MiningBackend,
    threads: Option<usize>,
    batch_size: usize,
    max_batches: u64,
    stop_on_proof: bool,
    start_nonce: u64,
    json: bool,
) -> anyhow::Result<()> {
    let (effective_backend, backend_note) = prepare_backend(backend, json)?;
    commands::mine::cmd_mine(
        challenge_hex,
        difficulty,
        effective_backend,
        backend_note,
        threads,
        batch_size,
        max_batches,
        stop_on_proof,
        start_nonce,
        json,
    )
}

fn cmd_tune(reset: bool, retune: bool, json: bool) -> anyhow::Result<()> {
    commands::tune::cmd_tune(reset, retune, json)
}

fn cmd_prove(
    challenge_hex: &str,
    difficulty: u32,
    start_nonce: u64,
    max_attempts: u64,
    json: bool,
) -> anyhow::Result<()> {
    commands::prove::cmd_prove(challenge_hex, difficulty, start_nonce, max_attempts, json)
}

fn cmd_verify(
    challenge_hex: &str,
    nonce: u64,
    hash_hex: &str,
    difficulty: Option<u32>,
    json: bool,
) -> anyhow::Result<()> {
    commands::prove::cmd_verify(challenge_hex, nonce, hash_hex, difficulty, json)
}

fn cmd_inspect(
    challenge_hex: Option<&str>,
    hash_hex: Option<&str>,
    json: bool,
) -> anyhow::Result<()> {
    commands::prove::cmd_inspect(challenge_hex, hash_hex, json)
}

fn main() {
    let cli = Cli::parse();
    let json = cli.json;

    let result = match cli.command {
        Commands::Prove {
            challenge,
            difficulty,
            start_nonce,
            max_attempts,
        } => cmd_prove(&challenge, difficulty, start_nonce, max_attempts, json),
        Commands::Mine {
            challenge,
            difficulty,
            backend,
            threads,
            batch_size,
            max_batches,
            stop_on_proof,
            start_nonce,
        } => cmd_mine(
            &challenge,
            difficulty,
            backend,
            threads,
            batch_size,
            max_batches,
            stop_on_proof,
            start_nonce,
            json,
        ),
        Commands::Verify {
            challenge,
            nonce,
            hash,
            difficulty,
        } => cmd_verify(&challenge, nonce, &hash, difficulty, json),
        Commands::Inspect { challenge, hash } => {
            cmd_inspect(challenge.as_deref(), hash.as_deref(), json)
        }
        Commands::Bench {
            count,
            backend,
            threads,
        } => cmd_bench(count, backend, threads, json),
        Commands::Tune { reset, retune } => cmd_tune(reset, retune, json),
        Commands::Wallet { command } => commands::wallet::cmd_wallet(command, cli.wallet, json),
    };

    if let Err(e) = result {
        if json {
            let err = JsonError {
                error: e.to_string(),
            };
            println!(
                "{}",
                serde_json::to_string(&err).unwrap_or_else(|_| "{}".to_string())
            );
        } else {
            eprintln!("Error: {}", e);
        }
        std::process::exit(1);
    }
}