use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use zakura_chain::block::{self, Height};
use zakura_header_chain::Frontier;
use crate::{
service::{
finalized_state::ZakuraDb,
non_finalized_state::{Chain, NonFinalizedState},
read::find::tip,
},
ContextuallyVerifiedBlock,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ChainTipStatus {
Active,
ValidFork,
HeadersOnly,
Invalid,
}
#[derive(Copy, Clone, Debug)]
pub struct SelectedHeaders<'a> {
pub tip: Frontier,
pub overlap: &'a [Frontier],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ChainTipInfo {
pub height: Height,
pub hash: block::Hash,
pub branch_len: u32,
pub status: ChainTipStatus,
}
pub fn chain_tips(
non_finalized_state: &NonFinalizedState,
db: &ZakuraDb,
selected_headers: Option<SelectedHeaders<'_>>,
) -> Vec<ChainTipInfo> {
let best_chain = non_finalized_state.best_chain();
let Some((best_height, best_hash)) = tip(best_chain, db) else {
return Vec::new();
};
let mut tips = vec![ChainTipInfo {
height: best_height,
hash: best_hash,
branch_len: 0,
status: ChainTipStatus::Active,
}];
let mut seen: HashSet<block::Hash> = HashSet::from([best_hash]);
let invalidated_branches = non_finalized_state.invalidated_blocks();
let mut invalidated_parents: HashSet<block::Hash> = HashSet::new();
let mut invalidated_by_hash: HashMap<block::Hash, (Height, block::Hash)> = HashMap::new();
for blocks in invalidated_branches.values() {
for invalidated_block in blocks.iter() {
let parent = invalidated_block.block.header.previous_block_hash;
invalidated_parents.insert(parent);
invalidated_by_hash.insert(invalidated_block.hash, (invalidated_block.height, parent));
}
}
let chains: Vec<&Arc<Chain>> = non_finalized_state.chain_iter().collect();
for (index, chain) in chains.iter().enumerate() {
let hash = chain.non_finalized_tip_hash();
if !seen.insert(hash) {
continue;
}
let has_successor = chains.iter().enumerate().any(|(other_index, other)| {
other_index != index
&& other.non_finalized_tip_hash() != hash
&& other.contains_block_hash(hash)
});
if has_successor || invalidated_parents.contains(&hash) {
continue;
}
let height = chain.non_finalized_tip_height();
let fork_height = fork_height(chain, best_chain);
tips.push(ChainTipInfo {
height,
hash,
branch_len: height.0.saturating_sub(fork_height.0),
status: ChainTipStatus::ValidFork,
});
}
for blocks in invalidated_branches.values() {
let (Some(root_block), Some(tip_block)) = (blocks.first(), blocks.last()) else {
continue;
};
if !seen.insert(tip_block.hash) {
continue;
}
if invalidated_parents.contains(&tip_block.hash) {
continue;
}
let fork_height =
invalidated_fork_height(root_block, &invalidated_by_hash, &chains, best_chain, db);
tips.push(ChainTipInfo {
height: tip_block.height,
hash: tip_block.hash,
branch_len: tip_block.height.0.saturating_sub(fork_height.0),
status: ChainTipStatus::Invalid,
});
}
if let Some(SelectedHeaders {
tip: header_tip,
overlap,
}) = selected_headers
{
let body_available = chains
.iter()
.any(|chain| chain.contains_block_hash(header_tip.hash))
|| db.height(header_tip.hash).is_some()
|| invalidated_branches
.values()
.any(|blocks| blocks.iter().any(|block| block.hash == header_tip.hash));
if !body_available {
let fork_height = overlap.iter().rev().find_map(|header| {
(best_chain_hash_at_height(best_chain, db, header.height) == Some(header.hash))
.then_some(header.height)
});
if let Some(fork_height) = fork_height {
if seen.insert(header_tip.hash) {
tips.push(ChainTipInfo {
height: header_tip.height,
hash: header_tip.hash,
branch_len: header_tip.height.0.saturating_sub(fork_height.0),
status: ChainTipStatus::HeadersOnly,
});
}
}
}
}
tips.sort_by(|a, b| {
b.height
.cmp(&a.height)
.then_with(|| a.hash.0.cmp(&b.hash.0))
});
tips
}
fn invalidated_fork_height(
root_block: &ContextuallyVerifiedBlock,
invalidated_by_hash: &HashMap<block::Hash, (Height, block::Hash)>,
chains: &[&Arc<Chain>],
best_chain: Option<&Arc<Chain>>,
db: &ZakuraDb,
) -> Height {
let mut root_height = root_block.height;
let mut parent = root_block.block.header.previous_block_hash;
for _ in 0..invalidated_by_hash.len() {
let Some(&(height, next_parent)) = invalidated_by_hash.get(&parent) else {
break;
};
root_height = height;
parent = next_parent;
}
let parent_height = Height(root_height.0.saturating_sub(1));
if best_chain_hash_at_height(best_chain, db, parent_height) == Some(parent) {
return parent_height;
}
chains
.iter()
.find(|chain| chain.contains_block_hash(parent))
.map(|chain| fork_height(chain, best_chain))
.unwrap_or(parent_height)
}
fn best_chain_hash_at_height(
best_chain: Option<&Arc<Chain>>,
db: &ZakuraDb,
height: Height,
) -> Option<block::Hash> {
best_chain
.and_then(|chain| chain.hash_by_height(height))
.or_else(|| db.hash(height))
}
fn fork_height(chain: &Arc<Chain>, best_chain: Option<&Arc<Chain>>) -> Height {
let root_height = chain.non_finalized_root_height();
let tip_height = chain.non_finalized_tip_height();
if let Some(best_chain) = best_chain {
for height in (root_height.0..=tip_height.0).rev() {
let height = Height(height);
if let Some(hash) = chain.hash_by_height(height) {
if best_chain.contains_block_hash(hash) {
return height;
}
}
}
}
Height(root_height.0.saturating_sub(1))
}