1use 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::{
45 utils::{median, MAX_32_BYTES},
46 RandomXFactory,
47 },
48 Error, Result,
49};
50
51const DIFFICULTY_WINDOW: usize = 720;
55const _DIFFICULTY_LAG: usize = 15;
60const BUF_SIZE: usize = 735;
63const _DIFFICULTY_CUT: usize = 60;
69const RETAINED: usize = 600;
72const CUT_BEGIN: usize = 60;
74const CUT_END: usize = 660;
76const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
78const BLOCK_FUTURE_TIME_LIMIT: Timestamp = Timestamp::from_u64(60 * 60 * 2);
80pub const RANDOMX_KEY_CHANGING_HEIGHT: u32 = 2048;
82pub const RANDOMX_KEY_CHANGE_DELAY: u32 = 64;
84
85#[derive(Clone)]
88pub struct PoWModule {
89 pub genesis: Timestamp,
91 pub target: u32,
93 pub fixed_difficulty: Option<BigUint>,
95 pub timestamps: RingBuffer<Timestamp, BUF_SIZE>,
97 pub difficulties: RingBuffer<BigUint, BUF_SIZE>,
99 pub cumulative_difficulty: BigUint,
104 pub darkfi_rx_keys: (HeaderHash, Option<HeaderHash>),
106 pub darkfi_rx_factory: RandomXFactory,
108 pub monero_rx_factory: RandomXFactory,
110}
111
112impl PoWModule {
113 pub fn new(
117 blockchain: Blockchain,
118 target: u32,
119 fixed_difficulty: Option<BigUint>,
120 height: Option<u32>,
121 ) -> Result<Self> {
122 let genesis = blockchain.genesis_block()?.header.timestamp;
124
125 let mut timestamps = RingBuffer::<Timestamp, BUF_SIZE>::new();
128 let mut difficulties = RingBuffer::<BigUint, BUF_SIZE>::new();
129 let mut cumulative_difficulty = BigUint::zero();
130 let last_n = match height {
131 Some(h) => blockchain.blocks.get_difficulties_before(h, BUF_SIZE)?,
132 None => blockchain.blocks.get_last_n_difficulties(BUF_SIZE)?,
133 };
134 for difficulty in last_n {
135 timestamps.push(difficulty.timestamp);
136 difficulties.push(difficulty.cumulative_difficulty.clone());
137 cumulative_difficulty = difficulty.cumulative_difficulty;
138 }
139
140 if let Some(diff) = &fixed_difficulty {
143 assert!(diff > &BigUint::zero());
144 }
145
146 let darkfi_rx_keys = blockchain.get_randomx_vm_keys(
149 &RANDOMX_KEY_CHANGING_HEIGHT,
150 &RANDOMX_KEY_CHANGE_DELAY,
151 height,
152 )?;
153 let darkfi_rx_factory = RandomXFactory::default();
154 let monero_rx_factory = RandomXFactory::default();
155
156 Ok(Self {
157 genesis,
158 target,
159 fixed_difficulty,
160 timestamps,
161 difficulties,
162 cumulative_difficulty,
163 darkfi_rx_keys,
164 darkfi_rx_factory,
165 monero_rx_factory,
166 })
167 }
168
169 pub fn next_difficulty(&self) -> Result<BigUint> {
174 let mut timestamps: Vec<Timestamp> =
177 self.timestamps.iter().take(DIFFICULTY_WINDOW).cloned().collect();
178
179 let length = timestamps.len();
181 if length < 2 {
182 return Ok(BigUint::one())
183 }
184
185 if let Some(diff) = &self.fixed_difficulty {
187 return Ok(diff.clone())
188 }
189
190 timestamps.sort_unstable();
192
193 let (cut_begin, cut_end) = self.cutoff(length)?;
195
196 let cut_end = cut_end - 1;
198
199 let mut time_span = timestamps[cut_end].checked_sub(timestamps[cut_begin])?;
200 if time_span.inner() == 0 {
201 time_span = 1.into();
202 }
203
204 let total_work = &self.difficulties[cut_end] - &self.difficulties[cut_begin];
206 if total_work <= BigUint::zero() {
207 return Err(Error::PoWTotalWorkIsZero)
208 }
209
210 let next_difficulty =
212 (total_work * self.target + time_span.inner() - BigUint::one()) / time_span.inner();
213
214 Ok(next_difficulty)
215 }
216
217 fn cutoff(&self, length: usize) -> Result<(usize, usize)> {
221 if length >= DIFFICULTY_WINDOW {
222 return Ok((CUT_BEGIN, CUT_END))
223 }
224
225 let (cut_begin, cut_end) = if length <= RETAINED {
226 (0, length)
227 } else {
228 let cut_begin = (length - RETAINED).div_ceil(2);
229 (cut_begin, cut_begin + RETAINED)
230 };
231 if
233 cut_begin + 2 > cut_end || cut_end > length {
235 return Err(Error::PoWCuttofCalculationError)
236 }
237
238 Ok((cut_begin, cut_end))
239 }
240
241 pub fn next_mine_target(&self) -> Result<BigUint> {
243 Ok(&*MAX_32_BYTES / &self.next_difficulty()?)
244 }
245
246 pub fn next_mine_target_and_difficulty(&self) -> Result<(BigUint, BigUint)> {
248 let difficulty = self.next_difficulty()?;
249 let mine_target = &*MAX_32_BYTES / &difficulty;
250 Ok((mine_target, difficulty))
251 }
252
253 pub fn verify_difficulty(&self, difficulty: &BigUint) -> Result<bool> {
255 Ok(difficulty == &self.next_difficulty()?)
256 }
257
258 pub fn verify_current_timestamp(&self, timestamp: Timestamp) -> Result<bool> {
261 if timestamp > Timestamp::current_time().checked_add(BLOCK_FUTURE_TIME_LIMIT)? {
262 return Ok(false)
263 }
264
265 Ok(self.verify_timestamp_by_median(timestamp))
266 }
267
268 pub fn verify_timestamp_by_median(&self, timestamp: Timestamp) -> bool {
271 if timestamp <= self.genesis {
273 return false
274 }
275
276 if self.timestamps.len() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
278 return true
279 }
280
281 let timestamps = self
283 .timestamps
284 .iter()
285 .rev()
286 .take(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW)
287 .map(|x| x.inner())
288 .collect();
289
290 timestamp >= median(timestamps).into()
291 }
292
293 pub fn verify_current_block(&mut self, header: &Header) -> Result<()> {
295 if !self.verify_current_timestamp(header.timestamp)? {
297 return Err(Error::PoWInvalidTimestamp)
298 }
299
300 self.verify_block_hash(header)
302 }
303
304 pub fn calculate_hash(&mut self, header: &Header) -> Result<BigUint> {
306 let vm_setup = Instant::now();
308 let (pow_type, vm, blob) = match &header.pow_data {
309 DarkFi => {
310 let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
314 header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
315 {
316 &self
317 .darkfi_rx_keys
318 .1
319 .ok_or_else(|| Error::ParseFailed("darkfi_rx_keys.1 unwrap() error"))?
320 } else {
321 &self.darkfi_rx_keys.0
322 };
323
324 (
325 "DarkFi",
326 self.darkfi_rx_factory.create(&randomx_key.inner()[..])?,
327 header.to_block_hashing_blob(),
328 )
329 }
330 Monero(powdata) => (
331 "Monero",
332 self.monero_rx_factory.create(powdata.randomx_key())?,
333 powdata.to_block_hashing_blob(),
334 ),
335 };
336 debug!(target: "validator::pow::calculate_hash", "[VERIFIER] {pow_type} PoW setup time: {:?}", vm_setup.elapsed());
337
338 let compute_time = Instant::now();
340 let out_hash = vm.calculate_hash(&blob)?;
341 let out_hash = BigUint::from_bytes_le(&out_hash);
342 debug!(target: "validator::pow::calculate_hash", "[VERIFIER] Hash compute time: {:?}", compute_time.elapsed());
343
344 Ok(out_hash)
345 }
346
347 pub fn verify_block_target(&mut self, header: &Header, target: &BigUint) -> Result<BigUint> {
349 let out_hash = self.calculate_hash(header)?;
351
352 if out_hash > *target {
354 return Err(Error::PoWInvalidOutHash)
355 }
356
357 Ok(out_hash)
358 }
359
360 pub fn verify_block_hash(&mut self, header: &Header) -> Result<()> {
362 let target = self.next_mine_target()?;
364
365 let _ = self.verify_block_target(header, &target)?;
367 Ok(())
368 }
369
370 pub fn append(&mut self, header: &Header, difficulty: &BigUint) -> Result<()> {
374 self.timestamps.push(header.timestamp);
375 self.cumulative_difficulty += difficulty;
376 self.difficulties.push(self.cumulative_difficulty.clone());
377
378 if header.height < RANDOMX_KEY_CHANGING_HEIGHT {
379 return Ok(())
380 }
381
382 if header.height.is_multiple_of(RANDOMX_KEY_CHANGING_HEIGHT) {
384 let next_key = header.hash();
385 let _ = self.darkfi_rx_factory.create(&next_key.inner()[..])?;
386 self.darkfi_rx_keys.1 = Some(next_key);
387 return Ok(())
388 }
389
390 if header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY {
392 self.darkfi_rx_keys.0 = self
393 .darkfi_rx_keys
394 .1
395 .ok_or_else(|| Error::ParseFailed("darkfi_rx_keys.1 unwrap() error"))?;
396 self.darkfi_rx_keys.1 = None;
397 }
398
399 Ok(())
400 }
401
402 pub fn append_difficulty(
405 &mut self,
406 overlay: &BlockchainOverlayPtr,
407 header: &Header,
408 difficulty: BlockDifficulty,
409 ) -> Result<()> {
410 self.append(header, &difficulty.difficulty)?;
411 overlay.lock().unwrap().blocks.insert_difficulty(&[difficulty])
412 }
413
414 pub fn mine_block(
417 &self,
418 header: &mut Header,
419 threads: usize,
420 stop_signal: &Receiver<()>,
421 ) -> Result<()> {
422 let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
426 header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
427 {
428 &self
429 .darkfi_rx_keys
430 .1
431 .ok_or_else(|| Error::ParseFailed("darkfi_rx_keys.1 unwrap() error"))?
432 } else {
433 &self.darkfi_rx_keys.0
434 };
435
436 let flags = get_mining_flags(false, false, false);
438 let vms = generate_mining_vms(flags, randomx_key, threads, stop_signal)?;
439
440 let target = self.next_mine_target()?;
442
443 mine_block(&vms, &target, header, stop_signal)
445 }
446}
447
448impl std::fmt::Display for PoWModule {
449 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
450 write!(f, "PoWModule:")?;
451 write!(f, "\ttarget: {}", self.target)?;
452 write!(f, "\ttimestamps: {:?}", self.timestamps)?;
453 write!(f, "\tdifficulties: {:?}", self.difficulties)?;
454 write!(f, "\tcumulative_difficulty: {}", self.cumulative_difficulty)
455 }
456}
457
458pub fn get_mining_flags(fast_mode: bool, large_pages: bool, secure: bool) -> RandomXFlags {
463 let mut flags = RandomXFlags::get_recommended_flags();
464 if fast_mode {
465 flags |= RandomXFlags::FULLMEM;
466 }
467 if large_pages {
468 flags |= RandomXFlags::LARGEPAGES;
469 }
470 if secure && flags.contains(RandomXFlags::JIT) {
471 flags |= RandomXFlags::SECURE;
472 }
473 flags
474}
475
476fn init_dataset(
479 flags: RandomXFlags,
480 input: &HeaderHash,
481 stop_signal: &Receiver<()>,
482) -> Result<RandomXDataset> {
483 let cache = RandomXCache::new(flags, &input.inner()[..])?;
485 let dataset_item_count = RandomXDataset::count()?;
486 let dataset = RandomXDataset::new(flags, cache, dataset_item_count)?;
487
488 let threads = thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
490 debug!(target: "validator::pow::init_dataset", "[MINER] Initializing RandomX dataset using {threads} threads...");
491 let mut handles = Vec::with_capacity(threads);
492 let threads_u32 = threads as u32;
493 let per_thread = dataset_item_count / threads_u32;
494 let remainder = dataset_item_count % threads_u32;
495 for t in 0..threads_u32 {
496 if stop_signal.is_full() {
498 debug!(target: "validator::pow::init_dataset", "[MINER] Stop signal received, threads creation loop exiting");
499 break
500 }
501
502 let dataset = dataset.clone();
503 let start_item = t * per_thread;
504 let count = per_thread + if t == threads_u32 - 1 { remainder } else { 0 };
505 handles.push(thread::spawn(move || {
506 dataset.init(start_item, count);
507 }));
508 }
509
510 for handle in handles {
512 let _ = handle.join();
513 }
514
515 if stop_signal.is_full() {
517 debug!(target: "validator::pow::init_dataset", "[MINER] Stop signal received, exiting");
518 return Err(Error::MinerTaskStopped);
519 }
520 Ok(dataset)
521}
522
523pub fn generate_mining_vms(
525 flags: RandomXFlags,
526 input: &HeaderHash,
527 threads: usize,
528 stop_signal: &Receiver<()>,
529) -> Result<Vec<Arc<RandomXVM>>> {
530 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX cache and dataset...");
531 debug!(target: "validator::pow::generate_mining_vms", "[MINER] PoW input: {input}");
532 let setup_start = Instant::now();
533 let (cache, dataset) = if flags.contains(RandomXFlags::FULLMEM) {
535 let dataset = init_dataset(flags, input, stop_signal)?;
537 (None, Some(dataset))
538 } else {
539 let cache = RandomXCache::new(flags, &input.inner()[..])?;
541 (Some(cache), None)
542 };
543 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX cache and dataset: {:?}", setup_start.elapsed());
544
545 if threads == 1 {
547 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX VM...");
548 let vm_start = Instant::now();
549 let vm = Arc::new(RandomXVM::new(flags, cache, dataset)?);
550 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX VM in {:?}", vm_start.elapsed());
551 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Setup time: {:?}", setup_start.elapsed());
552 return Ok(vec![vm])
553 }
554
555 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing {threads} RandomX VMs...");
557 let mut vms = Vec::with_capacity(threads);
558 let threads_u32 = threads as u32;
559 for t in 0..threads_u32 {
560 if stop_signal.is_full() {
562 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Stop signal received, exiting");
563 return Err(Error::MinerTaskStopped);
564 }
565
566 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX VM #{t}...");
567 let vm_start = Instant::now();
568 vms.push(Arc::new(RandomXVM::new(flags, cache.clone(), dataset.clone())?));
569 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX VM #{t} in {:?}", vm_start.elapsed());
570 }
571 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Setup time: {:?}", setup_start.elapsed());
572 Ok(vms)
573}
574
575pub fn mine_block(
578 vms: &[Arc<RandomXVM>],
579 target: &BigUint,
580 header: &mut Header,
581 stop_signal: &Receiver<()>,
582) -> Result<()> {
583 debug!(target: "validator::pow::mine_block", "[MINER] Mine target: 0x{target:064x}");
584
585 if vms.is_empty() {
587 error!(target: "validator::pow::mine_block", "[MINER] No VMs were provided!");
588 return Err(Error::MinerTaskStopped)
589 }
590
591 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Initializing mining threads...");
592 let mut handles = Vec::with_capacity(vms.len());
593 let atomic_nonce = Arc::new(AtomicU32::new(0));
594 let found_header = Arc::new(AtomicBool::new(false));
595 let found_nonce = Arc::new(AtomicU32::new(0));
596 let threads = vms.len() as u32;
597 let mining_start = Instant::now();
598 for t in 0..threads {
599 if stop_signal.is_full() {
601 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, threads creation loop exiting");
602 break
603 }
604
605 if found_header.load(Ordering::SeqCst) {
606 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header found, threads creation loop exiting");
607 break
608 }
609
610 let vm = vms[t as usize].clone();
611 let target = target.clone();
612 let mut thread_header = header.clone();
613 let atomic_nonce = atomic_nonce.clone();
614 let found_header = found_header.clone();
615 let found_nonce = found_nonce.clone();
616 let stop_signal = stop_signal.clone();
617
618 handles.push(thread::spawn(move || {
619 let mut last_nonce = atomic_nonce.fetch_add(1, Ordering::SeqCst);
620 thread_header.nonce = last_nonce;
621 if let Err(e) = vm.calculate_hash_first(thread_header.hash().inner()) {
622 error!(target: "validator::pow::randomx_vms_mine", "[MINER] Calculating hash in thread #{t} failed: {e}");
623 return
624 };
625 loop {
626 if stop_signal.is_full() {
628 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, thread #{t} exiting");
629 break
630 }
631
632 if found_header.load(Ordering::SeqCst) {
633 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header found, thread #{t} exiting");
634 break;
635 }
636
637 thread_header.nonce = atomic_nonce.fetch_add(1, Ordering::SeqCst);
638 let out_hash = match vm.calculate_hash_next(thread_header.hash().inner()) {
639 Ok(hash) => hash,
640 Err(e) => {
641 error!(target: "validator::pow::randomx_vms_mine", "[MINER] Calculating hash in thread #{t} failed: {e}");
642 break
643 }
644 };
645 let out_hash = BigUint::from_bytes_le(&out_hash);
646 if out_hash <= target {
647 found_header.store(true, Ordering::SeqCst);
648 thread_header.nonce = last_nonce;
650 found_nonce.store(thread_header.nonce, Ordering::SeqCst);
651 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Thread #{t} found block header using nonce {}",
652 thread_header.nonce
653 );
654 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header hash {}", thread_header.hash());
655 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] RandomX output: 0x{out_hash:064x}");
656 break;
657 }
658 last_nonce = thread_header.nonce;
659 }
660 }));
661 }
662
663 for handle in handles {
665 let _ = handle.join();
666 }
667
668 if stop_signal.is_full() {
670 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, exiting");
671 return Err(Error::MinerTaskStopped);
672 }
673
674 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Completed mining in {:?}", mining_start.elapsed());
675 header.nonce = found_nonce.load(Ordering::SeqCst);
676 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Mined header: {header:?}");
677 Ok(())
678}
679
680#[cfg(test)]
681mod tests {
682 use std::{
683 io::{BufRead, Cursor},
684 process::Command,
685 };
686
687 use darkfi_sdk::num_traits::Num;
688 use num_bigint::BigUint;
689 use sled_overlay::sled;
690
691 use crate::{
692 blockchain::{header_store::Header, BlockInfo, Blockchain},
693 Result,
694 };
695
696 use super::PoWModule;
697
698 const DEFAULT_TEST_THREADS: usize = 2;
699 const DEFAULT_TEST_DIFFICULTY_TARGET: u32 = 120;
700
701 #[test]
702 fn test_wide_difficulty() -> Result<()> {
703 let sled_db = sled::Config::new().temporary(true).open()?;
704 let blockchain = Blockchain::new(&sled_db)?;
705 let genesis_block = BlockInfo::default();
706 blockchain.add_block(&genesis_block)?;
707
708 let mut module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
709
710 let output = Command::new("./script/monero_gen_wide_data.py").output().unwrap();
711 let reader = Cursor::new(output.stdout);
712
713 let mut previous = genesis_block.header;
714 for (n, line) in reader.lines().enumerate() {
715 let line = line.unwrap();
716 let parts: Vec<String> = line.split(' ').map(|x| x.to_string()).collect();
717 assert!(parts.len() == 2);
718
719 let header = Header::new(
720 previous.hash(),
721 previous.height + 1,
722 0,
723 parts[0].parse::<u64>().unwrap().into(),
724 );
725 let difficulty = BigUint::from_str_radix(&parts[1], 10).unwrap();
726
727 let res = module.next_difficulty()?;
728
729 if res != difficulty {
730 eprintln!("Wrong wide difficulty for block {n}");
731 eprintln!("Expected: {difficulty}");
732 eprintln!("Found: {res}");
733 assert!(res == difficulty);
734 }
735
736 module.append(&header, &difficulty)?;
737 previous = header;
738 }
739
740 Ok(())
741 }
742
743 #[test]
744 fn test_miner_correctness() -> Result<()> {
745 let sled_db = sled::Config::new().temporary(true).open()?;
747 let blockchain = Blockchain::new(&sled_db)?;
748 let mut genesis_block = BlockInfo::default();
749 genesis_block.header.timestamp = 0.into();
750 blockchain.add_block(&genesis_block)?;
751
752 let mut module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
753
754 let (_, recvr) = smol::channel::bounded(1);
755
756 let mut next_block = BlockInfo::default();
758 next_block.header.previous = genesis_block.hash();
759 module.mine_block(&mut next_block.header, DEFAULT_TEST_THREADS, &recvr)?;
760
761 module.verify_current_block(&next_block.header)?;
763
764 Ok(())
765 }
766}