use std::{
iter,
ops::{RangeBounds, RangeInclusive},
sync::Arc,
};
use chrono::{DateTime, Utc};
use zebra_chain::{
amount::NonNegative,
block::{self, Block, Height},
serialization::DateTime32,
value_balance::ValueBalance,
};
use crate::{
constants,
service::{
block_iter::any_ancestor_blocks,
check::{difficulty::POW_MEDIAN_BLOCK_SPAN, AdjustedDifficulty},
finalized_state::ZebraDb,
non_finalized_state::{Chain, NonFinalizedState},
read::{self, block::block_header, FINALIZED_STATE_QUERY_RETRIES},
},
BoxError, KnownBlock,
};
#[cfg(test)]
mod tests;
pub fn best_tip(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
) -> Option<(block::Height, block::Hash)> {
tip(non_finalized_state.best_chain(), db)
}
pub fn tip<C>(chain: Option<C>, db: &ZebraDb) -> Option<(Height, block::Hash)>
where
C: AsRef<Chain>,
{
chain
.map(|chain| chain.as_ref().non_finalized_tip())
.or_else(|| db.tip())
}
pub fn tip_block<C>(chain: Option<C>, db: &ZebraDb) -> Option<Arc<Block>>
where
C: AsRef<Chain>,
{
chain
.and_then(|chain| chain.as_ref().tip_block().map(|b| b.block.clone()))
.or_else(|| db.tip_block())
}
pub fn tip_height<C>(chain: Option<C>, db: &ZebraDb) -> Option<Height>
where
C: AsRef<Chain>,
{
tip(chain, db).map(|(height, _hash)| height)
}
#[allow(dead_code)]
pub fn tip_hash<C>(chain: Option<C>, db: &ZebraDb) -> Option<block::Hash>
where
C: AsRef<Chain>,
{
tip(chain, db).map(|(_height, hash)| hash)
}
pub fn tip_with_value_balance<C>(
chain: Option<C>,
db: &ZebraDb,
) -> Result<Option<(Height, block::Hash, ValueBalance<NonNegative>)>, BoxError>
where
C: AsRef<Chain>,
{
match chain.map(|chain| chain.as_ref().non_finalized_tip_with_value_balance()) {
tip_with_value_balance @ Some(_) => Ok(tip_with_value_balance),
None => {
for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
let tip @ Some((tip_height, tip_hash)) = db.tip() else {
return Ok(None);
};
let value_balance = db.finalized_value_pool();
if tip == db.tip() {
return Ok(Some((tip_height, tip_hash, value_balance)));
}
}
Err("Zebra is committing too many blocks to the state, \
wait until it syncs to the chain tip"
.into())
}
}
}
pub fn depth<C>(chain: Option<C>, db: &ZebraDb, hash: block::Hash) -> Option<u32>
where
C: AsRef<Chain>,
{
let chain = chain.as_ref();
let tip = tip_height(chain, db)?;
let height = height_by_hash(chain, db, hash)?;
Some(tip.0 - height.0)
}
pub fn non_finalized_state_contains_block_hash(
non_finalized_state: &NonFinalizedState,
hash: block::Hash,
) -> Option<KnownBlock> {
let mut chains_iter = non_finalized_state.chain_iter();
let is_hash_in_chain = |chain: &Arc<Chain>| chain.contains_block_hash(hash);
let best_chain = chains_iter.next();
match best_chain.map(is_hash_in_chain) {
Some(true) => Some(KnownBlock::BestChain),
Some(false) if chains_iter.any(is_hash_in_chain) => Some(KnownBlock::SideChain),
Some(false) | None => None,
}
}
pub fn finalized_state_contains_block_hash(db: &ZebraDb, hash: block::Hash) -> Option<KnownBlock> {
db.contains_hash(hash).then_some(KnownBlock::Finalized)
}
pub fn height_by_hash<C>(chain: Option<C>, db: &ZebraDb, hash: block::Hash) -> Option<Height>
where
C: AsRef<Chain>,
{
chain
.and_then(|chain| chain.as_ref().height_by_hash(hash))
.or_else(|| db.height(hash))
}
pub fn hash_by_height<C>(chain: Option<C>, db: &ZebraDb, height: Height) -> Option<block::Hash>
where
C: AsRef<Chain>,
{
chain
.and_then(|chain| chain.as_ref().hash_by_height(height))
.or_else(|| db.hash(height))
}
pub fn chain_contains_hash<C>(chain: Option<C>, db: &ZebraDb, hash: block::Hash) -> bool
where
C: AsRef<Chain>,
{
chain
.map(|chain| chain.as_ref().height_by_hash.contains_key(&hash))
.unwrap_or(false)
|| db.contains_hash(hash)
}
pub fn block_locator<C>(chain: Option<C>, db: &ZebraDb) -> Option<Vec<block::Hash>>
where
C: AsRef<Chain>,
{
let chain = chain.as_ref();
let tip_height = tip_height(chain, db)?;
let heights = block_locator_heights(tip_height);
let mut hashes = Vec::with_capacity(heights.len());
for height in heights {
if let Some(hash) = hash_by_height(chain, db, height) {
hashes.push(hash);
}
}
Some(hashes)
}
pub fn block_locator_heights(tip_height: block::Height) -> Vec<block::Height> {
let min_locator_height = tip_height
.0
.saturating_sub(constants::MAX_BLOCK_REORG_HEIGHT);
let exponential_locators = iter::successors(Some(1u32), |h| h.checked_mul(2))
.flat_map(move |step| tip_height.0.checked_sub(step));
let locators = iter::once(tip_height.0)
.chain(exponential_locators)
.take_while(move |&height| height > min_locator_height)
.chain(iter::once(min_locator_height))
.map(block::Height)
.collect();
tracing::debug!(
?tip_height,
?min_locator_height,
?locators,
"created block locator"
);
locators
}
fn find_chain_intersection<C>(
chain: Option<C>,
db: &ZebraDb,
known_blocks: Vec<block::Hash>,
) -> Option<block::Hash>
where
C: AsRef<Chain>,
{
if chain.is_none() && db.is_empty() {
return None;
}
let chain = chain.as_ref();
known_blocks
.iter()
.find(|&&hash| chain_contains_hash(chain, db, hash))
.cloned()
}
fn find_chain_height_range<C>(
chain: Option<C>,
db: &ZebraDb,
intersection: Option<block::Hash>,
stop: Option<block::Hash>,
max_len: u32,
) -> impl RangeBounds<u32> + Iterator<Item = u32>
where
C: AsRef<Chain>,
{
#[allow(clippy::reversed_empty_ranges)]
const EMPTY_RANGE: RangeInclusive<u32> = 1..=0;
assert!(max_len > 0, "max_len must be at least 1");
let chain = chain.as_ref();
let chain_tip_height = if let Some(height) = tip_height(chain, db) {
height
} else {
tracing::debug!(
response_len = ?0,
"responding to peer GetBlocks or GetHeaders with empty state",
);
return EMPTY_RANGE;
};
let intersection_height = match intersection {
Some(intersection_hash) => match height_by_hash(chain, db, intersection_hash) {
Some(intersection_height) => Some(intersection_height),
None => {
info!(
?intersection,
?stop,
?max_len,
"state found intersection but then dropped it, ignoring request",
);
return EMPTY_RANGE;
}
},
None => None,
};
let (start_height, max_height) = match intersection_height {
Some(intersection_height) => (
Height(intersection_height.0 + 1),
Height(intersection_height.0 + max_len),
),
None => (Height(0), Height(max_len - 1)),
};
let stop_height = stop.and_then(|hash| height_by_hash(chain, db, hash));
let final_height = std::cmp::min(max_height, chain_tip_height);
let final_height = stop_height
.map(|stop_height| std::cmp::min(final_height, stop_height))
.unwrap_or(final_height);
let height_range = start_height.0..=final_height.0;
let response_len = height_range.clone().count();
tracing::debug!(
?start_height,
?final_height,
?response_len,
?chain_tip_height,
?stop_height,
?intersection_height,
?intersection,
?stop,
?max_len,
"responding to peer GetBlocks or GetHeaders",
);
assert!(
response_len <= max_len.try_into().expect("fits in usize"),
"a Find response must not exceed the maximum response length",
);
height_range
}
fn collect_chain_hashes<C>(
chain: Option<C>,
db: &ZebraDb,
intersection: Option<block::Hash>,
stop: Option<block::Hash>,
max_len: u32,
) -> Vec<block::Hash>
where
C: AsRef<Chain>,
{
let chain = chain.as_ref();
let height_range = find_chain_height_range(chain, db, intersection, stop, max_len);
let hashes: Vec<block::Hash> = height_range.into_iter().map_while(|height| {
let hash = hash_by_height(chain, db, Height(height));
if hash.is_none() {
info!(
?intersection,
?stop,
?max_len,
"state found height range, but then partially dropped it, returning partial response",
);
}
tracing::trace!(
?hash,
?height,
?intersection,
?stop,
?max_len,
"adding hash to peer Find response",
);
hash
}).collect();
assert!(
intersection
.map(|hash| !hashes.contains(&hash))
.unwrap_or(true),
"the list must not contain the intersection hash",
);
if let (Some(stop), Some((_, hashes_except_last))) = (stop, hashes.split_last()) {
assert!(
!hashes_except_last.contains(&stop),
"if the stop hash is in the list, it must be the final hash",
);
}
hashes
}
fn collect_chain_headers<C>(
chain: Option<C>,
db: &ZebraDb,
intersection: Option<block::Hash>,
stop: Option<block::Hash>,
max_len: u32,
) -> Vec<Arc<block::Header>>
where
C: AsRef<Chain>,
{
let chain = chain.as_ref();
let height_range = find_chain_height_range(chain, db, intersection, stop, max_len);
height_range.into_iter().map_while(|height| {
let header = block_header(chain, db, Height(height).into());
if header.is_none() {
info!(
?intersection,
?stop,
?max_len,
"state found height range, but then partially dropped it, returning partial response",
);
}
tracing::trace!(
?height,
?intersection,
?stop,
?max_len,
"adding header to peer Find response",
);
header
}).collect()
}
pub fn find_chain_hashes<C>(
chain: Option<C>,
db: &ZebraDb,
known_blocks: Vec<block::Hash>,
stop: Option<block::Hash>,
max_len: u32,
) -> Vec<block::Hash>
where
C: AsRef<Chain>,
{
let chain = chain.as_ref();
let intersection = find_chain_intersection(chain, db, known_blocks);
collect_chain_hashes(chain, db, intersection, stop, max_len)
}
pub fn find_chain_headers<C>(
chain: Option<C>,
db: &ZebraDb,
known_blocks: Vec<block::Hash>,
stop: Option<block::Hash>,
max_len: u32,
) -> Vec<Arc<block::Header>>
where
C: AsRef<Chain>,
{
let chain = chain.as_ref();
let intersection = find_chain_intersection(chain, db, known_blocks);
collect_chain_headers(chain, db, intersection, stop, max_len)
}
pub fn next_median_time_past(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
) -> Result<DateTime32, BoxError> {
let mut best_relevant_chain_result = best_relevant_chain(non_finalized_state, db);
for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
if best_relevant_chain_result.is_ok() {
break;
}
best_relevant_chain_result = best_relevant_chain(non_finalized_state, db);
}
Ok(calculate_median_time_past(best_relevant_chain_result?))
}
fn best_relevant_chain(
non_finalized_state: &NonFinalizedState,
db: &ZebraDb,
) -> Result<Vec<Arc<Block>>, 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_MEDIAN_BLOCK_SPAN)
.collect();
if best_relevant_chain.is_empty() {
return Err("missing genesis block, wait until it is committed".into());
};
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(best_relevant_chain)
}
pub(crate) fn calculate_median_time_past(relevant_chain: Vec<Arc<Block>>) -> DateTime32 {
let relevant_data: Vec<DateTime<Utc>> = relevant_chain
.iter()
.map(|block| block.header.time)
.collect();
let median_time_past = AdjustedDifficulty::median_time(relevant_data);
DateTime32::try_from(median_time_past).expect("valid blocks have in-range times")
}