#![deny(missing_docs)]
mod job;
mod pool;
mod settings;
mod solo;
mod state;
mod throttle;
mod worker;
pub use settings::{Persistence, Reply as ConsentReply, Settings, Status as ConsentStatus};
pub use state::{MiningState, ToggleResult, compile_env};
pub use throttle::Throttle;
use pool::PoolSource;
use solo::SoloSource;
use worker::Worker;
use std::{
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
mpsc,
},
thread,
time::{Duration, Instant},
};
#[derive(Clone)]
pub enum Source {
Solo {
node: String,
},
Pool {
url: String,
},
}
impl Source {
pub fn node(address: &str) -> Self {
Self::Solo {
node: address.into(),
}
}
pub fn pool(url: &str) -> Self {
Self::Pool { url: url.into() }
}
}
pub struct MinerBuilder {
sources: Vec<Source>,
wallet: String,
password: String,
threads: usize,
light: bool,
cpu_fraction: f32,
application_name: String,
consent_check: Option<Box<dyn FnOnce() -> ConsentReply + Send>>,
}
impl MinerBuilder {
pub fn sources(mut self, sources: &[Source]) -> Self {
self.sources = sources.to_vec();
self
}
pub fn wallet(mut self, wallet: &str) -> Self {
self.wallet = wallet.into();
self
}
pub fn password(mut self, password: &str) -> Self {
self.password = password.into();
self
}
pub fn threads(mut self, count: usize) -> Self {
self.threads = count;
self
}
pub fn light(mut self, enabled: bool) -> Self {
self.light = enabled;
self
}
pub fn cpu_fraction(mut self, fraction: f32) -> Self {
self.cpu_fraction = fraction;
self
}
pub fn application_name(mut self, name: &str) -> Self {
self.application_name = name.into();
self
}
pub fn consent_check(mut self, check: impl FnOnce() -> ConsentReply + Send + 'static) -> Self {
self.consent_check = Some(Box::new(check));
self
}
pub fn build(self) -> Miner {
let mut settings = Settings::new(&self.application_name);
let enabled = if settings.has_stored() {
settings.consent() == ConsentStatus::Granted
} else if let Some(check) = self.consent_check {
let reply = check();
settings.set_persistence(reply.persistence);
settings.set_consent(reply.consent);
reply.consent == ConsentStatus::Granted
} else {
false
};
let (thread_count, cpu_fraction) = if settings.has_stored() {
(settings.threads(), settings.cpu_fraction())
} else {
let threads = if self.threads == 0 {
thread::available_parallelism()
.map(std::num::NonZero::get)
.unwrap_or(1)
} else {
self.threads
};
(threads, self.cpu_fraction)
};
Miner {
sources: self.sources,
wallet: self.wallet,
password: self.password,
threads: thread_count,
light: self.light,
throttle: Throttle::new(cpu_fraction),
settings,
enabled,
handle: None,
running: Arc::new(AtomicBool::new(false)),
hash_count: Arc::new(AtomicU64::new(0)),
}
}
}
pub struct Miner {
sources: Vec<Source>,
wallet: String,
password: String,
threads: usize,
light: bool,
throttle: Throttle,
settings: Settings,
enabled: bool,
handle: Option<thread::JoinHandle<()>>,
running: Arc<AtomicBool>,
hash_count: Arc<AtomicU64>,
}
impl Miner {
pub fn builder() -> MinerBuilder {
MinerBuilder {
sources: Vec::new(),
wallet: String::new(),
password: "x".into(),
threads: 1,
light: true,
cpu_fraction: 0.25,
application_name: "opt-in-miner".into(),
consent_check: None,
}
}
pub fn start(&mut self) {
if !self.enabled || self.sources.is_empty() || self.running.load(Ordering::Relaxed) {
return;
}
let sources = self.sources.clone();
let wallet = self.wallet.clone();
let password = self.password.clone();
let threads = self.threads;
let light = self.light;
let throttle = self.throttle.clone();
let running = self.running.clone();
let hash_count = self.hash_count.clone();
running.store(true, Ordering::Relaxed);
hash_count.store(0, Ordering::Relaxed);
self.handle = Some(thread::spawn(move || {
run_mining_loop(
sources, wallet, password, threads, light, throttle, running, hash_count,
);
}));
}
pub fn stop(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
pub fn hash_count(&self) -> u64 {
self.hash_count.load(Ordering::Relaxed)
}
pub fn cpu_fraction(&self) -> f32 {
self.throttle.fraction()
}
pub fn threads(&self) -> usize {
self.threads
}
pub fn set_threads(&mut self, count: usize) {
self.threads = if count == 0 {
thread::available_parallelism()
.map(std::num::NonZero::get)
.unwrap_or(1)
} else {
count
};
self.settings.set_threads(self.threads);
if self.is_running() {
self.stop();
self.start();
}
}
pub fn set_cpu_fraction(&mut self, fraction: f32) {
self.throttle.set_fraction(fraction);
self.settings.set_cpu_fraction(fraction);
}
pub fn consent_status(&self) -> ConsentStatus {
self.settings.consent()
}
pub fn set_consent(&mut self, status: ConsentStatus) {
self.settings.set_consent(status);
self.enabled = status == ConsentStatus::Granted;
if !self.enabled {
self.stop();
}
}
pub fn persistence(&self) -> Persistence {
self.settings.persistence()
}
pub fn set_persistence(&mut self, persistence: Persistence) {
self.settings.set_persistence(persistence);
}
}
impl Drop for Miner {
fn drop(&mut self) {
self.stop();
}
}
fn run_mining_loop(
sources: Vec<Source>,
wallet: String,
password: String,
threads: usize,
light: bool,
throttle: Throttle,
running: Arc<AtomicBool>,
hash_count: Arc<AtomicU64>,
) {
let mut source_index = 0;
while running.load(Ordering::Relaxed) {
let source = &sources[source_index];
let result = match source {
Source::Pool { url } => run_pool(
url,
&wallet,
&password,
threads,
light,
&throttle,
&running,
&hash_count,
),
Source::Solo { node } => run_solo(
node,
&wallet,
threads,
light,
&throttle,
&running,
&hash_count,
),
};
if result.is_err() && running.load(Ordering::Relaxed) {
source_index = (source_index + 1) % sources.len();
thread::sleep(Duration::from_secs(5));
}
}
}
fn run_pool(
url: &str,
wallet: &str,
password: &str,
threads: usize,
light: bool,
throttle: &Throttle,
running: &Arc<AtomicBool>,
hash_count: &Arc<AtomicU64>,
) -> Result<(), ()> {
let (mut pool, initial_job) = PoolSource::login(url, wallet, password).map_err(|_| ())?;
let (share_sender, share_receiver) = mpsc::channel();
let worker = Worker::new(
threads,
light,
throttle.clone(),
share_sender,
hash_count.clone(),
);
worker.set_job(initial_job);
let mut last_keepalive = Instant::now();
let keepalive_interval = Duration::from_secs(60);
while running.load(Ordering::Relaxed) {
if let Some(job) = pool.try_receive_job() {
worker.set_job(job);
}
while let Ok(share) = share_receiver.try_recv() {
if pool
.submit(&share.job_id, &share.nonce_hex, &share.hash_hex)
.is_err()
{
worker.stop();
return Err(());
}
}
if last_keepalive.elapsed() >= keepalive_interval {
if pool.keepalive().is_err() {
worker.stop();
return Err(());
}
last_keepalive = Instant::now();
}
thread::sleep(Duration::from_millis(100));
}
worker.stop();
Ok(())
}
fn run_solo(
node: &str,
wallet: &str,
threads: usize,
light: bool,
throttle: &Throttle,
running: &Arc<AtomicBool>,
hash_count: &Arc<AtomicU64>,
) -> Result<(), ()> {
let mut source = SoloSource::new(node, wallet);
let initial_job = source.get_block_template().map_err(|_| ())?;
let (share_sender, share_receiver) = mpsc::channel();
let worker = Worker::new(
threads,
light,
throttle.clone(),
share_sender,
hash_count.clone(),
);
let mut current_job = initial_job.clone();
worker.set_job(initial_job);
let mut last_template_poll = Instant::now();
let template_poll_interval = Duration::from_secs(15);
while running.load(Ordering::Relaxed) {
if last_template_poll.elapsed() >= template_poll_interval {
if let Ok(new_job) = source.get_block_template() {
if new_job.id != current_job.id {
current_job = new_job.clone();
worker.set_job(new_job);
}
last_template_poll = Instant::now();
} else {
source.disconnect();
worker.stop();
return Err(());
}
}
while let Ok(share) = share_receiver.try_recv() {
if let Some(template) = ¤t_job.template_blob
&& share.job_id == current_job.id
{
let mut block = template.clone();
let nonce_offset = 39;
if block.len() > nonce_offset + 4 {
block[nonce_offset..nonce_offset + 4]
.copy_from_slice(&share.nonce_value.to_le_bytes());
let _ = source.submit_block(&block);
}
}
}
thread::sleep(Duration::from_millis(100));
}
worker.stop();
Ok(())
}