uhash-cli 0.5.1

UniversalHash proof-of-work miner for Bostrom blockchain
Documentation
use serde::Serialize;
use std::sync::Arc;
use std::time::Instant;
use uhash_prover::cpu::ParallelCpuSolver;
use uhash_prover::Solver;

#[cfg(feature = "gpu-cuda")]
use uhash::cuda_miner;
#[cfg(all(feature = "gpu-metal", target_os = "macos"))]
use uhash::metal_miner;
#[cfg(feature = "gpu-opencl")]
use uhash::opencl_solver;
#[cfg(feature = "gpu-wgpu")]
use uhash::wgpu_solver;

use crate::MiningBackend;

#[derive(Serialize)]
struct JsonMineFound {
    event: &'static str,
    nonce: u64,
    hash: String,
    hashes_total: u64,
    elapsed_s: f64,
    hashrate: f64,
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn cmd_mine(
    challenge_hex: &str,
    difficulty: u32,
    backend: MiningBackend,
    backend_note: Option<String>,
    threads: Option<usize>,
    batch_size: usize,
    max_batches: u64,
    stop_on_proof: bool,
    start_nonce: u64,
    json: bool,
) -> anyhow::Result<()> {
    let challenge =
        hex::decode(challenge_hex).map_err(|e| anyhow::anyhow!("Invalid challenge hex: {}", e))?;
    if challenge.is_empty() {
        anyhow::bail!("challenge must not be empty");
    }
    if batch_size == 0 {
        anyhow::bail!("batch-size must be > 0");
    }

    let challenge = Arc::new(challenge);
    let start = Instant::now();
    let mut next_nonce = start_nonce;
    let mut total_hashes: u64 = 0;
    let mut batches_done: u64 = 0;
    let mut proofs_found: u64 = 0;
    let mut last_report = Instant::now();

    if !json {
        println!("=== UniversalHash Local Miner ===");
        println!("Backend: {}", backend.as_str());
        println!("Difficulty: {} bits", difficulty);
        println!("Batch size: {}", batch_size);
        if let Some(note) = backend_note {
            eprintln!("Warning: {}", note);
        }
        println!("===============================");
    }

    let worker_threads = threads.filter(|t| *t > 0).unwrap_or_else(num_cpus::get);
    let mut cpu_solver = ParallelCpuSolver::new(worker_threads);

    #[cfg(feature = "gpu-cuda")]
    let mut cuda = if backend == MiningBackend::Cuda {
        Some(cuda_miner::CudaMiner::new()?)
    } else {
        None
    };
    #[cfg(all(feature = "gpu-metal", target_os = "macos"))]
    let mut metal = if backend == MiningBackend::Metal {
        Some(metal_miner::MetalMiner::new()?)
    } else {
        None
    };
    #[cfg(feature = "gpu-opencl")]
    let mut opencl = if backend == MiningBackend::Opencl {
        Some(opencl_solver::OpenClSolver::new()?)
    } else {
        None
    };
    #[cfg(feature = "gpu-wgpu")]
    let mut wgpu_backend = if backend == MiningBackend::Wgpu {
        Some(wgpu_solver::WgpuSolver::new()?)
    } else {
        None
    };

    loop {
        if max_batches > 0 && batches_done >= max_batches {
            break;
        }

        let (proof, actual) = match backend {
            MiningBackend::Cpu => {
                cpu_solver.find_proof_batch(&challenge, next_nonce, batch_size, difficulty)?
            }
            MiningBackend::Cuda => {
                #[cfg(feature = "gpu-cuda")]
                {
                    let miner = cuda.as_mut().expect("cuda miner initialized");
                    Solver::find_proof_batch(miner, &challenge, next_nonce, batch_size, difficulty)?
                }
                #[cfg(not(feature = "gpu-cuda"))]
                {
                    anyhow::bail!("cuda backend requires --features gpu-cuda");
                }
            }
            MiningBackend::Metal => {
                #[cfg(all(feature = "gpu-metal", target_os = "macos"))]
                {
                    let miner = metal.as_mut().expect("metal miner initialized");
                    Solver::find_proof_batch(miner, &challenge, next_nonce, batch_size, difficulty)?
                }
                #[cfg(not(all(feature = "gpu-metal", target_os = "macos")))]
                {
                    anyhow::bail!("metal backend requires macOS + --features gpu-metal");
                }
            }
            MiningBackend::Opencl => {
                #[cfg(feature = "gpu-opencl")]
                {
                    let miner = opencl.as_mut().expect("opencl miner initialized");
                    Solver::find_proof_batch(miner, &challenge, next_nonce, batch_size, difficulty)?
                }
                #[cfg(not(feature = "gpu-opencl"))]
                {
                    anyhow::bail!("opencl backend requires --features gpu-opencl");
                }
            }
            MiningBackend::Wgpu => {
                #[cfg(feature = "gpu-wgpu")]
                {
                    let miner = wgpu_backend.as_mut().expect("wgpu miner initialized");
                    Solver::find_proof_batch(miner, &challenge, next_nonce, batch_size, difficulty)?
                }
                #[cfg(not(feature = "gpu-wgpu"))]
                {
                    anyhow::bail!("wgpu backend requires --features gpu-wgpu");
                }
            }
            MiningBackend::Auto => unreachable!("backend is resolved before cmd_mine"),
        };

        batches_done = batches_done.saturating_add(1);
        total_hashes = total_hashes.saturating_add(actual as u64);

        if let Some((nonce, hash)) = proof {
            proofs_found = proofs_found.saturating_add(1);
            let elapsed_s = start.elapsed().as_secs_f64().max(1e-9);
            let hps = total_hashes as f64 / elapsed_s;
            if json {
                let out = JsonMineFound {
                    event: "proof_found",
                    nonce,
                    hash: hex::encode(hash),
                    hashes_total: total_hashes,
                    elapsed_s,
                    hashrate: hps,
                };
                println!("{}", serde_json::to_string(&out)?);
            } else {
                println!("\nFound proof #{}:", proofs_found);
                println!("  Nonce: {}", nonce);
                println!("  Hash: {}", hex::encode(hash));
                println!("  Hashes: {} ({:.2} H/s)", total_hashes, hps);
            }
            if stop_on_proof {
                break;
            }
        }

        next_nonce = next_nonce.saturating_add(batch_size as u64);

        if !json && last_report.elapsed().as_secs() >= 1 {
            let elapsed_s = start.elapsed().as_secs_f64().max(1e-9);
            let hps = total_hashes as f64 / elapsed_s;
            print!(
                "\rHashrate: {:.0} H/s | Hashes: {} | Batches: {} | Proofs: {}",
                hps, total_hashes, batches_done, proofs_found
            );
            let _ = std::io::Write::flush(&mut std::io::stdout());
            last_report = Instant::now();
        }
    }

    if !json {
        let elapsed_s = start.elapsed().as_secs_f64().max(1e-9);
        let hps = total_hashes as f64 / elapsed_s;
        println!(
            "\nDone. total_hashes={} proofs={} elapsed={:.2}s hashrate={:.2} H/s",
            total_hashes, proofs_found, elapsed_s, hps
        );
    }

    Ok(())
}