├── .gitignore ├── README.md ├── Cargo.toml └── src ├── hashrate.rs ├── miner.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode/ 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uzi 2 | 3 | A RandomX CPU-miner for Ziesha cryptocurrency 4 | 5 | ## Usage 6 | 7 | ``` 8 | uzi-miner --node http://127.0.0.1:3030 --threads 32 9 | ``` 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uzi-miner" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | rust-randomx = "0.6.0" 8 | num_cpus = "1.0" 9 | rand = "0.8.5" 10 | hex = "0.4.3" 11 | thiserror = "1.0" 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | structopt = { version = "0.3", default-features = false } 15 | ureq = { version = "2.6.2", features = ["json"], default-features = false } 16 | colored = "2.0.0" 17 | log = "0.4" 18 | env_logger = "0.9.0" 19 | -------------------------------------------------------------------------------- /src/hashrate.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Default)] 2 | pub struct Hashrate(pub f32); 3 | 4 | impl std::fmt::Display for Hashrate { 5 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 6 | let mut hashrate = self.0; 7 | let mut unit = ""; 8 | let units = vec!["k", "M", "G", "T", "P", "E", "Z", "Y"]; 9 | for u in units.iter() { 10 | if hashrate > 1000.0 { 11 | hashrate /= 1000.0; 12 | unit = u; 13 | } else { 14 | break; 15 | } 16 | } 17 | write!(f, "{:.3} {}H/s", hashrate, unit) 18 | } 19 | } 20 | 21 | impl core::iter::Sum for Hashrate { 22 | fn sum>(iter: I) -> Hashrate { 23 | iter.fold(Hashrate(0.0), |a, b| Hashrate(a.0 + b.0)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/miner.rs: -------------------------------------------------------------------------------- 1 | use crate::hashrate::Hashrate; 2 | use rand::prelude::*; 3 | use rust_randomx::{Context, Difficulty, Hasher}; 4 | use std::sync::mpsc; 5 | use std::sync::Arc; 6 | use std::thread; 7 | use std::time::Instant; 8 | use thiserror::Error; 9 | 10 | #[derive(Error, Debug)] 11 | pub enum WorkerError { 12 | #[error("message send error")] 13 | MessageSendError(#[from] mpsc::SendError), 14 | #[error("solution send error")] 15 | SolutionSendError(#[from] mpsc::SendError), 16 | #[error("recv error")] 17 | RecvError(#[from] mpsc::RecvError), 18 | #[error("worker is terminated")] 19 | Terminated, 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | pub struct Solution { 24 | pub id: u32, 25 | pub nonce: Vec, 26 | } 27 | 28 | unsafe impl Send for Solution {} 29 | unsafe impl Sync for Solution {} 30 | 31 | #[derive(Clone)] 32 | pub struct Puzzle { 33 | pub id: u32, 34 | pub context: Arc, 35 | pub blob: Vec, 36 | pub offset: usize, 37 | pub count: usize, 38 | pub target: Difficulty, 39 | } 40 | 41 | #[derive(Clone)] 42 | pub enum Message { 43 | Puzzle(Puzzle), 44 | Break, 45 | Terminate, 46 | } 47 | 48 | unsafe impl Send for Puzzle {} 49 | unsafe impl Sync for Puzzle {} 50 | 51 | #[derive(Debug)] 52 | pub struct Worker { 53 | worker_id: u32, 54 | handle: Option>>, 55 | chan: mpsc::Sender, 56 | } 57 | 58 | const HASH_PER_ROUND: usize = 512; 59 | 60 | impl Worker { 61 | pub fn send(&self, msg: Message) -> Result<(), WorkerError> { 62 | if self.handle.is_some() { 63 | self.chan.send(msg)?; 64 | Ok(()) 65 | } else { 66 | Err(WorkerError::Terminated) 67 | } 68 | } 69 | pub fn terminate(&mut self) -> Result<(), WorkerError> { 70 | if let Some(handle) = self.handle.take() { 71 | log::info!("Terminating worker {}...", self.worker_id); 72 | self.chan.send(Message::Terminate)?; 73 | handle.join().unwrap() 74 | } else { 75 | Err(WorkerError::Terminated) 76 | } 77 | } 78 | pub fn new( 79 | worker_id: u32, 80 | callback: mpsc::Sender, 81 | hashrate_sender: mpsc::Sender<(u32, Hashrate)>, 82 | ) -> Self { 83 | let (msg_send, msg_recv) = mpsc::channel::(); 84 | let handle = thread::spawn(move || -> Result<(), WorkerError> { 85 | let mut rng = rand::thread_rng(); 86 | let mut msg = msg_recv.recv()?; 87 | 88 | loop { 89 | let mut puzzle = match msg.clone() { 90 | Message::Puzzle(puzzle) => puzzle, 91 | Message::Break => { 92 | msg = msg_recv.recv()?; 93 | continue; 94 | } 95 | Message::Terminate => { 96 | log::info!("Worker {} terminated!", worker_id); 97 | return Ok(()); 98 | } 99 | }; 100 | 101 | let mut counter = 0; 102 | 103 | let mut hasher = Hasher::new(Arc::clone(&puzzle.context)); 104 | 105 | let (b, e) = (puzzle.offset, puzzle.offset + puzzle.count); 106 | 107 | rng.fill_bytes(&mut puzzle.blob[b..e]); 108 | hasher.hash_first(&puzzle.blob); 109 | 110 | let mut start = Instant::now(); 111 | loop { 112 | let prev_nonce = puzzle.blob[b..e].to_vec(); 113 | 114 | rng.fill_bytes(&mut puzzle.blob[b..e]); 115 | let out = hasher.hash_next(&puzzle.blob); 116 | 117 | if out.meets_difficulty(puzzle.target) { 118 | callback.send(Solution { 119 | id: puzzle.id, 120 | nonce: prev_nonce, 121 | })?; 122 | } 123 | counter += 1; 124 | 125 | // Every HASH_PER_ROUND hashes, if there is a new message, cancel the current 126 | // puzzle and process the message. 127 | if counter >= HASH_PER_ROUND { 128 | if let Ok(new_msg) = msg_recv.try_recv() { 129 | msg = new_msg; 130 | break; 131 | } 132 | let elapsed = start.elapsed().as_millis() as f32 / 1000.0; 133 | hashrate_sender 134 | .send((worker_id, Hashrate(HASH_PER_ROUND as f32 / elapsed))) 135 | .unwrap_or_else(|err| println!("{:?}", err)); 136 | start = Instant::now(); 137 | counter = 0; 138 | } 139 | } 140 | hasher.hash_last(); 141 | } 142 | }); 143 | log::info!("Worker {} created!", worker_id); 144 | 145 | Self { 146 | worker_id, 147 | handle: Some(handle), 148 | chan: msg_send, 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::json; 4 | use std::error::Error; 5 | use std::net::SocketAddr; 6 | use std::sync::{Arc, Mutex}; 7 | use std::thread; 8 | use std::time::{Duration, Instant}; 9 | use structopt::StructOpt; 10 | 11 | use crate::hashrate::Hashrate; 12 | 13 | mod hashrate; 14 | mod miner; 15 | 16 | #[derive(Debug, StructOpt, Clone)] 17 | #[structopt(name = "Uzi Miner", about = "Mine Ziesha with Uzi!")] 18 | struct Opt { 19 | #[structopt(short = "t", long = "threads", default_value = "1")] 20 | threads: usize, 21 | 22 | #[structopt(short = "n", long = "node")] 23 | node: SocketAddr, 24 | 25 | #[structopt(long = "slow")] 26 | slow: bool, 27 | 28 | #[structopt(long = "pool")] 29 | pool: bool, 30 | 31 | #[structopt(long, default_value = "")] 32 | miner_token: String, 33 | } 34 | 35 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 36 | struct Request { 37 | key: String, 38 | blob: String, 39 | offset: usize, 40 | size: usize, 41 | target: u32, 42 | reward: u64, 43 | } 44 | 45 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 46 | struct RequestWrapper { 47 | puzzle: Option, 48 | } 49 | 50 | fn process_request( 51 | context: Arc>, 52 | req: RequestWrapper, 53 | opt: &Opt, 54 | sol_send: std::sync::mpsc::Sender, 55 | hash_send: std::sync::mpsc::Sender<(u32, hashrate::Hashrate)>, 56 | ) -> Result<(), Box> { 57 | let mut ctx = context.lock().unwrap(); 58 | ctx.current_puzzle = Some(req.clone()); 59 | 60 | if let Some(req) = req.puzzle { 61 | let power = rust_randomx::Difficulty::new(req.target).power(); 62 | println!( 63 | "{} Approximately {} hashes need to be calculated...", 64 | "Got new puzzle!".bright_yellow(), 65 | power 66 | ); 67 | 68 | // Reinitialize context if needed 69 | let req_key = hex::decode(&req.key)?; 70 | 71 | if ctx 72 | .hasher_context 73 | .as_ref() 74 | .map(|ctx| ctx.key() != req_key) 75 | .unwrap_or(true) 76 | { 77 | ctx.hasher_context = Some(Arc::new(rust_randomx::Context::new(&req_key, !opt.slow))); 78 | } 79 | 80 | // Ensure correct number of workers 81 | while ctx.workers.len() < opt.threads { 82 | let worker = miner::Worker::new(ctx.worker_id, sol_send.clone(), hash_send.clone()); 83 | ctx.workers.push(worker); 84 | ctx.worker_id += 1; 85 | } 86 | 87 | // Send the puzzle to workers 88 | let hash_ctx = Arc::clone(ctx.hasher_context.as_ref().unwrap()); 89 | let puzzle_id = ctx.puzzle_id; 90 | let blob = hex::decode(&req.blob)?; 91 | ctx.workers.retain(|w| { 92 | w.send(miner::Message::Puzzle(miner::Puzzle { 93 | id: puzzle_id, 94 | context: Arc::clone(&hash_ctx), 95 | blob: blob.clone(), 96 | offset: req.offset, 97 | count: req.size, 98 | target: rust_randomx::Difficulty::new(req.target), 99 | })) 100 | .is_ok() 101 | }); 102 | 103 | ctx.puzzle_id += 1; 104 | } else { 105 | println!( 106 | "{} Suspending the workers...", 107 | "No puzzles available!".bright_yellow() 108 | ); 109 | 110 | // Suspend all workers 111 | ctx.workers 112 | .retain(|w| w.send(miner::Message::Break).is_ok()); 113 | } 114 | Ok(()) 115 | } 116 | 117 | struct MinerContext { 118 | hasher_context: Option>, 119 | current_puzzle: Option, 120 | workers: Vec, 121 | puzzle_id: u32, 122 | worker_id: u32, 123 | } 124 | 125 | fn main() { 126 | println!( 127 | "{} v{} - RandomX CPU Miner for Ziesha Cryptocurrency", 128 | "Uzi-Miner!".bright_green(), 129 | env!("CARGO_PKG_VERSION") 130 | ); 131 | 132 | env_logger::init(); 133 | let opt = Opt::from_args(); 134 | let mut nw: usize = 0; 135 | 136 | let (sol_send, sol_recv) = std::sync::mpsc::channel::(); 137 | let (hash_send, hash_recv) = std::sync::mpsc::channel::<(u32, hashrate::Hashrate)>(); 138 | let context = Arc::new(Mutex::new(MinerContext { 139 | workers: Vec::new(), 140 | current_puzzle: None, 141 | hasher_context: None, 142 | puzzle_id: 0, 143 | worker_id: 0, 144 | })); 145 | 146 | let solution_getter = { 147 | let ctx = Arc::clone(&context); 148 | let opt = opt.clone(); 149 | thread::spawn(move || { 150 | for sol in sol_recv { 151 | if let Err(e) = || -> Result<(), Box> { 152 | println!("{}", "Solution found!".bright_green()); 153 | if !opt.pool { 154 | ctx.lock()? 155 | .workers 156 | .retain(|w| w.send(miner::Message::Break).is_ok()); 157 | } 158 | ureq::post(&format!("http://{}/miner/solution", opt.node)) 159 | .set("X-ZIESHA-MINER-TOKEN", &opt.miner_token) 160 | .send_json(json!({ "nonce": hex::encode(sol.nonce) }))?; 161 | Ok(()) 162 | }() { 163 | log::error!("Error: {}", e); 164 | } 165 | } 166 | }) 167 | }; 168 | 169 | let hashrate_getter = { 170 | const HASHRATE_REPORT_INTERVAL: f32 = 30.0; 171 | let opt = opt.clone(); 172 | thread::spawn(move || { 173 | let mut start = Instant::now(); 174 | let mut v = vec![Hashrate::default(); opt.threads]; 175 | for (worker_id, hash) in hash_recv { 176 | let duration: Duration = start.elapsed(); 177 | v[worker_id as usize] = hash; 178 | if duration.as_secs_f32() > HASHRATE_REPORT_INTERVAL { 179 | for (i, h) in v.iter().enumerate() { 180 | log::info!("Worker {} Hashrate = {}", format!("#{}", i).yellow(), h); 181 | } 182 | let total_rate = v.iter().cloned().sum::(); 183 | println!( 184 | "{} = {} ({})", 185 | "Total Hashrate".blue(), 186 | total_rate, 187 | format!("{} Workers", v.len()).yellow() 188 | ); 189 | start = Instant::now(); 190 | } 191 | } 192 | }) 193 | }; 194 | 195 | let puzzle_getter = { 196 | let ctx = Arc::clone(&context); 197 | let opt = opt.clone(); 198 | let sol_send = sol_send.clone(); 199 | thread::spawn(move || loop { 200 | if let Err(e) = || -> Result<(), Box> { 201 | let pzl = ureq::get(&format!("http://{}/miner/puzzle", opt.node)) 202 | .set("X-ZIESHA-MINER-TOKEN", &opt.miner_token) 203 | .call()? 204 | .into_string()?; 205 | 206 | let pzl_json: RequestWrapper = serde_json::from_str(&pzl)?; 207 | 208 | if ctx.lock()?.current_puzzle != Some(pzl_json.clone()) { 209 | process_request( 210 | ctx.clone(), 211 | pzl_json, 212 | &opt, 213 | sol_send.clone(), 214 | hash_send.clone(), 215 | )?; 216 | nw = ctx.lock()?.workers.len(); 217 | log::info!("nWorkers: {}", nw); 218 | } 219 | Ok(()) 220 | }() { 221 | log::error!("Error: {}", e); 222 | } 223 | std::thread::sleep(std::time::Duration::from_secs(1)); 224 | }) 225 | }; 226 | 227 | if let Ok(ctx) = Arc::try_unwrap(context) { 228 | for mut w in ctx.into_inner().unwrap().workers { 229 | w.terminate().unwrap(); 230 | } 231 | } 232 | drop(sol_send); 233 | solution_getter.join().unwrap(); 234 | hashrate_getter.join().unwrap(); 235 | puzzle_getter.join().unwrap(); 236 | } 237 | --------------------------------------------------------------------------------