use ethrex_common::{
H256,
types::{BlockHash, BlockHeader, BlockNumber},
};
use ethrex_metrics::metrics;
use ethrex_storage::{Store, error::StoreError};
use tracing::{error, warn};
use crate::{
error::{self, InvalidForkChoice},
is_canonical,
};
pub const REORG_DEPTH_LIMIT: u64 = 128;
pub async fn apply_fork_choice(
store: &Store,
head_hash: H256,
safe_hash: H256,
finalized_hash: H256,
) -> Result<BlockHeader, InvalidForkChoice> {
if head_hash.is_zero() {
return Err(InvalidForkChoice::InvalidHeadHash);
}
let finalized_res = if !finalized_hash.is_zero() {
store.get_block_header_by_hash(finalized_hash)?
} else {
None
};
let safe_res = if !safe_hash.is_zero() {
store.get_block_header_by_hash(safe_hash)?
} else {
None
};
let head_res = store.get_block_header_by_hash(head_hash)?;
if !safe_hash.is_zero() {
check_order(&safe_res, &head_res)?;
}
if !finalized_hash.is_zero() && !safe_hash.is_zero() {
check_order(&finalized_res, &safe_res)?;
}
let Some(head) = head_res else {
return Err(InvalidForkChoice::Syncing);
};
let latest = store.get_latest_block_number().await?;
let head_is_canonical = is_canonical(store, head.number, head_hash).await?;
if let Some(stored_finalized) = store.get_finalized_block_number().await?
&& head.number < latest
&& head.number <= stored_finalized
&& head_is_canonical
{
return Err(InvalidForkChoice::NewHeadAlreadyCanonical);
}
let Some(new_canonical_blocks) = find_link_with_canonical_chain(store, &head).await? else {
return Err(InvalidForkChoice::UnlinkedHead);
};
let (link_block_number, link_block_hash) = match new_canonical_blocks.last() {
Some((number, hash)) => (*number, *hash),
None => (head.number, head_hash),
};
if let Some(ref finalized) = finalized_res
&& !((is_canonical(store, finalized.number, finalized_hash).await?
&& finalized.number <= link_block_number)
|| (finalized.number == head.number && finalized_hash == head_hash)
|| new_canonical_blocks.contains(&(finalized.number, finalized_hash)))
{
return Err(InvalidForkChoice::Disconnected(
error::ForkChoiceElement::Head,
error::ForkChoiceElement::Finalized,
));
}
if let Some(ref safe) = safe_res
&& !((is_canonical(store, safe.number, safe_hash).await?
&& safe.number <= link_block_number)
|| (safe.number == head.number && safe_hash == head_hash)
|| new_canonical_blocks.contains(&(safe.number, safe_hash)))
{
return Err(InvalidForkChoice::Disconnected(
error::ForkChoiceElement::Head,
error::ForkChoiceElement::Safe,
));
}
let canonical_link_height = if head_is_canonical {
head.number
} else {
new_canonical_blocks
.last()
.map(|(n, _)| *n)
.unwrap_or(head.number)
.saturating_sub(1)
};
let reorg_depth = latest.saturating_sub(canonical_link_height);
if reorg_depth > REORG_DEPTH_LIMIT {
return Err(InvalidForkChoice::TooDeepReorg {
reorg_depth,
limit: REORG_DEPTH_LIMIT,
});
}
let Some(link_header) = store.get_block_header_by_hash(link_block_hash)? else {
error!("Link block not found although it was just retrieved from the DB");
return Err(InvalidForkChoice::UnlinkedHead);
};
if !store.has_state_root(link_header.state_root)? {
warn!(
link_block=%link_block_hash,
link_number=%link_header.number,
head_number=%head.number,
"FCU head state not reachable from DB state. Starting sync toward head. This is expected if the consensus client is currently syncing."
);
return Err(InvalidForkChoice::StateNotReachable);
}
store
.forkchoice_update(
new_canonical_blocks,
head.number,
head_hash,
safe_res.map(|h| h.number),
finalized_res.map(|h| h.number),
)
.await?;
metrics!(
use ethrex_metrics::blocks::METRICS_BLOCKS;
METRICS_BLOCKS.set_head_height(head.number);
);
Ok(head)
}
fn check_order(
block_1: &Option<BlockHeader>,
block_2: &Option<BlockHeader>,
) -> Result<(), InvalidForkChoice> {
match (block_1, block_2) {
(None, Some(_)) => Err(InvalidForkChoice::ElementNotFound(
error::ForkChoiceElement::Finalized,
)),
(Some(b1), Some(b2)) => {
if b1.number > b2.number {
Err(InvalidForkChoice::Unordered)
} else {
Ok(())
}
}
_ => Err(InvalidForkChoice::Syncing),
}
}
async fn find_link_with_canonical_chain(
store: &Store,
block_header: &BlockHeader,
) -> Result<Option<Vec<(BlockNumber, BlockHash)>>, StoreError> {
let mut block_number = block_header.number;
let block_hash = block_header.hash();
let mut branch = Vec::new();
if is_canonical(store, block_number, block_hash).await? {
return Ok(Some(branch));
}
let genesis_number = store.get_earliest_block_number().await?;
let mut header = block_header.clone();
while block_number > genesis_number {
block_number -= 1;
let parent_hash = header.parent_hash;
let parent_header = match store.get_block_header_by_hash(parent_hash) {
Ok(Some(header)) => header,
Ok(None) => return Ok(None),
Err(error) => return Err(error),
};
if is_canonical(store, block_number, parent_hash).await? {
return Ok(Some(branch));
} else {
branch.push((block_number, parent_hash));
}
header = parent_header;
}
Ok(None)
}