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(())
}