use std::{num::NonZeroUsize, sync::Arc};
use crate::beacon::{BeaconEntry, IGNORE_DRAND};
use crate::blocks::{Tipset, TipsetKey};
use crate::chain::Error;
use crate::metrics;
use crate::shim::clock::ChainEpoch;
use crate::utils::ShallowClone;
use crate::utils::cache::SizeTrackingLruCache;
use fvm_ipld_blockstore::Blockstore;
use itertools::Itertools;
use nonzero_ext::nonzero;
use num::Integer;
const DEFAULT_TIPSET_CACHE_SIZE: NonZeroUsize = nonzero!(2880_usize);
type TipsetCache = SizeTrackingLruCache<TipsetKey, Tipset>;
type TipsetHeightCache = SizeTrackingLruCache<ChainEpoch, TipsetKey>;
type IsTipsetFinalizedFn = Arc<dyn Fn(&Tipset) -> bool + Send + Sync>;
pub struct ChainIndex<DB> {
ts_cache: TipsetCache,
ts_height_cache: TipsetHeightCache,
db: Arc<DB>,
is_tipset_finalized: Option<IsTipsetFinalizedFn>,
}
impl<DB> ShallowClone for ChainIndex<DB> {
fn shallow_clone(&self) -> Self {
Self {
ts_cache: self.ts_cache.shallow_clone(),
ts_height_cache: self.ts_height_cache.shallow_clone(),
db: self.db.shallow_clone(),
is_tipset_finalized: self.is_tipset_finalized.clone(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum ResolveNullTipset {
TakeNewer,
TakeOlder,
}
impl<DB: Blockstore> ChainIndex<DB> {
pub fn new(db: Arc<DB>) -> Self {
let ts_cache =
SizeTrackingLruCache::new_with_metrics("tipset".into(), DEFAULT_TIPSET_CACHE_SIZE);
let ts_height_cache: SizeTrackingLruCache<ChainEpoch, TipsetKey> =
SizeTrackingLruCache::new_with_metrics(
"tipset_by_height".into(),
nonzero!(1048576_usize),
);
Self {
ts_cache,
ts_height_cache,
db,
is_tipset_finalized: None,
}
}
pub fn with_is_tipset_finalized(mut self, f: IsTipsetFinalizedFn) -> Self {
self.is_tipset_finalized = Some(f);
self
}
pub fn db(&self) -> &Arc<DB> {
&self.db
}
pub fn load_tipset(&self, tsk: &TipsetKey) -> Result<Option<Tipset>, Error> {
crate::def_is_env_truthy!(cache_disabled, "FOREST_TIPSET_CACHE_DISABLED");
if !cache_disabled()
&& let Some(ts) = self.ts_cache.get_cloned(tsk)
{
metrics::LRU_CACHE_HIT
.get_or_create(&metrics::values::TIPSET)
.inc();
return Ok(Some(ts));
}
let ts_opt = Tipset::load(&self.db, tsk)?;
if !cache_disabled()
&& let Some(ts) = &ts_opt
{
self.ts_cache.push(tsk.clone(), ts.clone());
metrics::LRU_CACHE_MISS
.get_or_create(&metrics::values::TIPSET)
.inc();
}
Ok(ts_opt)
}
pub fn load_required_tipset(&self, tsk: &TipsetKey) -> Result<Tipset, Error> {
self.load_tipset(tsk)?
.ok_or_else(|| Error::NotFound("Key for header".into()))
}
pub fn tipset_by_height(
&self,
to: ChainEpoch,
mut from: Tipset,
resolve: ResolveNullTipset,
) -> Result<Tipset, Error> {
use crate::shim::policy::policy_constants::CHAIN_FINALITY;
const CHECKPOINT_INTERVAL: ChainEpoch = 20;
fn next_checkpoint(epoch: ChainEpoch) -> ChainEpoch {
epoch - epoch.mod_floor(&CHECKPOINT_INTERVAL) + CHECKPOINT_INTERVAL
}
fn is_checkpoint(epoch: ChainEpoch) -> bool {
epoch.mod_floor(&CHECKPOINT_INTERVAL) == 0
}
let from_epoch = from.epoch();
let mut checkpoint_from_epoch = to;
while checkpoint_from_epoch < from_epoch {
if let Some(checkpoint_from_key) =
self.ts_height_cache.get_cloned(&checkpoint_from_epoch)
&& let Ok(Some(checkpoint_from)) = self.load_tipset(&checkpoint_from_key)
{
from = checkpoint_from;
break;
}
checkpoint_from_epoch = next_checkpoint(checkpoint_from_epoch);
}
if to == 0 {
return Ok(Tipset::from(from.genesis(&self.db)?));
}
if to > from.epoch() {
return Err(Error::Other(format!(
"looking for tipset with height greater than start point, req: {to}, head: {from}",
from = from.epoch()
)));
}
let from_epoch = from.epoch();
let is_finalized = |ts: &Tipset| {
if let Some(is_finalized_fn) = &self.is_tipset_finalized {
is_finalized_fn(ts)
} else {
ts.epoch() <= from_epoch - CHAIN_FINALITY
}
};
for (child, parent) in from.chain(&self.db).tuple_windows() {
if is_checkpoint(child.epoch()) && is_finalized(&child) {
self.ts_height_cache
.push(child.epoch(), child.key().clone());
}
if to == child.epoch() {
return Ok(child);
}
if to > parent.epoch() {
match resolve {
ResolveNullTipset::TakeOlder => return Ok(parent),
ResolveNullTipset::TakeNewer => return Ok(child),
}
}
}
Err(Error::Other(format!(
"Tipset with epoch={to} does not exist"
)))
}
pub fn latest_beacon_entry(&self, tipset: Tipset) -> Result<BeaconEntry, Error> {
for ts in tipset.chain(&self.db).take(20) {
if let Some(entry) = ts.min_ticket_block().beacon_entries.last() {
return Ok(entry.clone());
}
if ts.epoch() == 0 {
return Err(Error::Other(
"made it back to genesis block without finding beacon entry".to_owned(),
));
}
}
if *IGNORE_DRAND {
return Ok(BeaconEntry::new(0, vec![9; 16]));
}
Err(Error::Other(
"Found no beacon entries in the 20 latest tipsets".to_owned(),
))
}
}
#[cfg(test)]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use super::*;
use crate::blocks::{CachingBlockHeader, RawBlockHeader};
use crate::db::MemoryDB;
use crate::utils::db::CborStoreExt;
fn persist_tipset(tipset: &Tipset, db: &impl Blockstore) {
for block in tipset.block_headers() {
db.put_cbor_default(block).unwrap();
}
}
fn genesis_tipset() -> Tipset {
Tipset::from(CachingBlockHeader::default())
}
fn tipset_child(parent: &Tipset, epoch: ChainEpoch) -> Tipset {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
Tipset::from(CachingBlockHeader::new(RawBlockHeader {
parents: parent.key().clone(),
epoch,
timestamp: n,
..Default::default()
}))
}
#[test]
fn get_null_tipset() {
let db = Arc::new(MemoryDB::default());
let genesis = genesis_tipset();
let epoch1 = tipset_child(&genesis, 1);
let epoch3 = tipset_child(&epoch1, 3);
let epoch4 = tipset_child(&epoch3, 4);
persist_tipset(&genesis, &db);
persist_tipset(&epoch1, &db);
persist_tipset(&epoch3, &db);
persist_tipset(&epoch4, &db);
let index = ChainIndex::new(db);
assert_eq!(
index
.tipset_by_height(2, epoch4.clone(), ResolveNullTipset::TakeOlder)
.unwrap(),
epoch1
);
assert_eq!(
index
.tipset_by_height(2, epoch4, ResolveNullTipset::TakeNewer)
.unwrap(),
epoch3
);
}
#[test]
fn get_different_branches() {
let db = Arc::new(MemoryDB::default());
let genesis = genesis_tipset();
let epoch1 = tipset_child(&genesis, 1);
let epoch2a = tipset_child(&epoch1, 2);
let epoch3a = tipset_child(&epoch2a, 3);
let epoch2b = tipset_child(&epoch1, 2);
let epoch3b = tipset_child(&epoch2b, 3);
persist_tipset(&genesis, &db);
persist_tipset(&epoch1, &db);
persist_tipset(&epoch2a, &db);
persist_tipset(&epoch3a, &db);
persist_tipset(&epoch2b, &db);
persist_tipset(&epoch3b, &db);
let index = ChainIndex::new(db);
assert_eq!(
index
.tipset_by_height(2, epoch3a, ResolveNullTipset::TakeOlder)
.unwrap(),
epoch2a
);
assert_eq!(
index
.tipset_by_height(2, epoch3b, ResolveNullTipset::TakeOlder)
.unwrap(),
epoch2b
);
}
}