use alloy_consensus::BlockHeader;
use alloy_eips::BlockNumHash;
use alloy_primitives::BlockNumber;
use parking_lot::RwLock;
use reth_chainspec::ChainInfo;
use reth_primitives::{NodePrimitives, SealedHeader};
use std::{
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Instant,
};
use tokio::sync::watch;
#[derive(Debug, Clone)]
pub struct ChainInfoTracker<N: NodePrimitives> {
inner: Arc<ChainInfoInner<N>>,
}
impl<N> ChainInfoTracker<N>
where
N: NodePrimitives,
N::BlockHeader: BlockHeader,
{
pub fn new(
head: SealedHeader<N::BlockHeader>,
finalized: Option<SealedHeader<N::BlockHeader>>,
safe: Option<SealedHeader<N::BlockHeader>>,
) -> Self {
let (finalized_block, _) = watch::channel(finalized);
let (safe_block, _) = watch::channel(safe);
Self {
inner: Arc::new(ChainInfoInner {
last_forkchoice_update: RwLock::new(None),
last_transition_configuration_exchange: RwLock::new(None),
canonical_head_number: AtomicU64::new(head.number()),
canonical_head: RwLock::new(head),
safe_block,
finalized_block,
}),
}
}
pub fn chain_info(&self) -> ChainInfo {
let inner = self.inner.canonical_head.read();
ChainInfo { best_hash: inner.hash(), best_number: inner.number() }
}
pub fn on_forkchoice_update_received(&self) {
self.inner.last_forkchoice_update.write().replace(Instant::now());
}
pub fn last_forkchoice_update_received_at(&self) -> Option<Instant> {
*self.inner.last_forkchoice_update.read()
}
pub fn on_transition_configuration_exchanged(&self) {
self.inner.last_transition_configuration_exchange.write().replace(Instant::now());
}
pub fn last_transition_configuration_exchanged_at(&self) -> Option<Instant> {
*self.inner.last_transition_configuration_exchange.read()
}
pub fn get_canonical_head(&self) -> SealedHeader<N::BlockHeader> {
self.inner.canonical_head.read().clone()
}
pub fn get_safe_header(&self) -> Option<SealedHeader<N::BlockHeader>> {
self.inner.safe_block.borrow().clone()
}
pub fn get_finalized_header(&self) -> Option<SealedHeader<N::BlockHeader>> {
self.inner.finalized_block.borrow().clone()
}
#[allow(dead_code)]
pub fn get_canonical_num_hash(&self) -> BlockNumHash {
self.inner.canonical_head.read().num_hash()
}
pub fn get_canonical_block_number(&self) -> BlockNumber {
self.inner.canonical_head_number.load(Ordering::Relaxed)
}
pub fn get_safe_num_hash(&self) -> Option<BlockNumHash> {
self.inner.safe_block.borrow().as_ref().map(SealedHeader::num_hash)
}
pub fn get_finalized_num_hash(&self) -> Option<BlockNumHash> {
self.inner.finalized_block.borrow().as_ref().map(SealedHeader::num_hash)
}
pub fn set_canonical_head(&self, header: SealedHeader<N::BlockHeader>) {
let number = header.number();
*self.inner.canonical_head.write() = header;
self.inner.canonical_head_number.store(number, Ordering::Relaxed);
}
pub fn set_safe(&self, header: SealedHeader<N::BlockHeader>) {
self.inner.safe_block.send_if_modified(|current_header| {
if current_header.as_ref().map(SealedHeader::hash) != Some(header.hash()) {
let _ = current_header.replace(header);
return true
}
false
});
}
pub fn set_finalized(&self, header: SealedHeader<N::BlockHeader>) {
self.inner.finalized_block.send_if_modified(|current_header| {
if current_header.as_ref().map(SealedHeader::hash) != Some(header.hash()) {
let _ = current_header.replace(header);
return true
}
false
});
}
pub fn subscribe_finalized_block(
&self,
) -> watch::Receiver<Option<SealedHeader<N::BlockHeader>>> {
self.inner.finalized_block.subscribe()
}
pub fn subscribe_safe_block(&self) -> watch::Receiver<Option<SealedHeader<N::BlockHeader>>> {
self.inner.safe_block.subscribe()
}
}
#[derive(Debug)]
struct ChainInfoInner<N: NodePrimitives = reth_primitives::EthPrimitives> {
last_forkchoice_update: RwLock<Option<Instant>>,
last_transition_configuration_exchange: RwLock<Option<Instant>>,
canonical_head_number: AtomicU64,
canonical_head: RwLock<SealedHeader<N::BlockHeader>>,
safe_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
finalized_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
}
#[cfg(test)]
mod tests {
use super::*;
use reth_primitives::EthPrimitives;
use reth_testing_utils::{generators, generators::random_header};
#[test]
fn test_chain_info() {
let mut rng = generators::rng();
let header = random_header(&mut rng, 10, None);
let tracker: ChainInfoTracker<EthPrimitives> =
ChainInfoTracker::new(header.clone(), None, None);
let chain_info = tracker.chain_info();
assert_eq!(chain_info.best_number, header.number);
assert_eq!(chain_info.best_hash, header.hash());
}
#[test]
fn test_on_forkchoice_update_received() {
let mut rng = generators::rng();
let header = random_header(&mut rng, 10, None);
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header, None, None);
assert!(tracker.last_forkchoice_update_received_at().is_none());
tracker.on_forkchoice_update_received();
assert!(tracker.last_forkchoice_update_received_at().is_some());
}
#[test]
fn test_on_transition_configuration_exchanged() {
let mut rng = generators::rng();
let header = random_header(&mut rng, 10, None);
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header, None, None);
assert!(tracker.last_transition_configuration_exchanged_at().is_none());
tracker.on_transition_configuration_exchanged();
assert!(tracker.last_transition_configuration_exchanged_at().is_some());
}
#[test]
fn test_set_canonical_head() {
let mut rng = generators::rng();
let header1 = random_header(&mut rng, 10, None);
let header2 = random_header(&mut rng, 20, None);
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header1, None, None);
tracker.set_canonical_head(header2.clone());
let canonical_head = tracker.get_canonical_head();
assert_eq!(canonical_head, header2);
}
#[test]
fn test_set_safe() {
let mut rng = generators::rng();
let header1 = random_header(&mut rng, 10, None);
let header2 = random_header(&mut rng, 20, None);
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header1, None, None);
tracker.set_safe(header2.clone());
let safe_header = tracker.get_safe_header();
assert!(safe_header.is_some()); let safe_header = safe_header.unwrap();
assert_eq!(safe_header, header2);
tracker.set_safe(header2.clone());
let same_safe_header = tracker.get_safe_header();
assert!(same_safe_header.is_some());
let same_safe_header = same_safe_header.unwrap();
assert_eq!(same_safe_header, header2);
let header3 = random_header(&mut rng, 30, None);
tracker.set_safe(header3.clone());
let updated_safe_header = tracker.get_safe_header();
assert!(updated_safe_header.is_some());
let updated_safe_header = updated_safe_header.unwrap();
assert_eq!(updated_safe_header, header3);
}
#[test]
fn test_set_finalized() {
let mut rng = generators::rng();
let header1 = random_header(&mut rng, 10, None);
let header2 = random_header(&mut rng, 20, None);
let header3 = random_header(&mut rng, 30, None);
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header1, None, None);
assert!(tracker.get_finalized_header().is_none());
tracker.set_finalized(header2.clone());
let finalized_header = tracker.get_finalized_header();
assert!(finalized_header.is_some());
let finalized_header = finalized_header.unwrap();
assert_eq!(finalized_header, header2);
tracker.set_finalized(header2.clone());
let unchanged_finalized_header = tracker.get_finalized_header();
assert_eq!(unchanged_finalized_header.unwrap(), header2); tracker.set_finalized(header3.clone());
let updated_finalized_header = tracker.get_finalized_header();
assert!(updated_finalized_header.is_some());
assert_eq!(updated_finalized_header.unwrap(), header3);
}
#[test]
fn test_get_finalized_num_hash() {
let mut rng = generators::rng();
let finalized_header = random_header(&mut rng, 10, None);
let tracker: ChainInfoTracker<EthPrimitives> =
ChainInfoTracker::new(finalized_header.clone(), Some(finalized_header.clone()), None);
assert_eq!(tracker.get_finalized_num_hash(), Some(finalized_header.num_hash()));
}
#[test]
fn test_get_safe_num_hash() {
let mut rng = generators::rng();
let safe_header = random_header(&mut rng, 10, None);
let tracker: ChainInfoTracker<EthPrimitives> =
ChainInfoTracker::new(safe_header.clone(), None, None);
tracker.set_safe(safe_header.clone());
assert_eq!(tracker.get_safe_num_hash(), Some(safe_header.num_hash()));
}
}