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::{utils::median, RandomXFactory},
45 Error, Result,
46};
47
48const DIFFICULTY_WINDOW: usize = 720;
52const _DIFFICULTY_LAG: usize = 15;
57const BUF_SIZE: usize = 735;
60const _DIFFICULTY_CUT: usize = 60;
66const RETAINED: usize = 600;
69const CUT_BEGIN: usize = 60;
71const CUT_END: usize = 660;
73const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
75const BLOCK_FUTURE_TIME_LIMIT: Timestamp = Timestamp::from_u64(60 * 60 * 2);
77pub const RANDOMX_KEY_CHANGING_HEIGHT: u32 = 2048;
79pub const RANDOMX_KEY_CHANGE_DELAY: u32 = 64;
81
82#[derive(Clone)]
84pub struct PoWModule {
85 pub genesis: Timestamp,
87 pub target: u32,
89 pub fixed_difficulty: Option<BigUint>,
91 pub timestamps: RingBuffer<Timestamp, BUF_SIZE>,
93 pub difficulties: RingBuffer<BigUint, BUF_SIZE>,
95 pub cumulative_difficulty: BigUint,
100 pub darkfi_rx_keys: (HeaderHash, HeaderHash),
102 pub darkfi_rx_factory: RandomXFactory,
104 pub monero_rx_factory: RandomXFactory,
106}
107
108impl PoWModule {
109 pub fn new(
112 blockchain: Blockchain,
113 target: u32,
114 fixed_difficulty: Option<BigUint>,
115 height: Option<u32>,
116 ) -> Result<Self> {
117 let genesis = blockchain.genesis_block()?.header.timestamp;
119
120 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 let Some(diff) = &fixed_difficulty {
136 assert!(diff > &BigUint::zero());
137 }
138
139 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 pub fn next_difficulty(&self) -> Result<BigUint> {
167 let mut timestamps: Vec<Timestamp> =
169 self.timestamps.iter().take(DIFFICULTY_WINDOW).cloned().collect();
170
171 let length = timestamps.len();
173 if length < 2 {
174 return Ok(BigUint::one())
175 }
176
177 if let Some(diff) = &self.fixed_difficulty {
179 return Ok(diff.clone())
180 }
181
182 timestamps.sort_unstable();
184
185 let (cut_begin, cut_end) = self.cutoff(length)?;
187
188 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 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 let next_difficulty =
204 (total_work * self.target + time_span.inner() - BigUint::one()) / time_span.inner();
205
206 Ok(next_difficulty)
207 }
208
209 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 if
225 cut_begin + 2 > cut_end || cut_end > length {
227 return Err(Error::PoWCuttofCalculationError)
228 }
229
230 Ok((cut_begin, cut_end))
231 }
232
233 pub fn next_mine_target(&self) -> Result<BigUint> {
235 Ok(BigUint::from_bytes_le(&[0xFF; 32]) / &self.next_difficulty()?)
236 }
237
238 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 pub fn verify_difficulty(&self, difficulty: &BigUint) -> Result<bool> {
247 Ok(difficulty == &self.next_difficulty()?)
248 }
249
250 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 pub fn verify_timestamp_by_median(&self, timestamp: Timestamp) -> bool {
262 if timestamp <= self.genesis {
264 return false
265 }
266
267 if self.timestamps.len() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
269 return true
270 }
271
272 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 pub fn verify_current_block(&self, header: &Header) -> Result<()> {
286 if !self.verify_current_timestamp(header.timestamp)? {
288 return Err(Error::PoWInvalidTimestamp)
289 }
290
291 self.verify_block_hash(header)
293 }
294
295 pub fn verify_block_target(&self, header: &Header, target: &BigUint) -> Result<BigUint> {
297 let verifier_setup = Instant::now();
298
299 let (out_hash, verification_time) = match &header.pow_data {
301 DarkFi => {
302 let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
306 header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
307 {
308 &self.darkfi_rx_keys.1
309 } else {
310 &self.darkfi_rx_keys.0
311 };
312
313 let vm = self.darkfi_rx_factory.create(&randomx_key.inner()[..])?;
314
315 debug!(
316 target: "validator::pow::verify_block_target",
317 "[VERIFIER] DarkFi PoW setup time: {:?}",
318 verifier_setup.elapsed(),
319 );
320
321 let verification_time = Instant::now();
322 let out_hash = vm.calculate_hash(&header.to_block_hashing_blob())?;
323 (BigUint::from_bytes_le(&out_hash), verification_time)
324 }
325 Monero(powdata) => {
326 let vm = self.monero_rx_factory.create(powdata.randomx_key())?;
327
328 debug!(
329 target: "validator::pow::verify_block_target",
330 "[VERIFIER] Monero PoW setup time: {:?}",
331 verifier_setup.elapsed(),
332 );
333
334 let verification_time = Instant::now();
335 let out_hash = vm.calculate_hash(&powdata.to_block_hashing_blob())?;
336 (BigUint::from_bytes_le(&out_hash), verification_time)
337 }
338 };
339 debug!(target: "validator::pow::verify_block_target", "[VERIFIER] Verification time: {:?}", verification_time.elapsed());
340
341 if out_hash > *target {
343 return Err(Error::PoWInvalidOutHash)
344 }
345
346 Ok(out_hash)
347 }
348
349 pub fn verify_block_hash(&self, header: &Header) -> Result<()> {
351 let target = self.next_mine_target()?;
353
354 let _ = self.verify_block_target(header, &target)?;
356 Ok(())
357 }
358
359 pub fn append(&mut self, header: &Header, difficulty: &BigUint) -> Result<()> {
363 self.timestamps.push(header.timestamp);
364 self.cumulative_difficulty += difficulty;
365 self.difficulties.push(self.cumulative_difficulty.clone());
366
367 if header.height < RANDOMX_KEY_CHANGING_HEIGHT {
368 return Ok(())
369 }
370
371 if header.height.is_multiple_of(RANDOMX_KEY_CHANGING_HEIGHT) {
373 let next_key = header.hash();
374 let _ = self.darkfi_rx_factory.create(&next_key.inner()[..])?;
375 self.darkfi_rx_keys.1 = next_key;
376 return Ok(())
377 }
378
379 if header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY {
381 self.darkfi_rx_keys.0 = self.darkfi_rx_keys.1;
382 }
383
384 Ok(())
385 }
386
387 pub fn append_difficulty(
390 &mut self,
391 overlay: &BlockchainOverlayPtr,
392 header: &Header,
393 difficulty: BlockDifficulty,
394 ) -> Result<()> {
395 self.append(header, &difficulty.difficulty)?;
396 overlay.lock().unwrap().blocks.insert_difficulty(&[difficulty])
397 }
398
399 pub fn mine_block(
402 &self,
403 header: &mut Header,
404 threads: usize,
405 stop_signal: &Receiver<()>,
406 ) -> Result<()> {
407 let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
411 header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
412 {
413 &self.darkfi_rx_keys.1
414 } else {
415 &self.darkfi_rx_keys.0
416 };
417
418 let flags = get_mining_flags(false, false, false);
420 let vms = generate_mining_vms(flags, randomx_key, threads, stop_signal)?;
421
422 let target = self.next_mine_target()?;
424
425 mine_block(&vms, &target, header, stop_signal)
427 }
428}
429
430impl std::fmt::Display for PoWModule {
431 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
432 write!(f, "PoWModule:")?;
433 write!(f, "\ttarget: {}", self.target)?;
434 write!(f, "\ttimestamps: {:?}", self.timestamps)?;
435 write!(f, "\tdifficulties: {:?}", self.difficulties)?;
436 write!(f, "\tcumulative_difficulty: {}", self.cumulative_difficulty)
437 }
438}
439
440pub fn get_mining_flags(fast_mode: bool, large_pages: bool, secure: bool) -> RandomXFlags {
445 let mut flags = RandomXFlags::get_recommended_flags();
446 if fast_mode {
447 flags |= RandomXFlags::FULLMEM;
448 }
449 if large_pages {
450 flags |= RandomXFlags::LARGEPAGES;
451 }
452 if secure && flags.contains(RandomXFlags::JIT) {
453 flags |= RandomXFlags::SECURE;
454 }
455 flags
456}
457
458fn init_dataset(
461 flags: RandomXFlags,
462 input: &HeaderHash,
463 stop_signal: &Receiver<()>,
464) -> Result<RandomXDataset> {
465 let cache = RandomXCache::new(flags, &input.inner()[..])?;
467 let dataset_item_count = RandomXDataset::count()?;
468 let dataset = RandomXDataset::new(flags, cache, dataset_item_count)?;
469
470 let threads = thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
472 debug!(target: "validator::pow::init_dataset", "[MINER] Initializing RandomX dataset using {threads} threads...");
473 let mut handles = Vec::with_capacity(threads);
474 let threads_u32 = threads as u32;
475 let per_thread = dataset_item_count / threads_u32;
476 let remainder = dataset_item_count % threads_u32;
477 for t in 0..threads_u32 {
478 if stop_signal.is_full() {
480 debug!(target: "validator::pow::init_dataset", "[MINER] Stop signal received, threads creation loop exiting");
481 break
482 }
483
484 let dataset = dataset.clone();
485 let start_item = t * per_thread;
486 let count = per_thread + if t == threads_u32 - 1 { remainder } else { 0 };
487 handles.push(thread::spawn(move || {
488 dataset.init(start_item, count);
489 }));
490 }
491
492 for handle in handles {
494 let _ = handle.join();
495 }
496
497 if stop_signal.is_full() {
499 debug!(target: "validator::pow::init_dataset", "[MINER] Stop signal received, exiting");
500 return Err(Error::MinerTaskStopped);
501 }
502 Ok(dataset)
503}
504
505pub fn generate_mining_vms(
507 flags: RandomXFlags,
508 input: &HeaderHash,
509 threads: usize,
510 stop_signal: &Receiver<()>,
511) -> Result<Vec<Arc<RandomXVM>>> {
512 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX cache and dataset...");
513 debug!(target: "validator::pow::generate_mining_vms", "[MINER] PoW input: {input}");
514 let setup_start = Instant::now();
515 let (cache, dataset) = if flags.contains(RandomXFlags::FULLMEM) {
517 let dataset = init_dataset(flags, input, stop_signal)?;
519 (None, Some(dataset))
520 } else {
521 let cache = RandomXCache::new(flags, &input.inner()[..])?;
523 (Some(cache), None)
524 };
525 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX cache and dataset: {:?}", setup_start.elapsed());
526
527 if threads == 1 {
529 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX VM...");
530 let vm_start = Instant::now();
531 let vm = Arc::new(RandomXVM::new(flags, cache, dataset)?);
532 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX VM in {:?}", vm_start.elapsed());
533 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Setup time: {:?}", setup_start.elapsed());
534 return Ok(vec![vm])
535 }
536
537 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing {threads} RandomX VMs...");
539 let mut vms = Vec::with_capacity(threads);
540 let threads_u32 = threads as u32;
541 for t in 0..threads_u32 {
542 if stop_signal.is_full() {
544 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Stop signal received, exiting");
545 return Err(Error::MinerTaskStopped);
546 }
547
548 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initializing RandomX VM #{t}...");
549 let vm_start = Instant::now();
550 vms.push(Arc::new(RandomXVM::new(flags, cache.clone(), dataset.clone())?));
551 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Initialized RandomX VM #{t} in {:?}", vm_start.elapsed());
552 }
553 debug!(target: "validator::pow::generate_mining_vms", "[MINER] Setup time: {:?}", setup_start.elapsed());
554 Ok(vms)
555}
556
557pub fn mine_block(
560 vms: &[Arc<RandomXVM>],
561 target: &BigUint,
562 header: &mut Header,
563 stop_signal: &Receiver<()>,
564) -> Result<()> {
565 debug!(target: "validator::pow::mine_block", "[MINER] Mine target: 0x{target:064x}");
566
567 if vms.is_empty() {
569 error!(target: "validator::pow::mine_block", "[MINER] No VMs were provided!");
570 return Err(Error::MinerTaskStopped)
571 }
572
573 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Initializing mining threads...");
574 let mut handles = Vec::with_capacity(vms.len());
575 let atomic_nonce = Arc::new(AtomicU32::new(0));
576 let found_header = Arc::new(AtomicBool::new(false));
577 let found_nonce = Arc::new(AtomicU32::new(0));
578 let threads = vms.len() as u32;
579 let mining_start = Instant::now();
580 for t in 0..threads {
581 if stop_signal.is_full() {
583 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, threads creation loop exiting");
584 break
585 }
586
587 if found_header.load(Ordering::SeqCst) {
588 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header found, threads creation loop exiting");
589 break
590 }
591
592 let vm = vms[t as usize].clone();
593 let target = target.clone();
594 let mut thread_header = header.clone();
595 let atomic_nonce = atomic_nonce.clone();
596 let found_header = found_header.clone();
597 let found_nonce = found_nonce.clone();
598 let stop_signal = stop_signal.clone();
599
600 handles.push(thread::spawn(move || {
601 let mut last_nonce = atomic_nonce.fetch_add(1, Ordering::SeqCst);
602 thread_header.nonce = last_nonce;
603 if let Err(e) = vm.calculate_hash_first(thread_header.hash().inner()) {
604 error!(target: "validator::pow::randomx_vms_mine", "[MINER] Calculating hash in thread #{t} failed: {e}");
605 return
606 };
607 loop {
608 if stop_signal.is_full() {
610 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, thread #{t} exiting");
611 break
612 }
613
614 if found_header.load(Ordering::SeqCst) {
615 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header found, thread #{t} exiting");
616 break;
617 }
618
619 thread_header.nonce = atomic_nonce.fetch_add(1, Ordering::SeqCst);
620 let out_hash = match vm.calculate_hash_next(thread_header.hash().inner()) {
621 Ok(hash) => hash,
622 Err(e) => {
623 error!(target: "validator::pow::randomx_vms_mine", "[MINER] Calculating hash in thread #{t} failed: {e}");
624 break
625 }
626 };
627 let out_hash = BigUint::from_bytes_le(&out_hash);
628 if out_hash <= target {
629 found_header.store(true, Ordering::SeqCst);
630 thread_header.nonce = last_nonce; found_nonce.store(thread_header.nonce, Ordering::SeqCst);
632 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Thread #{t} found block header using nonce {}",
633 thread_header.nonce
634 );
635 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Block header hash {}", thread_header.hash());
636 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] RandomX output: 0x{out_hash:064x}");
637 break;
638 }
639 last_nonce = thread_header.nonce;
640 }
641 }));
642 }
643
644 for handle in handles {
646 let _ = handle.join();
647 }
648
649 if stop_signal.is_full() {
651 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Stop signal received, exiting");
652 return Err(Error::MinerTaskStopped);
653 }
654
655 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Completed mining in {:?}", mining_start.elapsed());
656 header.nonce = found_nonce.load(Ordering::SeqCst);
657 debug!(target: "validator::pow::randomx_vms_mine", "[MINER] Mined header: {header:?}");
658 Ok(())
659}
660
661#[cfg(test)]
662mod tests {
663 use std::{
664 io::{BufRead, Cursor},
665 process::Command,
666 };
667
668 use darkfi_sdk::num_traits::Num;
669 use num_bigint::BigUint;
670 use sled_overlay::sled;
671
672 use crate::{
673 blockchain::{header_store::Header, BlockInfo, Blockchain},
674 Result,
675 };
676
677 use super::PoWModule;
678
679 const DEFAULT_TEST_THREADS: usize = 2;
680 const DEFAULT_TEST_DIFFICULTY_TARGET: u32 = 120;
681
682 #[test]
683 fn test_wide_difficulty() -> Result<()> {
684 let sled_db = sled::Config::new().temporary(true).open()?;
685 let blockchain = Blockchain::new(&sled_db)?;
686 let genesis_block = BlockInfo::default();
687 blockchain.add_block(&genesis_block)?;
688
689 let mut module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
690
691 let output = Command::new("./script/monero_gen_wide_data.py").output().unwrap();
692 let reader = Cursor::new(output.stdout);
693
694 let mut previous = genesis_block.header;
695 for (n, line) in reader.lines().enumerate() {
696 let line = line.unwrap();
697 let parts: Vec<String> = line.split(' ').map(|x| x.to_string()).collect();
698 assert!(parts.len() == 2);
699
700 let header = Header::new(
701 previous.hash(),
702 previous.height + 1,
703 0,
704 parts[0].parse::<u64>().unwrap().into(),
705 );
706 let difficulty = BigUint::from_str_radix(&parts[1], 10).unwrap();
707
708 let res = module.next_difficulty()?;
709
710 if res != difficulty {
711 eprintln!("Wrong wide difficulty for block {n}");
712 eprintln!("Expected: {difficulty}");
713 eprintln!("Found: {res}");
714 assert!(res == difficulty);
715 }
716
717 module.append(&header, &difficulty)?;
718 previous = header;
719 }
720
721 Ok(())
722 }
723
724 #[test]
725 fn test_miner_correctness() -> Result<()> {
726 let sled_db = sled::Config::new().temporary(true).open()?;
728 let blockchain = Blockchain::new(&sled_db)?;
729 let mut genesis_block = BlockInfo::default();
730 genesis_block.header.timestamp = 0.into();
731 blockchain.add_block(&genesis_block)?;
732
733 let module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
734
735 let (_, recvr) = smol::channel::bounded(1);
736
737 let mut next_block = BlockInfo::default();
739 next_block.header.previous = genesis_block.hash();
740 module.mine_block(&mut next_block.header, DEFAULT_TEST_THREADS, &recvr)?;
741
742 module.verify_current_block(&next_block.header)?;
744
745 Ok(())
746 }
747}