stratum-server 3.0.0-beta-2

The server code for the Rust Stratum (v1) implementation
Documentation
use async_std::sync::{Arc, Mutex};
use chrono::{NaiveDateTime, Utc};
use extended_primitives::Buffer;
use log::warn;
use uuid::Uuid;

use crate::connection::MinerOptions;

//A miner is essentially an individual worker unit. There can be multiple Miners on a single
//connection which is why we needed to break it into these primitives.
#[derive(Debug, Clone)]
pub struct Miner {
    pub id: Uuid,
    pub sid: Buffer,
    pub client: Option<String>,
    pub name: Option<String>,
    pub stats: Arc<Mutex<MinerStats>>,
    pub job_stats: Arc<Mutex<JobStats>>,
    pub options: Arc<MinerOptions>,
    pub needs_ban: Arc<Mutex<bool>>,
}

impl Miner {
    //@todo going to need to add a difficulty to this guy.
    pub fn new(
        id: Uuid,
        client: Option<String>,
        name: Option<String>,
        sid: Buffer,
        options: Arc<MinerOptions>,
    ) -> Self {
        Miner {
            id,
            sid,
            client,
            name,
            stats: Arc::new(Mutex::new(MinerStats {
                accepted_shares: 0,
                rejected_shares: 0,
                last_active: Utc::now().naive_utc(),
            })),
            job_stats: Arc::new(Mutex::new(Default::default())),
            options,
            needs_ban: Arc::new(Mutex::new(false)),
        }
    }

    pub async fn ban(&self) {
        *self.needs_ban.lock().await = true;
        // self.disconnect().await;
    }

    pub async fn consider_ban(&self) {
        let accepted = self.stats.lock().await.accepted_shares;
        let rejected = self.stats.lock().await.rejected_shares;

        let total = accepted + rejected;

        //@todo come from options.
        let check_threshold = 500;
        let invalid_percent = 50.0;

        if total >= check_threshold {
            let percent_bad: f64 = (rejected as f64 / total as f64) * 100.0;

            if percent_bad < invalid_percent {
                //@todo make this possible. Reset stats to 0.
                // self.stats.lock().await = MinerStats::default();
            } else {
                warn!(
                    "Miner: {} banned. {} out of the last {} shares were invalid",
                    self.id, rejected, total
                );
                // self.ban().await; @todo
            }
        }
    }

    pub async fn valid_share(&self) {
        let mut stats = self.stats.lock().await;
        stats.accepted_shares += 1;
        stats.last_active = Utc::now().naive_utc();
        drop(stats);
        // self.consider_ban().await; @todo
        // @todo if we want to wrap this in an option, lets make it options.
        self.retarget().await;
    }

    pub async fn invalid_share(&self) {
        self.stats.lock().await.rejected_shares += 1;
        // self.consider_ban().await;
        //@todo see below
        //I don't think we want to retarget on invalid shares, but let's double check later.
        // self.retarget().await;
    }

    //@todo note, this only can be sent over ExMessage when it's hit a certain threshold.
    //@todo self.set_difficulty
    //@todo self.set_next_difficulty
    //@todo does this need to return a result? Ideally not, but if we send difficulty, then maybe.
    //@todo see if we can solve a lot of these recasting issues.
    async fn retarget(&self) {
        let now = Utc::now().naive_utc().timestamp();

        let mut job_stats = self.job_stats.lock().await;

        let since_last = now - job_stats.last_timestamp;

        job_stats.times.push(since_last);
        job_stats.last_timestamp = now;

        if now - job_stats.last_retarget < self.options.retarget_time {
            return;
        }

        let variance = self.options.target_time * (self.options.variance_percent as f64 / 100.0);
        let time_min = self.options.target_time - variance;
        let time_max = self.options.target_time + variance;
        job_stats.last_retarget = now;

        let avg: i64 = job_stats.times.iter().sum::<i64>() / job_stats.times.len() as i64;
        let mut d_dif = self.options.target_time / avg as f64;

        if avg as f64 > time_max && job_stats.current_difficulty > self.options.min_diff {
            //@note can speed up vardiff with this optional mode. Think about implementing.
            // if self.options.x2_mode {
            //     d_dif = 0.5;
            // }

            if d_dif * job_stats.current_difficulty < self.options.min_diff {
                d_dif = self.options.min_diff / job_stats.current_difficulty;
            }
        } else if (avg as f64) < time_min {
            //@note can speed up vardiff with this optional mode. Think about implementing.
            // if self.options.x2_mode {
            //     d_dif = 2.0;
            // }

            if d_dif * job_stats.current_difficulty > self.options.max_diff {
                d_dif = self.options.max_diff / job_stats.current_difficulty;
            }
        } else {
            return;
        }

        let new_diff = job_stats.current_difficulty * d_dif;

        //@todo
        // *self.next_difficulty.lock().await = Some(new_diff.floor());

        job_stats.times.clear();
    }
}

#[derive(Debug, Clone)]
pub struct MinerStats {
    accepted_shares: u64,
    rejected_shares: u64,
    last_active: NaiveDateTime,
}

//@todo probably move these over to types.
//@todo maybe rename this as vardiff stats.
#[derive(Debug, Default)]
pub struct JobStats {
    last_timestamp: i64,
    last_share_timestamp: i64,
    last_retarget: i64,
    times: Vec<i64>,
    current_difficulty: f64,
    job_difficulty: f64,
}
//@todo potential defaults here.
// job_stats: Arc::new(Mutex::new(JobStats {
//     last_timestamp: Utc::now().naive_utc().timestamp(),
//     last_share_timestamp: 0,
//     last_retarget: Utc::now().naive_utc().timestamp() - self.options.retarget_time / 2,
//     times: Vec::new(),
//     current_difficulty: self.difficulty.lock().await.clone(),
//     job_difficulty: 0.0,
// })),