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, Option<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.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 if out_hash > *target {
344 return Err(Error::PoWInvalidOutHash)
345 }
346
347 Ok(out_hash)
348 }
349
350 pub fn verify_block_hash(&self, header: &Header) -> Result<()> {
352 let target = self.next_mine_target()?;
354
355 let _ = self.verify_block_target(header, &target)?;
357 Ok(())
358 }
359
360 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 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 if header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY {
382 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 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 pub fn mine_block(
405 &self,
406 header: &mut Header,
407 threads: usize,
408 stop_signal: &Receiver<()>,
409 ) -> Result<()> {
410 let randomx_key = if header.height > RANDOMX_KEY_CHANGING_HEIGHT &&
414 header.height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY
415 {
416 &self.darkfi_rx_keys.1.unwrap()
418 } else {
419 &self.darkfi_rx_keys.0
420 };
421
422 let flags = get_mining_flags(false, false, false);
424 let vms = generate_mining_vms(flags, randomx_key, threads, stop_signal)?;
425
426 let target = self.next_mine_target()?;
428
429 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
444pub 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
462fn init_dataset(
465 flags: RandomXFlags,
466 input: &HeaderHash,
467 stop_signal: &Receiver<()>,
468) -> Result<RandomXDataset> {
469 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 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 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 for handle in handles {
498 let _ = handle.join();
499 }
500
501 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
509pub 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 let (cache, dataset) = if flags.contains(RandomXFlags::FULLMEM) {
521 let dataset = init_dataset(flags, input, stop_signal)?;
523 (None, Some(dataset))
524 } else {
525 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 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 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 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
561pub 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 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 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 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; 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 for handle in handles {
650 let _ = handle.join();
651 }
652
653 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 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 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 module.verify_current_block(&next_block.header)?;
748
749 Ok(())
750 }
751}