use std::{collections::VecDeque, ops::Range};
use tower::ServiceExt;
use tracing::instrument;
use cuprate_helper::num::median;
use cuprate_types::{
blockchain::{BlockchainReadRequest, BlockchainResponse},
Chain,
};
use crate::{Database, ExtendedConsensusError, HardFork};
const DIFFICULTY_WINDOW: usize = 720;
const DIFFICULTY_CUT: usize = 60;
const DIFFICULTY_LAG: usize = 15;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct DifficultyCacheConfig {
pub(crate) window: usize,
pub(crate) cut: usize,
pub(crate) lag: usize,
}
impl DifficultyCacheConfig {
pub const fn new(window: usize, cut: usize, lag: usize) -> DifficultyCacheConfig {
DifficultyCacheConfig { window, cut, lag }
}
pub fn total_block_count(&self) -> usize {
self.window + self.lag
}
pub fn accounted_window_len(&self) -> usize {
self.window - 2 * self.cut
}
pub const fn main_net() -> DifficultyCacheConfig {
DifficultyCacheConfig {
window: DIFFICULTY_WINDOW,
cut: DIFFICULTY_CUT,
lag: DIFFICULTY_LAG,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DifficultyCache {
pub(crate) timestamps: VecDeque<u64>,
pub(crate) cumulative_difficulties: VecDeque<u128>,
pub(crate) last_accounted_height: usize,
pub(crate) config: DifficultyCacheConfig,
}
impl DifficultyCache {
#[instrument(name = "init_difficulty_cache", level = "info", skip(database, config))]
pub async fn init_from_chain_height<D: Database + Clone>(
chain_height: usize,
config: DifficultyCacheConfig,
database: D,
chain: Chain,
) -> Result<Self, ExtendedConsensusError> {
tracing::info!("Initializing difficulty cache this may take a while.");
let mut block_start = chain_height.saturating_sub(config.total_block_count());
if block_start == 0 {
block_start = 1;
}
let (timestamps, cumulative_difficulties) =
get_blocks_in_pow_info(database.clone(), block_start..chain_height, chain).await?;
debug_assert_eq!(timestamps.len(), chain_height - block_start);
tracing::info!(
"Current chain height: {}, accounting for {} blocks timestamps",
chain_height,
timestamps.len()
);
let diff = DifficultyCache {
timestamps,
cumulative_difficulties,
last_accounted_height: chain_height - 1,
config,
};
Ok(diff)
}
#[instrument(name = "pop_blocks_diff_cache", skip_all, fields(numb_blocks = numb_blocks))]
pub async fn pop_blocks_main_chain<D: Database + Clone>(
&mut self,
numb_blocks: usize,
database: D,
) -> Result<(), ExtendedConsensusError> {
let Some(retained_blocks) = self.timestamps.len().checked_sub(numb_blocks) else {
*self = Self::init_from_chain_height(
self.last_accounted_height - numb_blocks + 1,
self.config,
database,
Chain::Main,
)
.await?;
return Ok(());
};
let current_chain_height = self.last_accounted_height + 1;
let mut new_start_height = current_chain_height
.saturating_sub(self.config.total_block_count())
.saturating_sub(numb_blocks);
if new_start_height == 0 {
new_start_height = 1;
}
let (mut timestamps, mut cumulative_difficulties) = get_blocks_in_pow_info(
database,
new_start_height
..(current_chain_height - self.timestamps.len()),
Chain::Main,
)
.await?;
self.timestamps.drain(retained_blocks..);
self.cumulative_difficulties.drain(retained_blocks..);
timestamps.append(&mut self.timestamps);
cumulative_difficulties.append(&mut self.cumulative_difficulties);
self.timestamps = timestamps;
self.cumulative_difficulties = cumulative_difficulties;
self.last_accounted_height -= numb_blocks;
assert_eq!(self.timestamps.len(), self.cumulative_difficulties.len());
Ok(())
}
pub fn new_block(&mut self, height: usize, timestamp: u64, cumulative_difficulty: u128) {
assert_eq!(self.last_accounted_height + 1, height);
self.last_accounted_height += 1;
tracing::debug!(
"Accounting for new blocks timestamp ({timestamp}) and cumulative_difficulty ({cumulative_difficulty})",
);
self.timestamps.push_back(timestamp);
self.cumulative_difficulties
.push_back(cumulative_difficulty);
if self.timestamps.len() > self.config.total_block_count() {
self.timestamps.pop_front();
self.cumulative_difficulties.pop_front();
}
}
pub fn next_difficulty(&self, hf: &HardFork) -> u128 {
next_difficulty(
&self.config,
&self.timestamps,
&self.cumulative_difficulties,
hf,
)
}
pub fn next_difficulties(
&self,
blocks: Vec<(u64, HardFork)>,
current_hf: &HardFork,
) -> Vec<u128> {
let mut timestamps = self.timestamps.clone();
let mut cumulative_difficulties = self.cumulative_difficulties.clone();
let mut difficulties = Vec::with_capacity(blocks.len() + 1);
difficulties.push(self.next_difficulty(current_hf));
let mut diff_info_popped = Vec::new();
for (new_timestamp, hf) in blocks {
timestamps.push_back(new_timestamp);
let last_cum_diff = cumulative_difficulties.back().copied().unwrap_or(1);
cumulative_difficulties.push_back(last_cum_diff + *difficulties.last().unwrap());
if timestamps.len() > self.config.total_block_count() {
diff_info_popped.push((
timestamps.pop_front().unwrap(),
cumulative_difficulties.pop_front().unwrap(),
));
}
difficulties.push(next_difficulty(
&self.config,
×tamps,
&cumulative_difficulties,
&hf,
));
}
difficulties
}
pub fn median_timestamp(&self, numb_blocks: usize) -> Option<u64> {
let mut timestamps = if self.last_accounted_height + 1 == numb_blocks {
let mut timestamps = self.timestamps.clone();
timestamps.push_front(0);
timestamps.into()
} else {
self.timestamps
.range(self.timestamps.len().checked_sub(numb_blocks)?..)
.copied()
.collect::<Vec<_>>()
};
timestamps.sort_unstable();
debug_assert_eq!(timestamps.len(), numb_blocks);
Some(median(×tamps))
}
pub fn cumulative_difficulty(&self) -> u128 {
self.cumulative_difficulties.back().copied().unwrap_or(1)
}
pub fn top_block_timestamp(&self) -> Option<u64> {
self.timestamps.back().copied()
}
}
fn next_difficulty(
config: &DifficultyCacheConfig,
timestamps: &VecDeque<u64>,
cumulative_difficulties: &VecDeque<u128>,
hf: &HardFork,
) -> u128 {
if timestamps.len() <= 1 {
return 1;
}
let mut timestamps = timestamps.clone();
if timestamps.len() > config.window {
timestamps.drain(config.window..);
};
let timestamps_slice = timestamps.make_contiguous();
let (window_start, window_end) = get_window_start_and_end(
timestamps_slice.len(),
config.accounted_window_len(),
config.window,
);
let mut time_span = u128::from(
*timestamps_slice.select_nth_unstable(window_end - 1).1
- *timestamps_slice.select_nth_unstable(window_start).1,
);
let windowed_work =
cumulative_difficulties[window_end - 1] - cumulative_difficulties[window_start];
if time_span == 0 {
time_span = 1;
}
(windowed_work * hf.block_time().as_secs() as u128 + time_span - 1) / time_span
}
fn get_window_start_and_end(
window_len: usize,
accounted_window: usize,
window: usize,
) -> (usize, usize) {
debug_assert!(window > accounted_window);
let window_len = if window_len > window {
window
} else {
window_len
};
if window_len <= accounted_window {
(0, window_len)
} else {
let start = (window_len - (accounted_window) + 1) / 2;
(start, start + accounted_window)
}
}
#[instrument(name = "get_blocks_timestamps", skip(database), level = "info")]
async fn get_blocks_in_pow_info<D: Database + Clone>(
database: D,
block_heights: Range<usize>,
chain: Chain,
) -> Result<(VecDeque<u64>, VecDeque<u128>), ExtendedConsensusError> {
tracing::info!("Getting blocks timestamps");
let BlockchainResponse::BlockExtendedHeaderInRange(ext_header) = database
.oneshot(BlockchainReadRequest::BlockExtendedHeaderInRange(
block_heights,
chain,
))
.await?
else {
panic!("Database sent incorrect response");
};
Ok(ext_header
.into_iter()
.map(|info| (info.timestamp, info.cumulative_difficulty))
.unzip())
}