use std::sync::Arc;
use chrono::{DateTime, Utc};
use zebra_chain::{
block::{self, Block, Hash, Height},
history_tree::HistoryTree,
parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING},
serialization::{DateTime32, Duration32},
work::difficulty::{CompactDifficulty, PartialCumulativeWork, Work},
};
use crate::{
service::{
any_ancestor_blocks,
block_iter::any_chain_ancestor_iter,
check::{
difficulty::{
BLOCK_MAX_TIME_SINCE_MEDIAN, POW_ADJUSTMENT_BLOCK_SPAN, 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 const EXTRA_TIME_TO_MINE_A_BLOCK: u32 = POST_BLOSSOM_POW_TARGET_SPACING * 2;
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);
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);
}
let (best_tip_height, best_tip_hash, best_relevant_chain, best_tip_history_tree) =
best_relevant_chain_and_history_tree_result?;
Ok(difficulty_time_and_history_tree(
best_relevant_chain,
best_tip_height,
best_tip_hash,
network,
best_tip_history_tree,
))
}
#[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,
) -> Result<(Height, block::Hash, Vec<Arc<Block>>, 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 best_relevant_chain =
any_ancestor_blocks(non_finalized_state, db, state_tip_before_queries.1);
let best_relevant_chain: Vec<_> = best_relevant_chain
.into_iter()
.take(POW_ADJUSTMENT_BLOCK_SPAN)
.collect();
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_chain: Vec<Arc<Block>>,
tip_height: Height,
tip_hash: block::Hash,
network: &Network,
history_tree: Arc<HistoryTree>,
) -> GetBlockTemplateChainInfo {
let relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)> = relevant_chain
.iter()
.map(|block| (block.header.difficulty_threshold, block.header.time))
.collect();
let cur_time = DateTime32::now();
let median_time_past = calculate_median_time_past(
relevant_chain
.iter()
.take(POW_MEDIAN_BLOCK_SPAN)
.cloned()
.collect(),
);
let min_time = median_time_past
.checked_add(Duration32::from_seconds(1))
.expect("a valid block time plus a small constant is in-range");
let max_time = median_time_past
.checked_add(Duration32::from_seconds(BLOCK_MAX_TIME_SINCE_MEDIAN))
.expect("a valid block time plus a small constant is in-range");
let cur_time = cur_time.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,
};
adjust_difficulty_and_time_for_testnet(&mut result, network, tip_height, relevant_data);
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 Some(minimum_difficulty_spacing) =
NetworkUpgrade::minimum_difficulty_spacing_for_height(network, previous_block_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
.checked_add(minimum_difficulty_spacing)
.expect("a valid block time plus a small constant is in-range");
let min_difficulty_min_time = std_difficulty_max_time
.checked_add(Duration32::from_seconds(1))
.expect("a valid block time plus a small constant is in-range");
let local_std_difficulty_limit = std_difficulty_max_time
.checked_sub(Duration32::from_seconds(EXTRA_TIME_TO_MINE_A_BLOCK))
.expect("a valid block time minus a small constant is in-range");
if result.cur_time <= local_std_difficulty_limit {
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();
}
}