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#[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#[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 }
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 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 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 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 let now = utils::now();
203 let difficulty_config = self.config_manager.difficulty_config();
204
205 let retarget_time = difficulty_config.retarget_time as u128 * 1000;
207 let retarget_share_amount = difficulty_config.retarget_share_amount;
208 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 let total = stats.accepted + stats.rejected + stats.stale;
221
222 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 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 if avg > target_time {
246 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 }
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 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 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}