stratum_server/
miner.rs

1use crate::{
2    types::{
3        BanStats, ConnectionID, Difficulties, Difficulty, DifficultySettings, MinerStats,
4        VarDiffBuffer, VarDiffStats,
5    },
6    utils, ConfigManager, SessionID,
7};
8use parking_lot::Mutex;
9use std::sync::Arc;
10use tracing::warn;
11use uuid::Uuid;
12
13//A miner is essentially an individual worker unit. There can be multiple Miners on a single
14//connection which is why we needed to break it into these primitives.
15#[derive(Debug, Clone)]
16pub struct Miner {
17    config_manager: ConfigManager,
18    shared: Arc<Shared>,
19    inner: Arc<Inner>,
20}
21
22#[derive(Debug)]
23pub(crate) struct Inner {
24    pub(crate) worker_id: Uuid,
25    pub(crate) sid: SessionID,
26    pub(crate) connection_id: ConnectionID,
27    pub(crate) client: Option<String>,
28    pub(crate) name: Option<String>,
29}
30
31//@todo random reminder for myself here -> Would it be more efficient to wrap this entire struct in
32//a Mutex? My guess is no... But let's jsut re-review.
33#[derive(Debug)]
34pub(crate) struct Shared {
35    difficulties: Mutex<Difficulties>,
36    ban_stats: Mutex<BanStats>,
37    stats: Mutex<MinerStats>,
38    var_diff_stats: Mutex<VarDiffStats>,
39    difficulty_settings: Mutex<DifficultySettings>,
40}
41
42impl Miner {
43    #[must_use]
44    pub fn new(
45        connection_id: ConnectionID,
46        worker_id: Uuid,
47        sid: SessionID,
48        client: Option<String>,
49        name: Option<String>,
50        config_manager: ConfigManager,
51        difficulty: DifficultySettings,
52    ) -> Self {
53        let now = utils::now();
54
55        let shared = Shared {
56            difficulties: Mutex::new(Difficulties::new_only_current(difficulty.default)),
57            ban_stats: Mutex::new(BanStats {
58                last_ban_check_share: 0,
59                needs_ban: false,
60            }),
61            stats: Mutex::new(MinerStats {
62                accepted: 0,
63                stale: 0,
64                rejected: 0,
65                last_active: now,
66            }),
67            var_diff_stats: Mutex::new(VarDiffStats {
68                last_timestamp: now,
69                last_retarget: config_manager
70                    .difficulty_config()
71                    .initial_retarget_time(now),
72                vardiff_buf: VarDiffBuffer::new(),
73                last_retarget_share: 0,
74            }),
75            difficulty_settings: Mutex::new(difficulty),
76        };
77
78        let inner = Inner {
79            worker_id,
80            sid,
81            connection_id,
82            client,
83            name,
84        };
85
86        Miner {
87            config_manager,
88            shared: Arc::new(shared),
89            inner: Arc::new(inner),
90        }
91    }
92
93    pub(crate) fn ban(&self) {
94        //@todo I now set needs ban in consider ban so that I don't have to drop the Mutex. In
95        //here, we just really want to either contact the session, or disconnect the miner
96        // let mut ban_stats = self.shared.ban_stats.lock();
97        // ban_stats.needs_ban = true;
98
99        //@todo I think we need to disconnect here as well.
100        //@todo ok as far as I can tell, a ban will *not* lead to a miner being disconnected from
101        //the pool right now.
102        //
103        //Couple of thoughts.
104        //
105        //1. Let's go with the most complex scenario. Single connection with multiple miners. We
106        //   want to disconnect this miner ONLY - How we do that will be complex because we will
107        //   want the other miners in this connection to still work.
108        //
109        //2. If it is a single miner and single connection, I think this becomes a bit easier. We
110        //   probably need a check in the tcp loop where we see if there are still remaining miners
111        //   on a connection? Or we just send this individual miner a ban signal (tbd)
112    }
113
114    pub fn needs_ban(&self) -> bool {
115        self.shared.ban_stats.lock().needs_ban
116    }
117
118    pub fn consider_ban(&self) {
119        let stats = self.shared.stats.lock();
120        let mut ban_stats = self.shared.ban_stats.lock();
121
122        //@note this could possibly possibly possibly overflow - let's just think about that as we
123        //move forward.
124        //@todo I think this needs to be moved to like how retarget it used. Last ban check etc.
125        let total = stats.accepted + stats.stale + stats.rejected;
126
127        let config = &self.config_manager.connection_config();
128
129        if total - ban_stats.last_ban_check_share >= config.check_threshold {
130            let percent_bad: f64 = ((stats.stale + stats.rejected) as f64 / total as f64) * 100.0;
131
132            ban_stats.last_ban_check_share = total;
133
134            if percent_bad < config.invalid_percent {
135                //Does not need a ban
136                //@todo not sure if this is a good idea. Basically what we are saying is if the
137                //miner doesn't get banned in time, they can redeem themselves.
138                ban_stats.needs_ban = false;
139            } else {
140                warn!(
141                    id = ?self.inner.connection_id,
142                    worker_id = ?self.inner.worker_id,
143                    worker = ?self.inner.name,
144                    client = ?self.inner.client,
145                    "Miner banned. {} out of the last {} shares were invalid",
146                    stats.stale + stats.rejected,
147                    total
148                );
149                ban_stats.needs_ban = true;
150
151                self.ban();
152            }
153        }
154    }
155
156    #[must_use]
157    pub fn difficulties(&self) -> Difficulties {
158        self.shared.difficulties.lock().clone()
159    }
160
161    //@todo in the future have this accept difficulty, and then we could calculate hashrate here.
162    pub fn valid_share(&self) {
163        let mut stats = self.shared.stats.lock();
164        stats.accepted += 1;
165        stats.last_active = utils::now();
166
167        drop(stats);
168
169        self.consider_ban();
170
171        self.retarget();
172    }
173
174    pub fn stale_share(&self) {
175        let mut stats = self.shared.stats.lock();
176
177        stats.stale += 1;
178        stats.last_active = utils::now();
179
180        drop(stats);
181
182        self.consider_ban();
183
184        self.retarget();
185    }
186
187    pub fn rejected_share(&self) {
188        let mut stats = self.shared.stats.lock();
189
190        stats.rejected += 1;
191        stats.last_active = utils::now();
192
193        drop(stats);
194
195        self.consider_ban();
196
197        self.retarget();
198    }
199
200    fn retarget(&self) {
201        //This is in milliseconds
202        let now = utils::now();
203        let difficulty_config = self.config_manager.difficulty_config();
204
205        //@todo why not just store this as u128... Let's do that now.
206        let retarget_time = difficulty_config.retarget_time as u128 * 1000;
207        let retarget_share_amount = difficulty_config.retarget_share_amount;
208        //@todo see above, should we just store this as f64 * 1000.0?
209
210        let mut difficulties = self.shared.difficulties.lock();
211        let mut var_diff_stats = self.shared.var_diff_stats.lock();
212        let stats = self.shared.stats.lock();
213
214        let since_last = now - var_diff_stats.last_timestamp;
215
216        var_diff_stats.vardiff_buf.append(since_last);
217        var_diff_stats.last_timestamp = now;
218
219        //@todo add this as a function on miner stats please.
220        let total = stats.accepted + stats.rejected + stats.stale;
221
222        //This is the amoutn of shares we've added since the last retarget
223        let share_difference = total - var_diff_stats.last_retarget_share;
224        let time_difference = now - var_diff_stats.last_retarget;
225
226        if !((share_difference >= retarget_share_amount) || time_difference >= retarget_time) {
227            return;
228        }
229
230        var_diff_stats.last_retarget = now;
231        var_diff_stats.last_retarget_share = stats.accepted;
232
233        //This average is in milliseconds
234        let avg = var_diff_stats.vardiff_buf.avg();
235
236        if avg <= 0.0 {
237            return;
238        }
239
240        let mut new_diff;
241
242        let target_time = difficulty_config.target_time as f64 * 1000.0;
243
244        //@todo these variances should probs come from config.
245        if avg > target_time {
246            //@todo this needs to just be target_time since we multiplied it above.
247            if (avg / target_time) <= 1.5 {
248                return;
249            }
250            new_diff = difficulties.current().as_u64() / 2;
251        } else if (avg / target_time) >= 0.7 {
252            return;
253        } else {
254            new_diff = difficulties.current().as_u64() * 2;
255        }
256
257        new_diff = new_diff.clamp(
258            self.shared.difficulty_settings.lock().minimum.as_u64(),
259            difficulty_config.maximum_difficulty,
260        );
261
262        if new_diff != difficulties.current().as_u64() {
263            difficulties.update_next(Difficulty::from(new_diff));
264            var_diff_stats.vardiff_buf.reset();
265        }
266    }
267
268    #[must_use]
269    pub fn update_difficulty(&self) -> Option<Difficulty> {
270        let mut difficulties = self.shared.difficulties.lock();
271
272        difficulties.shift()
273    }
274
275    pub fn set_difficulty(&self, difficulty: Difficulty) {
276        let mut difficulties = self.shared.difficulties.lock();
277
278        difficulties.set_and_shift(difficulty);
279    }
280
281    #[must_use]
282    pub fn connection_id(&self) -> ConnectionID {
283        self.inner.connection_id.clone()
284    }
285
286    #[must_use]
287    pub fn worker_id(&self) -> Uuid {
288        self.inner.worker_id
289    }
290
291    #[must_use]
292    pub fn session_id(&self) -> SessionID {
293        self.inner.sid
294    }
295}
296
297#[cfg(test)]
298mod test {
299    use std::thread::sleep;
300
301    use super::*;
302    use crate::Config;
303
304    #[test]
305    fn test_valid_share() {
306        let connection_id = ConnectionID::new();
307        let worker_id = Uuid::new_v4();
308        let session_id = SessionID::from(1);
309
310        let config = Config::default();
311        let config_manager = ConfigManager::new(config.clone());
312
313        let diff_settings = DifficultySettings {
314            default: Difficulty::from(config.difficulty.initial_difficulty),
315            minimum: Difficulty::from(config.difficulty.minimum_difficulty),
316        };
317        let miner = Miner::new(
318            connection_id,
319            worker_id,
320            session_id,
321            None,
322            None,
323            config_manager,
324            diff_settings,
325        );
326
327        miner.valid_share();
328
329        for _ in 0..100 {
330            miner.valid_share();
331            sleep(std::time::Duration::from_millis(50));
332        }
333
334        let new_diff = miner.update_difficulty();
335        assert!(new_diff.is_some());
336
337        for _ in 0..100 {
338            miner.valid_share();
339        }
340
341        let new_diff = miner.update_difficulty();
342        assert!(new_diff.is_some());
343
344        //@todo we need some actual result here lol
345    }
346
347    #[test]
348    fn test_ban() {
349        let connection_id = ConnectionID::new();
350        let worker_id = Uuid::new_v4();
351        let session_id = SessionID::from(1);
352
353        let config = Config::default();
354        let config_manager = ConfigManager::new(config.clone());
355
356        let diff_settings = DifficultySettings {
357            default: Difficulty::from(config.difficulty.initial_difficulty),
358            minimum: Difficulty::from(config.difficulty.minimum_difficulty),
359        };
360        let miner = Miner::new(
361            connection_id,
362            worker_id,
363            session_id,
364            None,
365            None,
366            config_manager,
367            diff_settings,
368        );
369
370        miner.valid_share();
371
372        //Note Check threshold for miner bans is 500.
373        for _ in 0..500 {
374            miner.stale_share();
375        }
376
377        assert!(miner.needs_ban());
378    }
379
380    #[test]
381    fn test_retarget() {
382        let connection_id = ConnectionID::new();
383        let worker_id = Uuid::new_v4();
384        let session_id = SessionID::from(1);
385
386        let config = Config::default();
387        let config_manager = ConfigManager::new(config.clone());
388
389        let diff_settings = DifficultySettings {
390            default: Difficulty::from(config.difficulty.initial_difficulty),
391            minimum: Difficulty::from(config.difficulty.minimum_difficulty),
392        };
393
394        let miner = Miner::new(
395            connection_id,
396            worker_id,
397            session_id,
398            None,
399            None,
400            config_manager,
401            diff_settings,
402        );
403
404        // OK what do we need to test here....
405        // 1. We need to solve the issue with why difficulty is flucuating so much with the single
406        //    miner that is on the pool right now.
407        //
408        //    Scenario:
409        //    The current miner should be at roughly 120 TH/s
410        //    Which would equate to 120000000000000 (hashrate) = 4294967296 (multiplier) * effort /
411        //    time
412        //
413        //    For 1m (retarget interval) 120000000000000 * 60 / 4294967296 = effort for the whole
414        //    minute. So 1,676,380.6343078613. Cleaning that up, it's 1676360 If we divide that
415        //    across our various difficulty levels:
416        //
417        //    1676360 / 16384 ~= 102 shares per minute
418        //    1676360 / 32768 ~= 51 shares per minute
419        //    1676360 / 65536 ~= 25 shares per minute
420        //    1676360 / 131072  ~= 12.7  shares per minute
421        //    1676360 / 262144 ~=  6.4 shares per minute
422        //    1676360 / 524288 ~=  3.2 shares per minute
423        //
424
425        dbg!(miner.difficulties());
426
427        miner.valid_share();
428
429        for _ in 0..100 {
430            miner.valid_share();
431            sleep(std::time::Duration::from_millis(50));
432        }
433
434        dbg!(miner.difficulties());
435
436        let new_diff = miner.update_difficulty();
437        assert!(new_diff.is_some());
438
439        dbg!(miner.difficulties());
440
441        for _ in 0..100 {
442            miner.valid_share();
443        }
444
445        dbg!(miner.difficulties());
446
447        let new_diff = miner.update_difficulty();
448        assert!(new_diff.is_some());
449
450        dbg!(miner.difficulties());
451    }
452}