use std::cmp::{max, min};
use chrono::{DateTime, Duration, Utc};
use zebra_chain::{
block::{self, Block},
parameters::{Network, NetworkUpgrade, POW_AVERAGING_WINDOW},
work::difficulty::{CompactDifficulty, ExpandedDifficulty, ParameterDifficulty as _, U256},
BoundedVec,
};
pub const POW_MEDIAN_BLOCK_SPAN: usize = 11;
pub const POW_ADJUSTMENT_BLOCK_SPAN: usize = POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN;
pub const POW_DAMPING_FACTOR: i32 = 4;
pub const POW_MAX_ADJUST_UP_PERCENT: i32 = 16;
pub const POW_MAX_ADJUST_DOWN_PERCENT: i32 = 32;
pub const BLOCK_MAX_TIME_SINCE_MEDIAN: u32 = 90 * 60;
pub(crate) struct AdjustedDifficulty {
candidate_time: DateTime<Utc>,
candidate_height: block::Height,
network: Network,
relevant_difficulty_thresholds: BoundedVec<CompactDifficulty, 1, POW_ADJUSTMENT_BLOCK_SPAN>,
relevant_times: BoundedVec<DateTime<Utc>, 1, POW_ADJUSTMENT_BLOCK_SPAN>,
}
impl AdjustedDifficulty {
pub fn new_from_block<C>(
candidate_block: &Block,
network: &Network,
context: C,
) -> AdjustedDifficulty
where
C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
{
let candidate_block_height = candidate_block
.coinbase_height()
.expect("semantically valid blocks have a coinbase height");
let previous_block_height = (candidate_block_height - 1)
.expect("contextual validation is never run on the genesis block");
AdjustedDifficulty::new_from_header_time(
candidate_block.header.time,
previous_block_height,
network,
context,
)
}
pub fn new_from_header_time<C>(
candidate_header_time: DateTime<Utc>,
previous_block_height: block::Height,
network: &Network,
context: C,
) -> AdjustedDifficulty
where
C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
{
let candidate_height = (previous_block_height + 1).expect("next block height is valid");
let (thresholds, times) = context
.into_iter()
.take(POW_ADJUSTMENT_BLOCK_SPAN)
.unzip::<_, _, Vec<_>, Vec<_>>();
let relevant_difficulty_thresholds: BoundedVec<
CompactDifficulty,
1,
POW_ADJUSTMENT_BLOCK_SPAN,
> = thresholds
.try_into()
.expect("context must provide a bounded number of difficulty thresholds");
let relevant_times: BoundedVec<DateTime<Utc>, 1, POW_ADJUSTMENT_BLOCK_SPAN> = times
.try_into()
.expect("context must provide a bounded number of block times");
AdjustedDifficulty {
candidate_time: candidate_header_time,
candidate_height,
network: network.clone(),
relevant_difficulty_thresholds,
relevant_times,
}
}
pub fn candidate_height(&self) -> block::Height {
self.candidate_height
}
pub fn candidate_time(&self) -> DateTime<Utc> {
self.candidate_time
}
pub fn network(&self) -> Network {
self.network.clone()
}
pub fn expected_difficulty_threshold(&self) -> CompactDifficulty {
if NetworkUpgrade::is_testnet_min_difficulty_block(
&self.network,
self.candidate_height,
self.candidate_time,
*self.relevant_times.first(),
) {
assert!(
self.network.is_a_test_network(),
"invalid network: the minimum difficulty rule only applies on test networks"
);
self.network.target_difficulty_limit().to_compact()
} else {
self.threshold_bits()
}
}
fn threshold_bits(&self) -> CompactDifficulty {
let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
&self.network,
self.candidate_height,
);
let threshold = (self.mean_target_difficulty() / averaging_window_timespan.num_seconds())
* self.median_timespan_bounded().num_seconds();
let threshold = min(self.network.target_difficulty_limit(), threshold);
threshold.to_compact()
}
fn mean_target_difficulty(&self) -> ExpandedDifficulty {
let averaging_window_thresholds =
if self.relevant_difficulty_thresholds.len() >= POW_AVERAGING_WINDOW {
&self.relevant_difficulty_thresholds.as_slice()[0..POW_AVERAGING_WINDOW]
} else {
return self.network.target_difficulty_limit();
};
let total: ExpandedDifficulty = averaging_window_thresholds
.iter()
.map(|compact| {
compact
.to_expanded()
.expect("difficulty thresholds in previously verified blocks are valid")
})
.sum();
let divisor: U256 = POW_AVERAGING_WINDOW.into();
total / divisor
}
fn median_timespan_bounded(&self) -> Duration {
let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
&self.network,
self.candidate_height,
);
let damped_variance =
(self.median_timespan() - averaging_window_timespan) / POW_DAMPING_FACTOR;
let damped_variance = Duration::seconds(damped_variance.num_seconds());
let median_timespan_damped = averaging_window_timespan + damped_variance;
let min_median_timespan =
averaging_window_timespan * (100 - POW_MAX_ADJUST_UP_PERCENT) / 100;
let max_median_timespan =
averaging_window_timespan * (100 + POW_MAX_ADJUST_DOWN_PERCENT) / 100;
max(
min_median_timespan,
min(max_median_timespan, median_timespan_damped),
)
}
fn median_timespan(&self) -> Duration {
let newer_median = self.median_time_past();
let older_median = if self.relevant_times.len() > POW_AVERAGING_WINDOW {
let older_times: Vec<_> = self
.relevant_times
.iter()
.skip(POW_AVERAGING_WINDOW)
.cloned()
.take(POW_MEDIAN_BLOCK_SPAN)
.collect();
AdjustedDifficulty::median_time(older_times)
} else {
*self.relevant_times.last()
};
newer_median - older_median
}
pub fn median_time_past(&self) -> DateTime<Utc> {
let median_times: Vec<DateTime<Utc>> = self
.relevant_times
.iter()
.take(POW_MEDIAN_BLOCK_SPAN)
.cloned()
.collect();
AdjustedDifficulty::median_time(median_times)
}
pub(crate) fn median_time(mut median_block_span_times: Vec<DateTime<Utc>>) -> DateTime<Utc> {
median_block_span_times.sort_unstable();
let median_idx = median_block_span_times.len() / 2;
median_block_span_times[median_idx]
}
}