/ src / validator / pow.rs
pow.rs
  1  /* This file is part of DarkFi (https://dark.fi)
  2   *
  3   * Copyright (C) 2020-2025 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  
 19  use std::{
 20      sync::{
 21          atomic::{AtomicBool, AtomicU64, Ordering},
 22          Arc,
 23      },
 24      thread,
 25      time::Instant,
 26  };
 27  
 28  use darkfi_sdk::num_traits::{One, Zero};
 29  use num_bigint::BigUint;
 30  use randomx::{RandomXCache, RandomXDataset, RandomXFlags, RandomXVM};
 31  use smol::channel::Receiver;
 32  use tracing::debug;
 33  
 34  use crate::{
 35      blockchain::{
 36          block_store::{BlockDifficulty, BlockInfo},
 37          Blockchain, BlockchainOverlayPtr,
 38      },
 39      util::{ringbuffer::RingBuffer, time::Timestamp},
 40      validator::utils::median,
 41      Error, Result,
 42  };
 43  
 44  // Note: We have combined some constants for better performance.
 45  /// Amount of max items(blocks) to use for next difficulty calculation.
 46  /// Must be >= 2 and == BUF_SIZE - DIFFICULTY_LAG.
 47  const DIFFICULTY_WINDOW: usize = 720;
 48  /// Amount of latest blocks to exlude from the calculation.
 49  /// Our ring buffer has length: DIFFICULTY_WINDOW + DIFFICULTY_LAG,
 50  /// but we only use DIFFICULTY_WINDOW items in calculations.
 51  /// Must be == BUF_SIZE - DIFFICULTY_WINDOW.
 52  const _DIFFICULTY_LAG: usize = 15;
 53  /// Ring buffer length.
 54  /// Must be == DIFFICULTY_WINDOW + DIFFICULTY_LAG
 55  const BUF_SIZE: usize = 735;
 56  /// Used to calculate how many items to retain for next difficulty
 57  /// calculation. We are keeping the middle items, meaning cutting
 58  /// both from frond and back of the ring buffer, ending up with max
 59  /// DIFFICULTY_WINDOW - 2*DIFFICULTY_CUT items.
 60  /// (2*DIFFICULTY_CUT <= DIFFICULTY_WINDOW-2) must be true.
 61  const _DIFFICULTY_CUT: usize = 60;
 62  /// Max items to use for next difficulty calculation.
 63  /// Must be DIFFICULTY_WINDOW - 2 * DIFFICULTY_CUT
 64  const RETAINED: usize = 600;
 65  /// Already known cutoff start index for this config
 66  const CUT_BEGIN: usize = 60;
 67  /// Already known cutoff end index for this config
 68  const CUT_END: usize = 660;
 69  /// How many most recent blocks to use to verify new blocks' timestamp
 70  const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
 71  /// Time limit in the future of what blocks can be
 72  const BLOCK_FUTURE_TIME_LIMIT: Timestamp = Timestamp::from_u64(60 * 60 * 2);
 73  
 74  /// This struct represents the information required by the PoW algorithm
 75  #[derive(Clone)]
 76  pub struct PoWModule {
 77      /// Genesis block timestamp
 78      pub genesis: Timestamp,
 79      /// Target block time, in seconds
 80      pub target: u32,
 81      /// Optional fixed difficulty
 82      pub fixed_difficulty: Option<BigUint>,
 83      /// Latest block timestamps ringbuffer
 84      pub timestamps: RingBuffer<Timestamp, BUF_SIZE>,
 85      /// Latest block cumulative difficulties ringbuffer
 86      pub difficulties: RingBuffer<BigUint, BUF_SIZE>,
 87      /// Total blocks cumulative difficulty
 88      /// Note: we keep this as a struct field for faster
 89      /// access(optimization), since its always same as
 90      /// difficulties buffer last.
 91      pub cumulative_difficulty: BigUint,
 92  }
 93  
 94  impl PoWModule {
 95      // Initialize a new `PowModule` for provided target over provided `Blockchain`.
 96      // Optionally, a fixed difficulty can be set and/or initialize before some height.
 97      pub fn new(
 98          blockchain: Blockchain,
 99          target: u32,
100          fixed_difficulty: Option<BigUint>,
101          height: Option<u32>,
102      ) -> Result<Self> {
103          // Retrieve genesis block timestamp
104          let genesis = blockchain.genesis_block()?.header.timestamp;
105  
106          // Retrieving last BUF_SIZE difficulties from blockchain to build the buffers
107          let mut timestamps = RingBuffer::<Timestamp, BUF_SIZE>::new();
108          let mut difficulties = RingBuffer::<BigUint, BUF_SIZE>::new();
109          let mut cumulative_difficulty = BigUint::zero();
110          let last_n = match height {
111              Some(h) => blockchain.blocks.get_difficulties_before(h, BUF_SIZE)?,
112              None => blockchain.blocks.get_last_n_difficulties(BUF_SIZE)?,
113          };
114          for difficulty in last_n {
115              timestamps.push(difficulty.timestamp);
116              difficulties.push(difficulty.cumulative_difficulty.clone());
117              cumulative_difficulty = difficulty.cumulative_difficulty;
118          }
119  
120          // If a fixed difficulty has been set, assert its greater than zero
121          if let Some(diff) = &fixed_difficulty {
122              assert!(diff > &BigUint::zero());
123          }
124  
125          Ok(Self {
126              genesis,
127              target,
128              fixed_difficulty,
129              timestamps,
130              difficulties,
131              cumulative_difficulty,
132          })
133      }
134  
135      /// Compute the next mining difficulty, based on current ring buffers.
136      /// If ring buffers contain 2 or less items, difficulty 1 is returned.
137      /// If a fixed difficulty has been set, this function will always
138      /// return that after first 2 difficulties.
139      pub fn next_difficulty(&self) -> Result<BigUint> {
140          // Retrieve first DIFFICULTY_WINDOW timestamps from the ring buffer
141          let mut timestamps: Vec<Timestamp> =
142              self.timestamps.iter().take(DIFFICULTY_WINDOW).cloned().collect();
143  
144          // Check we have enough timestamps
145          let length = timestamps.len();
146          if length < 2 {
147              return Ok(BigUint::one())
148          }
149  
150          // If a fixed difficulty has been set, return that
151          if let Some(diff) = &self.fixed_difficulty {
152              return Ok(diff.clone())
153          }
154  
155          // Sort the timestamps vector
156          timestamps.sort_unstable();
157  
158          // Grab cutoff indexes
159          let (cut_begin, cut_end) = self.cutoff(length)?;
160  
161          // Calculate total time span
162          let cut_end = cut_end - 1;
163  
164          let mut time_span = timestamps[cut_end].checked_sub(timestamps[cut_begin])?;
165          if time_span.inner() == 0 {
166              time_span = 1.into();
167          }
168  
169          // Calculate total work done during this time span
170          let total_work = &self.difficulties[cut_end] - &self.difficulties[cut_begin];
171          if total_work <= BigUint::zero() {
172              return Err(Error::PoWTotalWorkIsZero)
173          }
174  
175          // Compute next difficulty
176          let next_difficulty =
177              (total_work * self.target + time_span.inner() - BigUint::one()) / time_span.inner();
178  
179          Ok(next_difficulty)
180      }
181  
182      /// Calculate cutoff indexes.
183      /// If buffers have been filled, we return the
184      /// already known indexes, for performance.
185      fn cutoff(&self, length: usize) -> Result<(usize, usize)> {
186          if length >= DIFFICULTY_WINDOW {
187              return Ok((CUT_BEGIN, CUT_END))
188          }
189  
190          let (cut_begin, cut_end) = if length <= RETAINED {
191              (0, length)
192          } else {
193              let cut_begin = (length - RETAINED).div_ceil(2);
194              (cut_begin, cut_begin + RETAINED)
195          };
196          // Sanity check
197          if
198          /* cut_begin < 0 || */
199          cut_begin + 2 > cut_end || cut_end > length {
200              return Err(Error::PoWCuttofCalculationError)
201          }
202  
203          Ok((cut_begin, cut_end))
204      }
205  
206      /// Compute the next mine target.
207      pub fn next_mine_target(&self) -> Result<BigUint> {
208          Ok(BigUint::from_bytes_be(&[0xFF; 32]) / &self.next_difficulty()?)
209      }
210  
211      /// Compute the next mine target and difficulty.
212      pub fn next_mine_target_and_difficulty(&self) -> Result<(BigUint, BigUint)> {
213          let difficulty = self.next_difficulty()?;
214          let mine_target = BigUint::from_bytes_be(&[0xFF; 32]) / &difficulty;
215          Ok((mine_target, difficulty))
216      }
217  
218      /// Verify provided difficulty corresponds to the next one.
219      pub fn verify_difficulty(&self, difficulty: &BigUint) -> Result<bool> {
220          Ok(difficulty == &self.next_difficulty()?)
221      }
222  
223      /// Verify provided block timestamp is not far in the future and
224      /// check its valid acorrding to current timestamps median.
225      pub fn verify_current_timestamp(&self, timestamp: Timestamp) -> Result<bool> {
226          if timestamp > Timestamp::current_time().checked_add(BLOCK_FUTURE_TIME_LIMIT)? {
227              return Ok(false)
228          }
229  
230          Ok(self.verify_timestamp_by_median(timestamp))
231      }
232  
233      /// Verify provided block timestamp is valid and matches certain criteria.
234      pub fn verify_timestamp_by_median(&self, timestamp: Timestamp) -> bool {
235          // Check timestamp is after genesis one
236          if timestamp <= self.genesis {
237              return false
238          }
239  
240          // If not enough blocks, no proper median yet, return true
241          if self.timestamps.len() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW {
242              return true
243          }
244  
245          // Make sure the timestamp is higher or equal to the median
246          let timestamps = self
247              .timestamps
248              .iter()
249              .rev()
250              .take(BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW)
251              .map(|x| x.inner())
252              .collect();
253  
254          timestamp >= median(timestamps).into()
255      }
256  
257      /// Verify provided block timestamp and hash.
258      pub fn verify_current_block(&self, block: &BlockInfo) -> Result<()> {
259          // First we verify the block's timestamp
260          if !self.verify_current_timestamp(block.header.timestamp)? {
261              return Err(Error::PoWInvalidTimestamp)
262          }
263  
264          // Then we verify the block's hash
265          self.verify_block_hash(block)
266      }
267  
268      /// Verify provided block corresponds to next mine target.
269      // TODO: Verify depending on block Proof of Work data
270      pub fn verify_block_hash(&self, block: &BlockInfo) -> Result<()> {
271          let verifier_setup = Instant::now();
272  
273          // Grab the next mine target
274          let target = self.next_mine_target()?;
275  
276          // Setup verifier
277          let flags = RandomXFlags::default();
278          let cache = RandomXCache::new(flags, block.header.previous.inner()).unwrap();
279          let vm = RandomXVM::new(flags, &cache).unwrap();
280          debug!(target: "validator::pow::verify_block", "[VERIFIER] Setup time: {:?}", verifier_setup.elapsed());
281  
282          // Compute the output hash
283          let verification_time = Instant::now();
284          let out_hash = vm.hash(block.header.hash().inner());
285          let out_hash = BigUint::from_bytes_be(&out_hash);
286  
287          // Verify hash is less than the expected mine target
288          if out_hash > target {
289              return Err(Error::PoWInvalidOutHash)
290          }
291          debug!(target: "validator::pow::verify_block", "[VERIFIER] Verification time: {:?}", verification_time.elapsed());
292  
293          Ok(())
294      }
295  
296      /// Append provided timestamp and difficulty to the ring buffers.
297      pub fn append(&mut self, timestamp: Timestamp, difficulty: &BigUint) {
298          self.timestamps.push(timestamp);
299          self.cumulative_difficulty += difficulty;
300          self.difficulties.push(self.cumulative_difficulty.clone());
301      }
302  
303      /// Append provided block difficulty to the ring buffers and insert
304      /// it to provided overlay.
305      pub fn append_difficulty(
306          &mut self,
307          overlay: &BlockchainOverlayPtr,
308          difficulty: BlockDifficulty,
309      ) -> Result<()> {
310          self.append(difficulty.timestamp, &difficulty.difficulty);
311          overlay.lock().unwrap().blocks.insert_difficulty(&[difficulty])
312      }
313  
314      /// Mine provided block, based on next mine target.
315      pub fn mine_block(
316          &self,
317          miner_block: &mut BlockInfo,
318          threads: usize,
319          stop_signal: &Receiver<()>,
320      ) -> Result<()> {
321          // Grab the next mine target
322          let target = self.next_mine_target()?;
323  
324          mine_block(&target, miner_block, threads, stop_signal)
325      }
326  }
327  
328  impl std::fmt::Display for PoWModule {
329      fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
330          write!(f, "PoWModule:")?;
331          write!(f, "\ttarget: {}", self.target)?;
332          write!(f, "\ttimestamps: {:?}", self.timestamps)?;
333          write!(f, "\tdifficulties: {:?}", self.difficulties)?;
334          write!(f, "\tcumulative_difficulty: {}", self.cumulative_difficulty)
335      }
336  }
337  
338  /// Mine provided block, based on provided PoW module next mine target.
339  pub fn mine_block(
340      target: &BigUint,
341      miner_block: &mut BlockInfo,
342      threads: usize,
343      stop_signal: &Receiver<()>,
344  ) -> Result<()> {
345      let miner_setup = Instant::now();
346  
347      debug!(target: "validator::pow::mine_block", "[MINER] Mine target: 0x{target:064x}");
348      // Get the PoW input. The key changes with every mined block.
349      let input = miner_block.header.previous;
350      debug!(target: "validator::pow::mine_block", "[MINER] PoW input: {input}");
351      let flags = RandomXFlags::default() | RandomXFlags::FULLMEM;
352      debug!(target: "validator::pow::mine_block", "[MINER] Initializing RandomX dataset...");
353      let dataset = Arc::new(RandomXDataset::new(flags, input.inner(), threads).unwrap());
354      debug!(target: "validator::pow::mine_block", "[MINER] Setup time: {:?}", miner_setup.elapsed());
355  
356      // Multithreaded mining setup
357      let mining_time = Instant::now();
358      let mut handles = vec![];
359      let found_block = Arc::new(AtomicBool::new(false));
360      let found_nonce = Arc::new(AtomicU64::new(0));
361      let threads = threads as u64;
362      for t in 0..threads {
363          let target = target.clone();
364          let mut block = miner_block.clone();
365          let found_block = Arc::clone(&found_block);
366          let found_nonce = Arc::clone(&found_nonce);
367          let dataset = Arc::clone(&dataset);
368          let stop_signal = stop_signal.clone();
369  
370          handles.push(thread::spawn(move || {
371              debug!(target: "validator::pow::mine_block", "[MINER] Initializing RandomX VM #{t}...");
372              let mut miner_nonce = t;
373              let vm = RandomXVM::new_fast(flags, &dataset).unwrap();
374              loop {
375                  // Check if stop signal was received
376                  if stop_signal.is_full() {
377                      debug!(target: "validator::pow::mine_block", "[MINER] Stop signal received, thread #{t} exiting");
378                      break
379                  }
380  
381                  block.header.nonce = miner_nonce;
382                  if found_block.load(Ordering::SeqCst) {
383                      debug!(target: "validator::pow::mine_block", "[MINER] Block found, thread #{t} exiting");
384                      break
385                  }
386  
387                  let out_hash = vm.hash(block.hash().inner());
388                  let out_hash = BigUint::from_bytes_be(&out_hash);
389                  if out_hash <= target {
390                      found_block.store(true, Ordering::SeqCst);
391                      found_nonce.store(miner_nonce, Ordering::SeqCst);
392                      debug!(target: "validator::pow::mine_block", "[MINER] Thread #{t} found block using nonce {miner_nonce}");
393                      debug!(target: "validator::pow::mine_block", "[MINER] Block hash {}", block.hash());
394                      debug!(target: "validator::pow::mine_block", "[MINER] RandomX output: 0x{out_hash:064x}");
395                      break
396                  }
397  
398                  // This means thread 0 will use nonces, 0, 4, 8, ...
399                  // and thread 1 will use nonces, 1, 5, 9, ...
400                  miner_nonce += threads;
401              }
402          }));
403      }
404  
405      for handle in handles {
406          let _ = handle.join();
407      }
408      // Check if stop signal was received
409      if stop_signal.is_full() {
410          return Err(Error::MinerTaskStopped)
411      }
412  
413      debug!(target: "validator::pow::mine_block", "[MINER] Mining time: {:?}", mining_time.elapsed());
414  
415      // Set the valid mined nonce in the block
416      miner_block.header.nonce = found_nonce.load(Ordering::SeqCst);
417  
418      Ok(())
419  }
420  
421  #[cfg(test)]
422  mod tests {
423      use std::{
424          io::{BufRead, Cursor},
425          process::Command,
426      };
427  
428      use darkfi_sdk::num_traits::Num;
429      use num_bigint::BigUint;
430      use sled_overlay::sled;
431  
432      use crate::{
433          blockchain::{BlockInfo, Blockchain},
434          Result,
435      };
436  
437      use super::PoWModule;
438  
439      const DEFAULT_TEST_THREADS: usize = 2;
440      const DEFAULT_TEST_DIFFICULTY_TARGET: u32 = 120;
441  
442      #[test]
443      fn test_wide_difficulty() -> Result<()> {
444          let sled_db = sled::Config::new().temporary(true).open()?;
445          let blockchain = Blockchain::new(&sled_db)?;
446          let genesis_block = BlockInfo::default();
447          blockchain.add_block(&genesis_block)?;
448          let mut module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
449  
450          let output = Command::new("./script/research/pow/gen_wide_data.py").output().unwrap();
451          let reader = Cursor::new(output.stdout);
452  
453          for (n, line) in reader.lines().enumerate() {
454              let line = line.unwrap();
455              let parts: Vec<String> = line.split(' ').map(|x| x.to_string()).collect();
456              assert!(parts.len() == 2);
457  
458              let timestamp = parts[0].parse::<u64>().unwrap().into();
459              let difficulty = BigUint::from_str_radix(&parts[1], 10).unwrap();
460  
461              let res = module.next_difficulty()?;
462  
463              if res != difficulty {
464                  eprintln!("Wrong wide difficulty for block {n}");
465                  eprintln!("Expected: {difficulty}");
466                  eprintln!("Found: {res}");
467                  assert!(res == difficulty);
468              }
469  
470              module.append(timestamp, &difficulty);
471          }
472  
473          Ok(())
474      }
475  
476      #[test]
477      fn test_miner_correctness() -> Result<()> {
478          // Default setup
479          let sled_db = sled::Config::new().temporary(true).open()?;
480          let blockchain = Blockchain::new(&sled_db)?;
481          let mut genesis_block = BlockInfo::default();
482          genesis_block.header.timestamp = 0.into();
483          blockchain.add_block(&genesis_block)?;
484          let module = PoWModule::new(blockchain, DEFAULT_TEST_DIFFICULTY_TARGET, None, None)?;
485          let (_, recvr) = smol::channel::bounded(1);
486  
487          // Mine next block
488          let mut next_block = BlockInfo::default();
489          next_block.header.previous = genesis_block.hash();
490          module.mine_block(&mut next_block, DEFAULT_TEST_THREADS, &recvr)?;
491  
492          // Verify it
493          module.verify_current_block(&next_block)?;
494  
495          Ok(())
496      }
497  }