darkfi/validator/
pow.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    sync::{
21        atomic::{AtomicBool, AtomicU32, Ordering},
22        Arc,
23    },
24    thread,
25    time::Instant,
26};
27
28use darkfi_sdk::num_traits::{One, Zero};
29use num_bigint::BigUint;
30use randomx::{RandomXCache, RandomXDataset, RandomXFlags, RandomXVM};
31use smol::channel::Receiver;
32use tracing::{debug, error};
33
34use crate::{
35    blockchain::{
36        block_store::BlockDifficulty,
37        header_store::{
38            Header, HeaderHash,
39            PowData::{DarkFi, Monero},
40        },
41        Blockchain, BlockchainOverlayPtr,
42    },
43    util::{ringbuffer::RingBuffer, time::Timestamp},
44    validator::{utils::median, RandomXFactory},
45    Error, Result,
46};
47
48// Note: We have combined some constants for better performance.
49/// Amount of max items(blocks) to use for next difficulty calculation.
50/// Must be >= 2 and == BUF_SIZE - DIFFICULTY_LAG.
51const DIFFICULTY_WINDOW: usize = 720;
52/// Amount of latest blocks to exlude from the calculation.
53/// Our ring buffer has length: DIFFICULTY_WINDOW + DIFFICULTY_LAG,
54/// but we only use DIFFICULTY_WINDOW items in calculations.
55/// Must be == BUF_SIZE - DIFFICULTY_WINDOW.
56const _DIFFICULTY_LAG: usize = 15;
57/// Ring buffer length.
58/// Must be == DIFFICULTY_WINDOW + DIFFICULTY_LAG
59const BUF_SIZE: usize = 735;
60/// Used to calculate how many items to retain for next difficulty
61/// calculation. We are keeping the middle items, meaning cutting
62/// both from frond and back of the ring buffer, ending up with max
63/// DIFFICULTY_WINDOW - 2*DIFFICULTY_CUT items.
64/// (2*DIFFICULTY_CUT <= DIFFICULTY_WINDOW-2) must be true.
65const _DIFFICULTY_CUT: usize = 60;
66/// Max items to use for next difficulty calculation.
67/// Must be DIFFICULTY_WINDOW - 2 * DIFFICULTY_CUT
68const RETAINED: usize = 600;
69/// Already known cutoff start index for this config
70const CUT_BEGIN: usize = 60;
71/// Already known cutoff end index for this config
72const CUT_END: usize = 660;
73/// How many most recent blocks to use to verify new blocks' timestamp
74const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
75/// Time limit in the future of what blocks can be
76const BLOCK_FUTURE_TIME_LIMIT: Timestamp = Timestamp::from_u64(60 * 60 * 2);
77/// RandomX VM key changing height
78pub const RANDOMX_KEY_CHANGING_HEIGHT: u32 = 2048;
79/// RandomX VM key change delay
80pub const RANDOMX_KEY_CHANGE_DELAY: u32 = 64;
81
82/// This struct represents the information required by the PoW algorithm
83#[derive(Clone)]
84pub struct PoWModule {
85    /// Genesis block timestamp
86    pub genesis: Timestamp,
87    /// Target block time, in seconds
88    pub target: u32,
89    /// Optional fixed difficulty
90    pub fixed_difficulty: Option<BigUint>,
91    /// Latest block timestamps ringbuffer
92    pub timestamps: RingBuffer<Timestamp, BUF_SIZE>,
93    /// Latest block cumulative difficulties ringbuffer
94    pub difficulties: RingBuffer<BigUint, BUF_SIZE>,
95    /// Total blocks cumulative difficulty
96    /// Note: we keep this as a struct field for faster
97    /// access(optimization), since its always same as
98    /// difficulties buffer last.
99    pub cumulative_difficulty: BigUint,
100    /// Native PoW RandomX VMs current and next keys pair
101    pub darkfi_rx_keys: (HeaderHash, Option<HeaderHash>),
102    /// RandomXFactory for native PoW (Arc from parent)
103    pub darkfi_rx_factory: RandomXFactory,
104    /// RandomXFactory for Monero PoW (Arc from parent)
105    pub monero_rx_factory: RandomXFactory,
106}
107
108impl PoWModule {
109    // Initialize a new `PowModule` for provided target over provided `Blockchain`.
110    // Optionally, a fixed difficulty can be set and/or initialize before some height.
111    pub fn new(
112        blockchain: Blockchain,
113        target: u32,
114        fixed_difficulty: Option<BigUint>,
115        height: Option<u32>,
116    ) -> Result<Self> {
117        // Retrieve genesis block timestamp
118        let genesis = blockchain.genesis_block()?.header.timestamp;
119
120        // Retrieving last BUF_SIZE difficulties from blockchain to build the buffers
121        let mut timestamps = RingBuffer::<Timestamp, BUF_SIZE>::new();
122        let mut difficulties = RingBuffer::<BigUint, BUF_SIZE>::new();
123        let mut cumulative_difficulty = BigUint::zero();
124        let last_n = match height {
125            Some(h) => blockchain.blocks.get_difficulties_before(h, BUF_SIZE)?,
126            None => blockchain.blocks.get_last_n_difficulties(BUF_SIZE)?,
127        };
128        for difficulty in last_n {
129            timestamps.push(difficulty.timestamp);
130            difficulties.push(difficulty.cumulative_difficulty.clone());
131            cumulative_difficulty = difficulty.cumulative_difficulty;
132        }
133
134        // If a fixed difficulty has been set, assert its greater than zero
135        if let Some(diff) = &fixed_difficulty {
136            assert!(diff > &BigUint::zero());
137        }
138
139        // Retrieve current and next native PoW RandomX VM keys pair,
140        // and generate the RandomX factories.
141        let darkfi_rx_keys = blockchain.get_randomx_vm_keys(
142            &RANDOMX_KEY_CHANGING_HEIGHT,
143            &RANDOMX_KEY_CHANGE_DELAY,
144            height,
145        )?;
146        let darkfi_rx_factory = RandomXFactory::default();
147        let monero_rx_factory = RandomXFactory::default();
148
149        Ok(Self {
150            genesis,
151            target,
152            fixed_difficulty,
153            timestamps,
154            difficulties,
155            cumulative_difficulty,
156            darkfi_rx_keys,
157            darkfi_rx_factory,
158            monero_rx_factory,
159        })
160    }
161
162    /// Compute the next mining difficulty, based on current ring buffers.
163    /// If ring buffers contain 2 or less items, difficulty 1 is returned.
164    /// If a fixed difficulty has been set, this function will always
165    /// return that after first 2 difficulties.
166    pub fn next_difficulty(&self) -> Result<BigUint> {
167        // Retrieve first DIFFICULTY_WINDOW timestamps from the ring buffer
168        let mut timestamps: Vec<Timestamp> =
169            self.timestamps.iter().take(DIFFICULTY_WINDOW).cloned().collect();
170
171        // Check we have enough timestamps
172        let length = timestamps.len();
173        if length < 2 {
174            return Ok(BigUint::one())
175        }
176
177        // If a fixed difficulty has been set, return that
178        if let Some(diff) = &self.fixed_difficulty {
179            return Ok(diff.clone())
180        }
181
182        // Sort the timestamps vector
183        timestamps.sort_unstable();
184
185        // Grab cutoff indexes
186        let (cut_begin, cut_end) = self.cutoff(length)?;
187
188        // Calculate total time span
189        let cut_end = cut_end - 1;
190
191        let mut time_span = timestamps[cut_end].checked_sub(timestamps[cut_begin])?;
192        if time_span.inner() == 0 {
193            time_span = 1.into();
194        }
195
196        // Calculate total work done during this time span
197        let total_work = &self.difficulties[cut_end] - &self.difficulties[cut_begin];
198        if total_work <= BigUint::zero() {
199            return Err(Error::PoWTotalWorkIsZero)
200        }
201
202        // Compute next difficulty
203        let next_difficulty =
204            (total_work * self.target + time_span.inner() - BigUint::one()) / time_span.inner();
205
206        Ok(next_difficulty)
207    }
208
209    /// Calculate cutoff indexes.
210    /// If buffers have been filled, we return the
211    /// already known indexes, for performance.
212    fn cutoff(&self, length: usize) -> Result<(usize, usize)> {
213        if length >= DIFFICULTY_WINDOW {
214            return Ok((CUT_BEGIN, CUT_END))
215        }
216
217        let (cut_begin, cut_end) = if length <= RETAINED {
218            (0, length)
219        } else {
220            let cut_begin = (length - RETAINED).div_ceil(2);
221            (cut_begin, cut_begin + RETAINED)
222        };
223        // Sanity check
224        if
225        /* cut_begin < 0 || */
226        cut_begin + 2 > cut_end || cut_end > length {
227            return Err(Error::PoWCuttofCalculationError)
228        }
229
230        Ok((cut_begin, cut_end))
231    }
232
233    /// Compute the next mine target.
234    pub fn next_mine_target(&self) -> Result<BigUint> {
235        Ok(BigUint::from_bytes_le(&[0xFF; 32]) / &self.next_difficulty()?)
236    }
237
238    /// Compute the next mine target and difficulty.
239    pub fn next_mine_target_and_difficulty(&self) -> Result<(BigUint, BigUint)> {
240        let difficulty = self.next_difficulty()?;
241        let mine_target = BigUint::from_bytes_le(&[0xFF; 32]) / &difficulty;
242        Ok((mine_target, difficulty))
243    }
244
245    /// Verify provided difficulty corresponds to the next one.
246    pub fn verify_difficulty(&self, difficulty: &BigUint) -> Result<bool> {
247        Ok(difficulty == &self.next_difficulty()?)
248    }
249
250    /// Verify provided block timestamp is not far in the future and
251    /// check its valid acorrding to current timestamps median.
252    pub fn verify_current_timestamp(&self, timestamp: Timestamp) -> Result<bool> {
253        if timestamp > Timestamp::current_time().checked_add(BLOCK_FUTURE_TIME_LIMIT)? {
254            return Ok(false)
255        }
256
257        Ok(self.verify_timestamp_by_median(timestamp))
258    }
259
260    /// Verify provided block timestamp is valid and matches certain criteria.
261    pub fn verify_timestamp_by_median(&self, timestamp: Timestamp) -> bool {
262        // Check timestamp is after genesis one
263        if timestamp <= self.genesis {
264            return false
265        }
266
267        // If not enough blocks, no proper median yet, return true
268        if self.timestamps.len() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
269            return true
270        }
271
272        // Make sure the timestamp is higher or equal to the median
273        let timestamps = self
274            .timestamps
275            .iter()
276            .rev()
277            .take(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW)
278            .map(|x| x.inner())
279            .collect();
280
281        timestamp >= median(timestamps).into()
282    }
283
284    /// Verify provided block timestamp and hash.
285    pub fn verify_current_block(&self, header: &Header) -> Result<()> {
286        // First we verify the block's timestamp
287        if !self.verify_current_timestamp(header.timestamp)? {
288            return Err(Error::PoWInvalidTimestamp)
289        }
290
291        // Then we verify the block's hash
292        self.verify_block_hash(header)
293    }
294
295    /// Verify provided block hash is less than provided mine target.
296    pub fn verify_block_target(&self, header: &Header, target: &BigUint) -> Result<BigUint> {
297        let verifier_setup = Instant::now();
298
299        // Grab verifier output hash based on block PoW data
300        let (out_hash, verification_time) = match &header.pow_data {
301            DarkFi => {
302                // Check which VM key should be used.
303                // We only use the next key when the next block is the
304                // height changing one.
305                let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
306                    header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
307                {
308                    // Its safe to unwrap here since we know the key has been set
309                    &self.darkfi_rx_keys.1.unwrap()
310                } else {
311                    &self.darkfi_rx_keys.0
312                };
313
314                let vm = self.darkfi_rx_factory.create(&randomx_key.inner()[..])?;
315
316                debug!(
317                    target: "validator::pow::verify_block_target",
318                    "[VERIFIER] DarkFi PoW setup time: {:?}",
319                    verifier_setup.elapsed(),
320                );
321
322                let verification_time = Instant::now();
323                let out_hash = vm.calculate_hash(&header.to_block_hashing_blob())?;
324                (BigUint::from_bytes_le(&out_hash), verification_time)
325            }
326            Monero(powdata) => {
327                let vm = self.monero_rx_factory.create(powdata.randomx_key())?;
328
329                debug!(
330                    target: "validator::pow::verify_block_target",
331                    "[VERIFIER] Monero PoW setup time: {:?}",
332                    verifier_setup.elapsed(),
333                );
334
335                let verification_time = Instant::now();
336                let out_hash = vm.calculate_hash(&powdata.to_block_hashing_blob())?;
337                (BigUint::from_bytes_le(&out_hash), verification_time)
338            }
339        };
340        debug!(target: "validator::pow::verify_block_target", "[VERIFIER] Verification time: {:?}", verification_time.elapsed());
341
342        // Verify hash is less than the provided mine target
343        if out_hash > *target {
344            return Err(Error::PoWInvalidOutHash)
345        }
346
347        Ok(out_hash)
348    }
349
350    /// Verify provided block corresponds to next mine target.
351    pub fn verify_block_hash(&self, header: &Header) -> Result<()> {
352        // Grab the next mine target
353        let target = self.next_mine_target()?;
354
355        // Verify hash is less than the expected mine target
356        let _ = self.verify_block_target(header, &target)?;
357        Ok(())
358    }
359
360    /// Append provided header timestamp and difficulty to the ring
361    /// buffers, and check if we need to rotate and/or create the next
362    /// key RandomX VM in the native PoW factory.
363    pub fn append(&mut self, header: &Header, difficulty: &BigUint) -> Result<()> {
364        self.timestamps.push(header.timestamp);
365        self.cumulative_difficulty += difficulty;
366        self.difficulties.push(self.cumulative_difficulty.clone());
367
368        if header.height < RANDOMX_KEY_CHANGING_HEIGHT {
369            return Ok(())
370        }
371
372        // Check if need to set the new key
373        if header.height.is_multiple_of(RANDOMX_KEY_CHANGING_HEIGHT) {
374            let next_key = header.hash();
375            let _ = self.darkfi_rx_factory.create(&next_key.inner()[..])?;
376            self.darkfi_rx_keys.1 = Some(next_key);
377            return Ok(())
378        }
379
380        // Check if need to rotate keys
381        if header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY {
382            // Its safe to unwrap here since we know the key has been set
383            self.darkfi_rx_keys.0 = self.darkfi_rx_keys.1.unwrap();
384            self.darkfi_rx_keys.1 = None;
385        }
386
387        Ok(())
388    }
389
390    /// Append provided block difficulty to the ring buffers and insert
391    /// it to provided overlay.
392    pub fn append_difficulty(
393        &mut self,
394        overlay: &BlockchainOverlayPtr,
395        header: &Header,
396        difficulty: BlockDifficulty,
397    ) -> Result<()> {
398        self.append(header, &difficulty.difficulty)?;
399        overlay.lock().unwrap().blocks.insert_difficulty(&[difficulty])
400    }
401
402    /// Mine provided block, based on next mine target.
403    /// Note: this is used in tests not in actual mining.
404    pub fn mine_block(
405        &self,
406        header: &mut Header,
407        threads: usize,
408        stop_signal: &Receiver<()>,
409    ) -> Result<()> {
410        // Grab the RandomX key to use.
411        // We only use the next key when the next block is the
412        // height changing one.
413        let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
414            header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
415        {
416            // Its safe to unwrap here since we know the key has been set
417            &self.darkfi_rx_keys.1.unwrap()
418        } else {
419            &self.darkfi_rx_keys.0
420        };
421
422        // Generate the RandomX VMs for the key
423        let flags = get_mining_flags(false, false, false);
424        let vms = generate_mining_vms(flags, randomx_key, threads, stop_signal)?;
425
426        // Grab the next mine target
427        let target = self.next_mine_target()?;
428
429        // Mine the block
430        mine_block(&vms, &target, header, stop_signal)
431    }
432}
433
434impl std::fmt::Display for PoWModule {
435    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
436        write!(f, "PoWModule:")?;
437        write!(f, "\ttarget: {}", self.target)?;
438        write!(f, "\ttimestamps: {:?}", self.timestamps)?;
439        write!(f, "\tdifficulties: {:?}", self.difficulties)?;
440        write!(f, "\tcumulative_difficulty: {}", self.cumulative_difficulty)
441    }
442}
443
444/// Auxiliary function to define `RandomXFlags` used in mining.
445///
446/// Note: RandomX recommended flags will include `SSSE3` and `AVX2`
447/// extensions if CPU supports them.
448pub fn get_mining_flags(fast_mode: bool, large_pages: bool, secure: bool) -> RandomXFlags {
449    let mut flags = RandomXFlags::get_recommended_flags();
450    if fast_mode {
451        flags |= RandomXFlags::FULLMEM;
452    }
453    if large_pages {
454        flags |= RandomXFlags::LARGEPAGES;
455    }
456    if secure && flags.contains(RandomXFlags::JIT) {
457        flags |= RandomXFlags::SECURE;
458    }
459    flags
460}
461
462/// Auxiliary function to initialize a `RandomXDataset` using all
463/// available threads.
464fn init_dataset(
465    flags: RandomXFlags,
466    input: &HeaderHash,
467    stop_signal: &Receiver<()>,
468) -> Result<RandomXDataset> {
469    // Allocate cache and dataset
470    let cache = RandomXCache::new(flags, &input.inner()[..])?;
471    let dataset_item_count = RandomXDataset::count()?;
472    let dataset = RandomXDataset::new(flags, cache, dataset_item_count)?;
473
474    // Multithreaded dataset init using all available threads
475    let threads = thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
476    debug!(target: "validator::pow::init_dataset", "[MINER] Initializing RandomX dataset using {threads} threads...");
477    let mut handles = Vec::with_capacity(threads);
478    let threads_u32 = threads as u32;
479    let per_thread = dataset_item_count / threads_u32;
480    let remainder = dataset_item_count % threads_u32;
481    for t in 0..threads_u32 {
482        // Check if stop signal is received
483        if stop_signal.is_full() {
484            debug!(target: "validator::pow::init_dataset", "[MINER] Stop signal received, threads creation loop exiting");
485            break
486        }
487
488        let dataset = dataset.clone();
489        let start_item = t * per_thread;
490        let count = per_thread + if t == threads_u32 - 1 { remainder } else { 0 };
491        handles.push(thread::spawn(move || {
492            dataset.init(start_item, count);
493        }));
494    }
495
496    // Wait for threads to finish setup
497    for handle in handles {
498        let _ = handle.join();
499    }
500
501    // Check if stop signal is received
502    if stop_signal.is_full() {
503        debug!(target: "validator::pow::init_dataset", "[MINER] Stop signal received, exiting");
504        return Err(Error::MinerTaskStopped);
505    }
506    Ok(dataset)
507}
508
509/// Auxiliary function to generate mining VMs for provided RandomX key.
510pub fn generate_mining_vms(
511    flags: RandomXFlags,
512    input: &HeaderHash,
513    threads: usize,
514    stop_signal: &Receiver<()>,
515) -> Result<Vec<Arc<RandomXVM>>> {
516    debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX cache and dataset...");
517    debug!(target: "validator::pow::generate_mining_vms", "[MINER] PoW input: {input}");
518    let setup_start = Instant::now();
519    // Check if fast mode is enabled
520    let (cache, dataset) = if flags.contains(RandomXFlags::FULLMEM) {
521        // Initialize dataset
522        let dataset = init_dataset(flags, input, stop_signal)?;
523        (None, Some(dataset))
524    } else {
525        // Initialize cache for light mode
526        let cache = RandomXCache::new(flags, &input.inner()[..])?;
527        (Some(cache), None)
528    };
529    debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX cache and dataset: {:?}", setup_start.elapsed());
530
531    // Single thread mining VM
532    if threads == 1 {
533        debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX VM...");
534        let vm_start = Instant::now();
535        let vm = Arc::new(RandomXVM::new(flags, cache, dataset)?);
536        debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX VM in {:?}", vm_start.elapsed());
537        debug!(target: "validator::pow::generate_mining_vms", "[MINER] Setup time: {:?}", setup_start.elapsed());
538        return Ok(vec![vm])
539    }
540
541    // Multi thread mining VMs
542    debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing {threads} RandomX VMs...");
543    let mut vms = Vec::with_capacity(threads);
544    let threads_u32 = threads as u32;
545    for t in 0..threads_u32 {
546        // Check if stop signal is received
547        if stop_signal.is_full() {
548            debug!(target: "validator::pow::generate_mining_vms", "[MINER] Stop signal received, exiting");
549            return Err(Error::MinerTaskStopped);
550        }
551
552        debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX VM #{t}...");
553        let vm_start = Instant::now();
554        vms.push(Arc::new(RandomXVM::new(flags, cache.clone(), dataset.clone())?));
555        debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX VM #{t} in {:?}", vm_start.elapsed());
556    }
557    debug!(target: "validator::pow::generate_mining_vms", "[MINER] Setup time: {:?}", setup_start.elapsed());
558    Ok(vms)
559}
560
561/// Mine provided header, based on provided PoW module next mine target,
562/// using provided RandomX VMs setup.
563pub fn mine_block(
564    vms: &[Arc<RandomXVM>],
565    target: &BigUint,
566    header: &mut Header,
567    stop_signal: &Receiver<()>,
568) -> Result<()> {
569    debug!(target: "validator::pow::mine_block", "[MINER] Mine target: 0x{target:064x}");
570
571    // Check VMs were provided
572    if vms.is_empty() {
573        error!(target: "validator::pow::mine_block", "[MINER] No VMs were provided!");
574        return Err(Error::MinerTaskStopped)
575    }
576
577    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Initializing mining threads...");
578    let mut handles = Vec::with_capacity(vms.len());
579    let atomic_nonce = Arc::new(AtomicU32::new(0));
580    let found_header = Arc::new(AtomicBool::new(false));
581    let found_nonce = Arc::new(AtomicU32::new(0));
582    let threads = vms.len() as u32;
583    let mining_start = Instant::now();
584    for t in 0..threads {
585        // Check if stop signal is received
586        if stop_signal.is_full() {
587            debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, threads creation loop exiting");
588            break
589        }
590
591        if found_header.load(Ordering::SeqCst) {
592            debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header found, threads creation loop exiting");
593            break
594        }
595
596        let vm = vms[t as usize].clone();
597        let target = target.clone();
598        let mut thread_header = header.clone();
599        let atomic_nonce = atomic_nonce.clone();
600        let found_header = found_header.clone();
601        let found_nonce = found_nonce.clone();
602        let stop_signal = stop_signal.clone();
603
604        handles.push(thread::spawn(move || {
605            let mut last_nonce = atomic_nonce.fetch_add(1, Ordering::SeqCst);
606            thread_header.nonce = last_nonce;
607            if let Err(e) = vm.calculate_hash_first(thread_header.hash().inner()) {
608                error!(target: "validator::pow::randomx_vms_mine", "[MINER] Calculating hash in thread #{t} failed: {e}");
609                return
610            };
611            loop {
612                // Check if stop signal was received
613                if stop_signal.is_full() {
614                    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, thread #{t} exiting");
615                    break
616                }
617
618                if found_header.load(Ordering::SeqCst) {
619                    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header found, thread #{t} exiting");
620                    break;
621                }
622
623                thread_header.nonce = atomic_nonce.fetch_add(1, Ordering::SeqCst);
624                let out_hash = match vm.calculate_hash_next(thread_header.hash().inner()) {
625                    Ok(hash) => hash,
626                    Err(e) => {
627                        error!(target: "validator::pow::randomx_vms_mine", "[MINER] Calculating hash in thread #{t} failed: {e}");
628                        break
629                    }
630                };
631                let out_hash = BigUint::from_bytes_le(&out_hash);
632                if out_hash <= target {
633                    found_header.store(true, Ordering::SeqCst);
634                    thread_header.nonce = last_nonce; // Since out hash refers to previous run nonce
635                    found_nonce.store(thread_header.nonce, Ordering::SeqCst);
636                    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Thread #{t} found block header using nonce {}",
637                        thread_header.nonce
638                    );
639                    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header hash {}", thread_header.hash());
640                    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] RandomX output: 0x{out_hash:064x}");
641                    break;
642                }
643                last_nonce = thread_header.nonce;
644            }
645        }));
646    }
647
648    // Wait for threads to finish mining
649    for handle in handles {
650        let _ = handle.join();
651    }
652
653    // Check if stop signal is received
654    if stop_signal.is_full() {
655        debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, exiting");
656        return Err(Error::MinerTaskStopped);
657    }
658
659    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Completed mining in {:?}", mining_start.elapsed());
660    header.nonce = found_nonce.load(Ordering::SeqCst);
661    debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Mined header: {header:?}");
662    Ok(())
663}
664
665#[cfg(test)]
666mod tests {
667    use std::{
668        io::{BufRead, Cursor},
669        process::Command,
670    };
671
672    use darkfi_sdk::num_traits::Num;
673    use num_bigint::BigUint;
674    use sled_overlay::sled;
675
676    use crate::{
677        blockchain::{header_store::Header, BlockInfo, Blockchain},
678        Result,
679    };
680
681    use super::PoWModule;
682
683    const DEFAULT_TEST_THREADS: usize = 2;
684    const DEFAULT_TEST_DIFFICULTY_TARGET: u32 = 120;
685
686    #[test]
687    fn test_wide_difficulty() -> Result<()> {
688        let sled_db = sled::Config::new().temporary(true).open()?;
689        let blockchain = Blockchain::new(&sled_db)?;
690        let genesis_block = BlockInfo::default();
691        blockchain.add_block(&genesis_block)?;
692
693        let mut module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
694
695        let output = Command::new("./script/monero_gen_wide_data.py").output().unwrap();
696        let reader = Cursor::new(output.stdout);
697
698        let mut previous = genesis_block.header;
699        for (n, line) in reader.lines().enumerate() {
700            let line = line.unwrap();
701            let parts: Vec<String> = line.split(' ').map(|x| x.to_string()).collect();
702            assert!(parts.len() == 2);
703
704            let header = Header::new(
705                previous.hash(),
706                previous.height + 1,
707                0,
708                parts[0].parse::<u64>().unwrap().into(),
709            );
710            let difficulty = BigUint::from_str_radix(&parts[1], 10).unwrap();
711
712            let res = module.next_difficulty()?;
713
714            if res != difficulty {
715                eprintln!("Wrong wide difficulty for block {n}");
716                eprintln!("Expected: {difficulty}");
717                eprintln!("Found: {res}");
718                assert!(res == difficulty);
719            }
720
721            module.append(&header, &difficulty)?;
722            previous = header;
723        }
724
725        Ok(())
726    }
727
728    #[test]
729    fn test_miner_correctness() -> Result<()> {
730        // Default setup
731        let sled_db = sled::Config::new().temporary(true).open()?;
732        let blockchain = Blockchain::new(&sled_db)?;
733        let mut genesis_block = BlockInfo::default();
734        genesis_block.header.timestamp = 0.into();
735        blockchain.add_block(&genesis_block)?;
736
737        let module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
738
739        let (_, recvr) = smol::channel::bounded(1);
740
741        // Mine next block
742        let mut next_block = BlockInfo::default();
743        next_block.header.previous = genesis_block.hash();
744        module.mine_block(&mut next_block.header, DEFAULT_TEST_THREADS, &recvr)?;
745
746        // Verify it
747        module.verify_current_block(&next_block.header)?;
748
749        Ok(())
750    }
751}