cuprate_consensus_context/
difficulty.rs

1//! Difficulty Module
2//!
3//! This module handles keeping track of the data required to calculate block difficulty.
4//! This data is currently the cumulative difficulty of each block and its timestamp.
5//!
6//! The timestamps are also used in other consensus rules so instead of duplicating the same
7//! data in a different cache, the timestamps needed are retrieved from here.
8//!
9use std::{collections::VecDeque, ops::Range};
10
11use tower::ServiceExt;
12use tracing::instrument;
13
14use cuprate_helper::num::median;
15use cuprate_types::{
16    blockchain::{BlockchainReadRequest, BlockchainResponse},
17    Chain,
18};
19
20use crate::{ContextCacheError, Database, HardFork};
21
22/// The amount of blocks we account for to calculate difficulty
23const DIFFICULTY_WINDOW: usize = 720;
24/// The proportion of blocks we remove from the [`DIFFICULTY_WINDOW`]. When the window
25/// if 720 this means that 60 blocks are removed from the ends of the window so 120
26/// blocks removed in total.
27const DIFFICULTY_CUT: usize = 60;
28/// The amount of blocks we add onto the window before doing any calculations so that the
29/// difficulty lags by this amount of blocks
30const DIFFICULTY_LAG: usize = 15;
31
32/// Configuration for the difficulty cache.
33///
34#[derive(Debug, Clone, Copy, Eq, PartialEq)]
35pub struct DifficultyCacheConfig {
36    pub window: usize,
37    pub cut: usize,
38    pub lag: usize,
39    /// If [`Some`] the difficulty cache will always return this value as the current difficulty.
40    pub fixed_difficulty: Option<u128>,
41}
42
43impl DifficultyCacheConfig {
44    /// Returns the total amount of blocks we need to track to calculate difficulty
45    pub const fn total_block_count(&self) -> usize {
46        self.window + self.lag
47    }
48
49    /// The amount of blocks we account for after removing the outliers.
50    pub const fn accounted_window_len(&self) -> usize {
51        self.window - 2 * self.cut
52    }
53
54    /// Returns the config needed for [`Mainnet`](cuprate_helper::network::Network::Mainnet). This is also the
55    /// config for all other current networks.
56    pub const fn main_net() -> Self {
57        Self {
58            window: DIFFICULTY_WINDOW,
59            cut: DIFFICULTY_CUT,
60            lag: DIFFICULTY_LAG,
61            fixed_difficulty: None,
62        }
63    }
64}
65
66/// This struct is able to calculate difficulties from blockchain information.
67///
68#[derive(Debug, Clone, Eq, PartialEq)]
69pub struct DifficultyCache {
70    /// The list of timestamps in the window.
71    pub timestamps: VecDeque<u64>,
72    /// The current cumulative difficulty of the chain.
73    pub cumulative_difficulties: VecDeque<u128>,
74    /// The last height we accounted for.
75    pub last_accounted_height: usize,
76    /// The config
77    pub config: DifficultyCacheConfig,
78}
79
80impl DifficultyCache {
81    /// Initialize the difficulty cache from the specified chain height.
82    #[instrument(name = "init_difficulty_cache", level = "info", skip(database, config))]
83    pub async fn init_from_chain_height<D: Database + Clone>(
84        chain_height: usize,
85        config: DifficultyCacheConfig,
86        database: D,
87        chain: Chain,
88    ) -> Result<Self, ContextCacheError> {
89        tracing::info!("Initializing difficulty cache this may take a while.");
90
91        let mut block_start = chain_height.saturating_sub(config.total_block_count());
92
93        // skip the genesis block.
94        if block_start == 0 {
95            block_start = 1;
96        }
97
98        let (timestamps, cumulative_difficulties) =
99            get_blocks_in_pow_info(database.clone(), block_start..chain_height, chain).await?;
100
101        debug_assert_eq!(timestamps.len(), chain_height - block_start);
102
103        tracing::info!(
104            "Current chain height: {}, accounting for {} blocks timestamps",
105            chain_height,
106            timestamps.len()
107        );
108
109        let diff = Self {
110            timestamps,
111            cumulative_difficulties,
112            last_accounted_height: chain_height - 1,
113            config,
114        };
115
116        Ok(diff)
117    }
118
119    /// Pop some blocks from the top of the cache.
120    ///
121    /// The cache will be returned to the state it would have been in `numb_blocks` ago.
122    ///
123    /// # Invariant
124    ///
125    /// This _must_ only be used on a main-chain cache.
126    #[instrument(name = "pop_blocks_diff_cache", skip_all, fields(numb_blocks = numb_blocks))]
127    pub async fn pop_blocks_main_chain<D: Database + Clone>(
128        &mut self,
129        numb_blocks: usize,
130        database: D,
131    ) -> Result<(), ContextCacheError> {
132        let Some(retained_blocks) = self.timestamps.len().checked_sub(numb_blocks) else {
133            // More blocks to pop than we have in the cache, so just restart a new cache.
134            *self = Self::init_from_chain_height(
135                self.last_accounted_height - numb_blocks + 1,
136                self.config,
137                database,
138                Chain::Main,
139            )
140            .await?;
141
142            return Ok(());
143        };
144
145        let current_chain_height = self.last_accounted_height + 1;
146
147        let mut new_start_height = current_chain_height
148            .saturating_sub(self.config.total_block_count())
149            .saturating_sub(numb_blocks);
150
151        // skip the genesis block.
152        if new_start_height == 0 {
153            new_start_height = 1;
154        }
155
156        let (mut timestamps, mut cumulative_difficulties) = get_blocks_in_pow_info(
157            database,
158            new_start_height
159                // current_chain_height - self.timestamps.len() blocks are already in the cache.
160                ..(current_chain_height - self.timestamps.len()),
161            Chain::Main,
162        )
163        .await?;
164
165        self.timestamps.drain(retained_blocks..);
166        self.cumulative_difficulties.drain(retained_blocks..);
167        timestamps.append(&mut self.timestamps);
168        cumulative_difficulties.append(&mut self.cumulative_difficulties);
169
170        self.timestamps = timestamps;
171        self.cumulative_difficulties = cumulative_difficulties;
172        self.last_accounted_height -= numb_blocks;
173
174        assert_eq!(self.timestamps.len(), self.cumulative_difficulties.len());
175
176        Ok(())
177    }
178
179    /// Add a new block to the difficulty cache.
180    pub fn new_block(&mut self, height: usize, timestamp: u64, cumulative_difficulty: u128) {
181        assert_eq!(self.last_accounted_height + 1, height);
182        self.last_accounted_height += 1;
183
184        tracing::debug!(
185            "Accounting for new blocks timestamp ({timestamp}) and cumulative_difficulty ({cumulative_difficulty})",
186        );
187
188        self.timestamps.push_back(timestamp);
189        self.cumulative_difficulties
190            .push_back(cumulative_difficulty);
191
192        if self.timestamps.len() > self.config.total_block_count() {
193            self.timestamps.pop_front();
194            self.cumulative_difficulties.pop_front();
195        }
196    }
197
198    /// Returns the required difficulty for the next block.
199    ///
200    /// See: <https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#calculating-difficulty>
201    pub fn next_difficulty(&self, hf: HardFork) -> u128 {
202        next_difficulty(
203            &self.config,
204            &self.timestamps,
205            &self.cumulative_difficulties,
206            hf,
207        )
208    }
209
210    /// Returns the difficulties for multiple next blocks, using the provided timestamps and hard-forks when needed.
211    ///
212    /// The first difficulty will be the same as the difficulty from [`DifficultyCache::next_difficulty`] after that the
213    /// first timestamp and hf will be applied to the cache and the difficulty from that will be added to the list.
214    ///
215    /// After all timestamps and hfs have been dealt with the cache will be returned back to its original state and the
216    /// difficulties will be returned.
217    pub fn next_difficulties(
218        &self,
219        blocks: Vec<(u64, HardFork)>,
220        current_hf: HardFork,
221    ) -> Vec<u128> {
222        let mut timestamps = self.timestamps.clone();
223        let mut cumulative_difficulties = self.cumulative_difficulties.clone();
224
225        let mut difficulties = Vec::with_capacity(blocks.len() + 1);
226
227        difficulties.push(self.next_difficulty(current_hf));
228
229        for (new_timestamp, hf) in blocks {
230            timestamps.push_back(new_timestamp);
231
232            let last_cum_diff = cumulative_difficulties.back().copied().unwrap_or(1);
233            cumulative_difficulties.push_back(last_cum_diff + *difficulties.last().unwrap());
234
235            if timestamps.len() > self.config.total_block_count() {
236                timestamps.pop_front().unwrap();
237                cumulative_difficulties.pop_front().unwrap();
238            }
239
240            difficulties.push(next_difficulty(
241                &self.config,
242                &timestamps,
243                &cumulative_difficulties,
244                hf,
245            ));
246        }
247
248        difficulties
249    }
250
251    /// Returns the median timestamp over the last `numb_blocks`, including the genesis block if the block height is low enough.
252    ///
253    /// Will return [`None`] if there aren't enough blocks.
254    pub fn median_timestamp(&self, numb_blocks: usize) -> Option<u64> {
255        let mut timestamps = if self.last_accounted_height + 1 == numb_blocks {
256            // if the chain height is equal to `numb_blocks` add the genesis block.
257            // otherwise if the chain height is less than `numb_blocks` None is returned
258            // and if it's more it would be excluded from calculations.
259            let mut timestamps = self.timestamps.clone();
260            // all genesis blocks have a timestamp of 0.
261            // https://cuprate.github.io/monero-book/consensus_rules/genesis_block.html
262            timestamps.push_front(0);
263            timestamps.into()
264        } else {
265            self.timestamps
266                .range(self.timestamps.len().checked_sub(numb_blocks)?..)
267                .copied()
268                .collect::<Vec<_>>()
269        };
270        timestamps.sort_unstable();
271        debug_assert_eq!(timestamps.len(), numb_blocks);
272
273        Some(median(&timestamps))
274    }
275
276    /// Returns the cumulative difficulty of the chain.
277    pub fn cumulative_difficulty(&self) -> u128 {
278        // the genesis block has a difficulty of 1
279        self.cumulative_difficulties.back().copied().unwrap_or(1)
280    }
281
282    /// Returns the top block's timestamp, returns [`None`] if the top block is the genesis block.
283    pub fn top_block_timestamp(&self) -> Option<u64> {
284        self.timestamps.back().copied()
285    }
286}
287
288/// Calculates the next difficulty with the inputted `config/timestamps/cumulative_difficulties`.
289fn next_difficulty(
290    config: &DifficultyCacheConfig,
291    timestamps: &VecDeque<u64>,
292    cumulative_difficulties: &VecDeque<u128>,
293    hf: HardFork,
294) -> u128 {
295    if let Some(fixed_difficulty) = config.fixed_difficulty {
296        return fixed_difficulty;
297    }
298
299    if timestamps.len() <= 1 {
300        return 1;
301    }
302
303    let mut timestamps = timestamps.clone();
304
305    if timestamps.len() > config.window {
306        // remove the lag.
307        timestamps.drain(config.window..);
308    };
309    let timestamps_slice = timestamps.make_contiguous();
310
311    let (window_start, window_end) = get_window_start_and_end(
312        timestamps_slice.len(),
313        config.accounted_window_len(),
314        config.window,
315    );
316
317    // We don't sort the whole timestamp list
318    let mut time_span = u128::from(
319        *timestamps_slice.select_nth_unstable(window_end - 1).1
320            - *timestamps_slice.select_nth_unstable(window_start).1,
321    );
322
323    let windowed_work =
324        cumulative_difficulties[window_end - 1] - cumulative_difficulties[window_start];
325
326    if time_span == 0 {
327        time_span = 1;
328    }
329
330    // TODO: do `checked_mul` here and unwrap so we don't silently overflow?
331    (windowed_work * u128::from(hf.block_time().as_secs())).div_ceil(time_span)
332}
333
334/// Get the start and end of the window to calculate difficulty.
335fn get_window_start_and_end(
336    window_len: usize,
337    accounted_window: usize,
338    window: usize,
339) -> (usize, usize) {
340    debug_assert!(window > accounted_window);
341
342    let window_len = if window_len > window {
343        window
344    } else {
345        window_len
346    };
347
348    if window_len <= accounted_window {
349        (0, window_len)
350    } else {
351        let start = (window_len - (accounted_window) + 1) / 2;
352        (start, start + accounted_window)
353    }
354}
355
356/// Returns the timestamps and cumulative difficulty for the blocks with heights in the specified range.
357#[instrument(name = "get_blocks_timestamps", skip(database), level = "info")]
358async fn get_blocks_in_pow_info<D: Database + Clone>(
359    database: D,
360    block_heights: Range<usize>,
361    chain: Chain,
362) -> Result<(VecDeque<u64>, VecDeque<u128>), ContextCacheError> {
363    tracing::info!("Getting blocks timestamps");
364
365    let BlockchainResponse::BlockExtendedHeaderInRange(ext_header) = database
366        .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
367            block_heights,
368            chain,
369        ))
370        .await?
371    else {
372        panic!("Database sent incorrect response");
373    };
374
375    Ok(ext_header
376        .into_iter()
377        .map(|info| (info.timestamp, info.cumulative_difficulty))
378        .unzip())
379}