use std::sync::Arc;
use chrono::{DateTime, Utc};
use zebra_chain::{
block::{self, Hash, Height},
history_tree::HistoryTree,
parameters::{Network, NetworkUpgrade},
serialization::{DateTime32, Duration32},
work::difficulty::{CompactDifficulty, PartialCumulativeWork, Work},
};
use crate::{
service::{
block_iter::any_chain_ancestor_iter,
check::{
difficulty::{
pow_adjustment_block_span, BLOCK_MAX_TIME_SINCE_MEDIAN, POW_MEDIAN_BLOCK_SPAN,
},
AdjustedDifficulty,
},
finalized_state::ZebraDb,
read::{
self, find::calculate_median_time_past, tree::history_tree,
FINALIZED_STATE_QUERY_RETRIES,
},
NonFinalizedState,
},
BoxError, GetBlockTemplateChainInfo,
};
pub fn get_block_template_chain_info(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
network: &Network,
) -> Result<GetBlockTemplateChainInfo, BoxError> {
let mut best_relevant_chain_and_history_tree_result =
best_relevant_chain_and_history_tree(non_finalized_state, db, network);
for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
if best_relevant_chain_and_history_tree_result.is_ok() {
break;
}
best_relevant_chain_and_history_tree_result =
best_relevant_chain_and_history_tree(non_finalized_state, db, network);
}
let (best_tip_height, best_tip_hash, best_relevant_chain, best_tip_history_tree) =
best_relevant_chain_and_history_tree_result?;
let chain_value_pools = {
let (_, tip_hash, chain_value_pools) =
read::find::tip_with_value_balance(non_finalized_state.best_chain(), db)?
.ok_or("Zebra's state is empty, wait until it syncs to the chain tip")?;
if tip_hash != best_tip_hash {
return Err("Zebra is committing too many blocks to the state, \
wait until it syncs to the chain tip"
.into());
}
chain_value_pools
};
difficulty_time_and_history_tree(
best_relevant_chain,
best_tip_height,
best_tip_hash,
network,
best_tip_history_tree,
chain_value_pools,
DateTime32::now(),
)
}
#[allow(unused)]
pub fn solution_rate(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
num_blocks: usize,
start_hash: Hash,
) -> Option<u128> {
let mut header_iter =
any_chain_ancestor_iter::<block::Header>(non_finalized_state, db, start_hash)
.take(num_blocks.checked_add(1).unwrap_or(num_blocks))
.peekable();
let get_work = |header: &block::Header| {
header
.difficulty_threshold
.to_work()
.expect("work has already been validated")
};
let last_header = header_iter.peek()?;
let mut min_time = last_header.time;
let mut max_time = last_header.time;
let mut last_work = Work::zero();
let mut total_work = PartialCumulativeWork::zero();
for header in header_iter {
min_time = min_time.min(header.time);
max_time = max_time.max(header.time);
last_work = get_work(&header);
total_work += last_work;
}
total_work -= last_work;
let work_duration = (max_time - min_time).num_seconds();
if work_duration <= 0 {
return None;
}
Some(total_work.as_u128() / work_duration as u128)
}
fn best_relevant_chain_and_history_tree(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
network: &Network,
) -> Result<
(
Height,
block::Hash,
Vec<(CompactDifficulty, DateTime<Utc>)>,
Arc<HistoryTree>,
),
BoxError,
> {
let state_tip_before_queries = read::best_tip(non_finalized_state, db).ok_or_else(|| {
BoxError::from("Zebra's state is empty, wait until it syncs to the chain tip")
})?;
let candidate_height = state_tip_before_queries.0.next()?;
let block_span = pow_adjustment_block_span(network, candidate_height);
let best_relevant_chain = any_chain_ancestor_iter::<block::Header>(
non_finalized_state,
db,
state_tip_before_queries.1,
)
.take(block_span)
.map(|header| (header.difficulty_threshold, header.time))
.collect::<Vec<_>>();
if best_relevant_chain.is_empty() {
return Err("missing genesis block, wait until it is committed".into());
};
let history_tree = history_tree(
non_finalized_state.best_chain(),
db,
state_tip_before_queries.into(),
)
.expect("tip hash should exist in the chain");
let state_tip_after_queries =
read::best_tip(non_finalized_state, db).expect("already checked for an empty tip");
if state_tip_before_queries != state_tip_after_queries {
return Err("Zebra is committing too many blocks to the state, \
wait until it syncs to the chain tip"
.into());
}
Ok((
state_tip_before_queries.0,
state_tip_before_queries.1,
best_relevant_chain,
history_tree,
))
}
fn difficulty_time_and_history_tree(
relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)>,
tip_height: Height,
tip_hash: block::Hash,
network: &Network,
history_tree: Arc<HistoryTree>,
chain_value_pools: zebra_chain::value_balance::ValueBalance<zebra_chain::amount::NonNegative>,
now: DateTime32,
) -> Result<GetBlockTemplateChainInfo, BoxError> {
let median_time_past = calculate_median_time_past(
relevant_data
.iter()
.take(POW_MEDIAN_BLOCK_SPAN)
.map(|(_, time)| *time)
.collect(),
);
let min_time = median_time_past
.checked_add(Duration32::from_seconds(1))
.ok_or("median-time-past leaves no representable block timestamp")?;
let max_time = now.saturating_add(Duration32::from_hours(2));
let candidate_height = (tip_height + 1).ok_or("next block height is out of range")?;
let max_time = if network.is_max_block_time_enforced(candidate_height) {
max_time.min(
median_time_past.saturating_add(Duration32::from_seconds(BLOCK_MAX_TIME_SINCE_MEDIAN)),
)
} else {
max_time
};
if min_time > max_time {
return Err("median-time-past is too far ahead of the local clock".into());
}
let cur_time = if network.is_regtest() {
min_time
} else {
now.clamp(min_time, max_time)
};
let difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
cur_time.into(),
tip_height,
network,
relevant_data.iter().cloned(),
);
let expected_difficulty = difficulty_adjustment.expected_difficulty_threshold();
let mut result = GetBlockTemplateChainInfo {
tip_hash,
tip_height,
chain_history_root: history_tree.hash(),
expected_difficulty,
cur_time,
min_time,
max_time,
chain_value_pools,
};
adjust_difficulty_and_time_for_testnet(&mut result, network, tip_height, relevant_data);
Ok(result)
}
fn adjust_difficulty_and_time_for_testnet(
result: &mut GetBlockTemplateChainInfo,
network: &Network,
previous_block_height: Height,
relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)>,
) {
if network == &Network::Mainnet {
return;
}
let previous_block_time = relevant_data.first().expect("has at least one block").1;
let previous_block_time: DateTime32 = previous_block_time
.try_into()
.expect("valid blocks have in-range times");
let candidate_height = (previous_block_height + 1).expect("next block height is valid");
let Some(minimum_difficulty_spacing) =
NetworkUpgrade::minimum_difficulty_spacing_for_height(network, candidate_height)
else {
return;
};
let minimum_difficulty_spacing: Duration32 = minimum_difficulty_spacing
.try_into()
.expect("small positive values are in-range");
let std_difficulty_max_time = previous_block_time.saturating_add(minimum_difficulty_spacing);
let min_difficulty_min_time =
std_difficulty_max_time.saturating_add(Duration32::from_seconds(1));
if result.cur_time <= std_difficulty_max_time {
result.max_time = std_difficulty_max_time.clamp(result.min_time, result.max_time);
result.cur_time = result.cur_time.clamp(result.min_time, result.max_time);
} else {
result.min_time = min_difficulty_min_time.clamp(result.min_time, result.max_time);
result.cur_time = result.cur_time.clamp(result.min_time, result.max_time);
result.expected_difficulty = AdjustedDifficulty::new_from_header_time(
result.cur_time.into(),
previous_block_height,
network,
relevant_data.iter().cloned(),
)
.expected_difficulty_threshold();
}
}
#[cfg(test)]
mod tests {
mod vectors;
use super::*;
use crate::service::check::difficulty::MAX_POW_ADJUSTMENT_BLOCK_SPAN;
use zebra_chain::work::difficulty::ParameterDifficulty as _;
const ACTIVE_HEIGHT: Height = Height(2_000_000);
const PREV: u32 = 1_600_000_000;
const GAP: u32 = 6 * 75;
fn recent_block_data(network: &Network) -> Vec<(CompactDifficulty, DateTime<Utc>)> {
let threshold = network.target_difficulty_limit().to_compact();
(0..MAX_POW_ADJUSTMENT_BLOCK_SPAN)
.map(|i| (threshold, DateTime32::from(PREV - i as u32).into()))
.collect()
}
fn chain_info(cur_time: u32) -> GetBlockTemplateChainInfo {
GetBlockTemplateChainInfo {
tip_hash: Hash([0; 32]),
tip_height: Height(0),
chain_history_root: None,
expected_difficulty: CompactDifficulty::default(),
cur_time: DateTime32::from(cur_time),
min_time: DateTime32::from(PREV - 100),
max_time: DateTime32::from(PREV + BLOCK_MAX_TIME_SINCE_MEDIAN),
chain_value_pools: Default::default(),
}
}
#[test]
fn eager_window_stays_standard_difficulty_and_is_not_future_dated() {
let network = Network::new_default_testnet();
let cur = PREV + 400;
let mut result = chain_info(cur);
adjust_difficulty_and_time_for_testnet(
&mut result,
&network,
ACTIVE_HEIGHT,
recent_block_data(&network),
);
assert_eq!(result.expected_difficulty, CompactDifficulty::default());
assert_eq!(result.cur_time, DateTime32::from(cur));
assert_eq!(result.max_time, DateTime32::from(PREV + GAP));
}
#[test]
fn past_the_threshold_switches_to_minimum_difficulty() {
let network = Network::new_default_testnet();
let cur = PREV + 500;
let mut result = chain_info(cur);
adjust_difficulty_and_time_for_testnet(
&mut result,
&network,
ACTIVE_HEIGHT,
recent_block_data(&network),
);
assert_eq!(
result.expected_difficulty,
network.target_difficulty_limit().to_compact()
);
assert_eq!(result.min_time, DateTime32::from(PREV + GAP + 1));
assert_eq!(result.cur_time, DateTime32::from(cur));
}
#[test]
fn threshold_is_inclusive_for_standard_difficulty() {
let network = Network::new_default_testnet();
let mut at_threshold = chain_info(PREV + GAP);
adjust_difficulty_and_time_for_testnet(
&mut at_threshold,
&network,
ACTIVE_HEIGHT,
recent_block_data(&network),
);
assert_eq!(
at_threshold.expected_difficulty,
CompactDifficulty::default()
);
let mut past_threshold = chain_info(PREV + GAP + 1);
adjust_difficulty_and_time_for_testnet(
&mut past_threshold,
&network,
ACTIVE_HEIGHT,
recent_block_data(&network),
);
assert_eq!(
past_threshold.expected_difficulty,
network.target_difficulty_limit().to_compact()
);
}
#[test]
fn mainnet_is_a_no_op() {
let network = Network::Mainnet;
let cur = PREV + 400;
let mut result = chain_info(cur);
adjust_difficulty_and_time_for_testnet(
&mut result,
&network,
ACTIVE_HEIGHT,
recent_block_data(&network),
);
assert_eq!(result.expected_difficulty, CompactDifficulty::default());
assert_eq!(result.cur_time, DateTime32::from(cur));
assert_eq!(result.min_time, DateTime32::from(PREV - 100));
assert_eq!(
result.max_time,
DateTime32::from(PREV + BLOCK_MAX_TIME_SINCE_MEDIAN)
);
}
#[test]
fn below_start_height_is_a_no_op() {
let network = Network::new_default_testnet();
let cur = PREV + 400;
let mut result = chain_info(cur);
adjust_difficulty_and_time_for_testnet(
&mut result,
&network,
Height(100_000),
recent_block_data(&network),
);
assert_eq!(result.expected_difficulty, CompactDifficulty::default());
assert_eq!(result.cur_time, DateTime32::from(cur));
assert_eq!(
result.max_time,
DateTime32::from(PREV + BLOCK_MAX_TIME_SINCE_MEDIAN)
);
}
}