darkfi/validator/
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
19use std::{
20    sync::{
21        atomic::{AtomicBool, AtomicU64, Ordering},
22        Arc,
23    },
24    thread,
25    time::Instant,
26};
27
28use darkfi_sdk::num_traits::{One, Zero};
29use log::debug;
30use num_bigint::BigUint;
31use randomx::{RandomXCache, RandomXDataset, RandomXFlags, RandomXVM};
32use smol::channel::Receiver;
33
34use 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.
47const 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.
52const _DIFFICULTY_LAG: usize = 15;
53/// Ring buffer length.
54/// Must be == DIFFICULTY_WINDOW + DIFFICULTY_LAG
55const 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.
61const _DIFFICULTY_CUT: usize = 60;
62/// Max items to use for next difficulty calculation.
63/// Must be DIFFICULTY_WINDOW - 2 * DIFFICULTY_CUT
64const RETAINED: usize = 600;
65/// Already known cutoff start index for this config
66const CUT_BEGIN: usize = 60;
67/// Already known cutoff end index for this config
68const CUT_END: usize = 660;
69/// How many most recent blocks to use to verify new blocks' timestamp
70const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60;
71/// Time limit in the future of what blocks can be
72const 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)]
76pub 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
94impl 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
328impl 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.
339pub 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)]
422mod 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}