use std::sync::Arc;
use chrono::{DateTime, Utc};
use zebra_chain::{
block::{self, Block, Hash, Height},
history_tree::HistoryTree,
parameters::{Network, NetworkUpgrade},
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 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 = if network.is_regtest() {
min_time
} else {
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");
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 {
use super::*;
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..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),
}
}
#[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)
);
}
}