cuprate_consensus_context/
difficulty.rs1use 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
22const DIFFICULTY_WINDOW: usize = 720;
24const DIFFICULTY_CUT: usize = 60;
28const DIFFICULTY_LAG: usize = 15;
31
32#[derive(Debug, Clone, Copy, Eq, PartialEq)]
35pub struct DifficultyCacheConfig {
36 pub window: usize,
37 pub cut: usize,
38 pub lag: usize,
39 pub fixed_difficulty: Option<u128>,
41}
42
43impl DifficultyCacheConfig {
44 pub const fn total_block_count(&self) -> usize {
46 self.window + self.lag
47 }
48
49 pub const fn accounted_window_len(&self) -> usize {
51 self.window - 2 * self.cut
52 }
53
54 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#[derive(Debug, Clone, Eq, PartialEq)]
69pub struct DifficultyCache {
70 pub timestamps: VecDeque<u64>,
72 pub cumulative_difficulties: VecDeque<u128>,
74 pub last_accounted_height: usize,
76 pub config: DifficultyCacheConfig,
78}
79
80impl DifficultyCache {
81 #[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 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 #[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 *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 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()),
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 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 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 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 ×tamps,
243 &cumulative_difficulties,
244 hf,
245 ));
246 }
247
248 difficulties
249 }
250
251 pub fn median_timestamp(&self, numb_blocks: usize) -> Option<u64> {
255 let mut timestamps = if self.last_accounted_height + 1 == numb_blocks {
256 let mut timestamps = self.timestamps.clone();
260 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(×tamps))
274 }
275
276 pub fn cumulative_difficulty(&self) -> u128 {
278 self.cumulative_difficulties.back().copied().unwrap_or(1)
280 }
281
282 pub fn top_block_timestamp(&self) -> Option<u64> {
284 self.timestamps.back().copied()
285 }
286}
287
288fn 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 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 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 (windowed_work * u128::from(hf.block_time().as_secs())).div_ceil(time_span)
332}
333
334fn 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)).div_ceil(2);
352 (start, start + accounted_window)
353 }
354}
355
356#[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}