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};
1011use tower::ServiceExt;
12use tracing::instrument;
1314use cuprate_helper::num::median;
15use cuprate_types::{
16 blockchain::{BlockchainReadRequest, BlockchainResponse},
17 Chain,
18};
1920use crate::{ContextCacheError, Database, HardFork};
2122/// 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;
3132/// Configuration for the difficulty cache.
33///
34#[derive(Debug, Clone, Copy, Eq, PartialEq)]
35pub struct DifficultyCacheConfig {
36pub window: usize,
37pub cut: usize,
38pub lag: usize,
39/// If [`Some`] the difficulty cache will always return this value as the current difficulty.
40pub fixed_difficulty: Option<u128>,
41}
4243impl DifficultyCacheConfig {
44/// Returns the total amount of blocks we need to track to calculate difficulty
45pub const fn total_block_count(&self) -> usize {
46self.window + self.lag
47 }
4849/// The amount of blocks we account for after removing the outliers.
50pub const fn accounted_window_len(&self) -> usize {
51self.window - 2 * self.cut
52 }
5354/// Returns the config needed for [`Mainnet`](cuprate_helper::network::Network::Mainnet). This is also the
55 /// config for all other current networks.
56pub const fn main_net() -> Self {
57Self {
58 window: DIFFICULTY_WINDOW,
59 cut: DIFFICULTY_CUT,
60 lag: DIFFICULTY_LAG,
61 fixed_difficulty: None,
62 }
63 }
64}
6566/// 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.
71pub timestamps: VecDeque<u64>,
72/// The current cumulative difficulty of the chain.
73pub cumulative_difficulties: VecDeque<u128>,
74/// The last height we accounted for.
75pub last_accounted_height: usize,
76/// The config
77pub config: DifficultyCacheConfig,
78}
7980impl DifficultyCache {
81/// Initialize the difficulty cache from the specified chain height.
82#[instrument(name = "init_difficulty_cache", level = "info", skip(database, config))]
83pub 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> {
89tracing::info!("Initializing difficulty cache this may take a while.");
9091let mut block_start = chain_height.saturating_sub(config.total_block_count());
9293// skip the genesis block.
94if block_start == 0 {
95 block_start = 1;
96 }
9798let (timestamps, cumulative_difficulties) =
99 get_blocks_in_pow_info(database.clone(), block_start..chain_height, chain).await?;
100101debug_assert_eq!(timestamps.len(), chain_height - block_start);
102103tracing::info!(
104"Current chain height: {}, accounting for {} blocks timestamps",
105 chain_height,
106 timestamps.len()
107 );
108109let diff = Self {
110 timestamps,
111 cumulative_difficulties,
112 last_accounted_height: chain_height - 1,
113 config,
114 };
115116Ok(diff)
117 }
118119/// 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))]
127pub async fn pop_blocks_main_chain<D: Database + Clone>(
128&mut self,
129 numb_blocks: usize,
130 database: D,
131 ) -> Result<(), ContextCacheError> {
132let 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(
135self.last_accounted_height - numb_blocks + 1,
136self.config,
137 database,
138 Chain::Main,
139 )
140 .await?;
141142return Ok(());
143 };
144145let current_chain_height = self.last_accounted_height + 1;
146147let mut new_start_height = current_chain_height
148 .saturating_sub(self.config.total_block_count())
149 .saturating_sub(numb_blocks);
150151// skip the genesis block.
152if new_start_height == 0 {
153 new_start_height = 1;
154 }
155156let (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?;
164165self.timestamps.drain(retained_blocks..);
166self.cumulative_difficulties.drain(retained_blocks..);
167 timestamps.append(&mut self.timestamps);
168 cumulative_difficulties.append(&mut self.cumulative_difficulties);
169170self.timestamps = timestamps;
171self.cumulative_difficulties = cumulative_difficulties;
172self.last_accounted_height -= numb_blocks;
173174assert_eq!(self.timestamps.len(), self.cumulative_difficulties.len());
175176Ok(())
177 }
178179/// Add a new block to the difficulty cache.
180pub fn new_block(&mut self, height: usize, timestamp: u64, cumulative_difficulty: u128) {
181assert_eq!(self.last_accounted_height + 1, height);
182self.last_accounted_height += 1;
183184tracing::debug!(
185"Accounting for new blocks timestamp ({timestamp}) and cumulative_difficulty ({cumulative_difficulty})",
186 );
187188self.timestamps.push_back(timestamp);
189self.cumulative_difficulties
190 .push_back(cumulative_difficulty);
191192if self.timestamps.len() > self.config.total_block_count() {
193self.timestamps.pop_front();
194self.cumulative_difficulties.pop_front();
195 }
196 }
197198/// Returns the required difficulty for the next block.
199 ///
200 /// See: <https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#calculating-difficulty>
201pub fn next_difficulty(&self, hf: HardFork) -> u128 {
202 next_difficulty(
203&self.config,
204&self.timestamps,
205&self.cumulative_difficulties,
206 hf,
207 )
208 }
209210/// 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.
217pub fn next_difficulties(
218&self,
219 blocks: Vec<(u64, HardFork)>,
220 current_hf: HardFork,
221 ) -> Vec<u128> {
222let mut timestamps = self.timestamps.clone();
223let mut cumulative_difficulties = self.cumulative_difficulties.clone();
224225let mut difficulties = Vec::with_capacity(blocks.len() + 1);
226227 difficulties.push(self.next_difficulty(current_hf));
228229for (new_timestamp, hf) in blocks {
230 timestamps.push_back(new_timestamp);
231232let last_cum_diff = cumulative_difficulties.back().copied().unwrap_or(1);
233 cumulative_difficulties.push_back(last_cum_diff + *difficulties.last().unwrap());
234235if timestamps.len() > self.config.total_block_count() {
236 timestamps.pop_front().unwrap();
237 cumulative_difficulties.pop_front().unwrap();
238 }
239240 difficulties.push(next_difficulty(
241&self.config,
242×tamps,
243&cumulative_difficulties,
244 hf,
245 ));
246 }
247248 difficulties
249 }
250251/// 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.
254pub fn median_timestamp(&self, numb_blocks: usize) -> Option<u64> {
255let 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.
259let 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
262timestamps.push_front(0);
263 timestamps.into()
264 } else {
265self.timestamps
266 .range(self.timestamps.len().checked_sub(numb_blocks)?..)
267 .copied()
268 .collect::<Vec<_>>()
269 };
270 timestamps.sort_unstable();
271debug_assert_eq!(timestamps.len(), numb_blocks);
272273Some(median(×tamps))
274 }
275276/// Returns the cumulative difficulty of the chain.
277pub fn cumulative_difficulty(&self) -> u128 {
278// the genesis block has a difficulty of 1
279self.cumulative_difficulties.back().copied().unwrap_or(1)
280 }
281282/// Returns the top block's timestamp, returns [`None`] if the top block is the genesis block.
283pub fn top_block_timestamp(&self) -> Option<u64> {
284self.timestamps.back().copied()
285 }
286}
287288/// 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 {
295if let Some(fixed_difficulty) = config.fixed_difficulty {
296return fixed_difficulty;
297 }
298299if timestamps.len() <= 1 {
300return 1;
301 }
302303let mut timestamps = timestamps.clone();
304305if timestamps.len() > config.window {
306// remove the lag.
307timestamps.drain(config.window..);
308 };
309let timestamps_slice = timestamps.make_contiguous();
310311let (window_start, window_end) = get_window_start_and_end(
312 timestamps_slice.len(),
313 config.accounted_window_len(),
314 config.window,
315 );
316317// We don't sort the whole timestamp list
318let 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 );
322323let windowed_work =
324 cumulative_difficulties[window_end - 1] - cumulative_difficulties[window_start];
325326if time_span == 0 {
327 time_span = 1;
328 }
329330// 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}
333334/// 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) {
340debug_assert!(window > accounted_window);
341342let window_len = if window_len > window {
343 window
344 } else {
345 window_len
346 };
347348if window_len <= accounted_window {
349 (0, window_len)
350 } else {
351let start = (window_len - (accounted_window) + 1) / 2;
352 (start, start + accounted_window)
353 }
354}
355356/// 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> {
363tracing::info!("Getting blocks timestamps");
364365let BlockchainResponse::BlockExtendedHeaderInRange(ext_header) = database
366 .oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
367 block_heights,
368 chain,
369 ))
370 .await?
371else {
372panic!("Database sent incorrect response");
373 };
374375Ok(ext_header
376 .into_iter()
377 .map(|info| (info.timestamp, info.cumulative_difficulty))
378 .unzip())
379}