1use super::{
5 Error,
6 index::{ChainIndex, ResolveNullTipset},
7 tipset_tracker::TipsetTracker,
8};
9use crate::networks::{ChainConfig, Height};
10use crate::prelude::*;
11use crate::rpc::chain::PathChange;
12use crate::rpc::{
13 chain::ChainGetTipSetFinalityStatus,
14 eth::{eth_tx_from_signed_eth_message, types::EthHash},
15};
16use crate::shim::clock::ChainEpoch;
17use crate::shim::{
18 address::Address, executor::Receipt, message::Message, state_tree::StateTree,
19 version::NetworkVersion,
20};
21use crate::state_manager::ExecutedTipset;
22use crate::utils::db::{BlockstoreExt, CborStoreExt};
23use crate::utils::publisher::Publisher;
24use crate::{
25 blocks::{CachingBlockHeader, Tipset, TipsetKey, TxMeta},
26 db::{DbImpl, EthMappingsStoreExt as _, HeaviestTipsetKeyProvider},
27 message::{ChainMessage, SignedMessage},
28};
29use crate::{fil_cns, utils::cache::SizeTrackingCache};
30use crate::{
31 interpreter::{BlockMessages, VMTrace},
32 rpc::chain::PathChanges,
33};
34use ahash::HashMap;
35use arc_swap::{ArcSwap, ArcSwapOption};
36use fil_actors_shared::fvm_ipld_amt::Amtv0 as Amt;
37use fvm_ipld_encoding::CborStore;
38use nonzero_ext::nonzero;
39use serde::{Serialize, de::DeserializeOwned};
40use std::{
41 num::NonZeroUsize,
42 sync::atomic::{self, AtomicI64},
43};
44
45const VALIDATED_BLOCKS_CACHE_SIZE: NonZeroUsize = nonzero!(14400usize);
47
48pub type ChainEpochDelta = ChainEpoch;
51
52pub enum AtFinalityResolution {
54 ReorgStable(Address),
58 Unstable(Address),
61}
62
63impl AtFinalityResolution {
64 pub fn into_address(self) -> Address {
65 match self {
66 Self::ReorgStable(addr) | Self::Unstable(addr) => addr,
67 }
68 }
69}
70
71pub type HeadChange = PathChange<Tipset>;
74
75pub type HeadChanges = PathChanges<Tipset>;
76
77pub struct ChainStore {
81 head_changes: Publisher<HeadChanges>,
83
84 heaviest_tipset: Arc<ArcSwap<Tipset>>,
86
87 f3_finalized_tipset: Arc<ArcSwapOption<Tipset>>,
89
90 ec_calculator_finalized_epoch: Arc<AtomicI64>,
92
93 chain_index: ChainIndex,
95
96 tipset_tracker: TipsetTracker<DbImpl>,
98
99 genesis: Tipset,
101
102 pub(crate) validated_blocks: SizeTrackingCache<CidWrapper, ()>,
104
105 chain_config: Arc<ChainConfig>,
107
108 messages_in_tipset_cache: MessagesInTipsetCache,
110
111 last_clean_lookup_repair_head: Arc<ArcSwapOption<TipsetKey>>,
113}
114
115impl ShallowClone for ChainStore {
116 fn shallow_clone(&self) -> Self {
117 Self {
118 head_changes: self.head_changes.clone(),
119 heaviest_tipset: self.heaviest_tipset.shallow_clone(),
120 f3_finalized_tipset: self.f3_finalized_tipset.shallow_clone(),
121 ec_calculator_finalized_epoch: self.ec_calculator_finalized_epoch.shallow_clone(),
122 chain_index: self.chain_index.shallow_clone(),
123 tipset_tracker: self.tipset_tracker.shallow_clone(),
124 genesis: self.genesis.shallow_clone(),
125 validated_blocks: self.validated_blocks.shallow_clone(),
126 chain_config: self.chain_config.shallow_clone(),
127 messages_in_tipset_cache: self.messages_in_tipset_cache.shallow_clone(),
128 last_clean_lookup_repair_head: self.last_clean_lookup_repair_head.shallow_clone(),
129 }
130 }
131}
132
133impl ChainStore {
134 pub fn new(
135 db: impl Into<DbImpl>,
136 chain_config: Arc<ChainConfig>,
137 genesis: impl Into<Tipset>,
138 ) -> anyhow::Result<Self> {
139 let db = db.into();
140 let genesis = genesis.into();
141 anyhow::ensure!(genesis.epoch() == 0, "genesis tipset must be at epoch 0");
142 let head = if let Some(head_tsk) = db
143 .heaviest_tipset_key()
144 .context("failed to load head tipset key")?
145 {
146 Tipset::load_required(&db, &head_tsk)
147 .with_context(|| format!("failed to load head tipset with key {head_tsk}"))?
148 } else {
149 genesis.shallow_clone()
150 };
151 let heaviest_tipset = Arc::new(ArcSwap::from_pointee(head.shallow_clone()));
152 let f3_finalized_tipset: Arc<ArcSwapOption<Tipset>> = Default::default();
153 let chain_index = ChainIndex::new(db.shallow_clone(), genesis.shallow_clone());
154 let ec_calculator_finalized_epoch = Arc::new(AtomicI64::new(
155 ChainGetTipSetFinalityStatus::get_ec_finality_epoch(&chain_index, &chain_config, &head),
156 ));
157 let chain_index = chain_index.with_is_epoch_finalized(Arc::new({
158 let ec_calculator_finalized_epoch = ec_calculator_finalized_epoch.shallow_clone();
159 move |epoch| {
160 let finalized = ec_calculator_finalized_epoch.load(atomic::Ordering::Acquire);
161 epoch <= finalized
162 }
163 }));
164 Ok(Self {
165 head_changes: Publisher::default(),
166 chain_index,
167 tipset_tracker: TipsetTracker::new(db, chain_config.clone()),
168 heaviest_tipset,
169 f3_finalized_tipset,
170 ec_calculator_finalized_epoch,
171 genesis,
172 validated_blocks: SizeTrackingCache::new_with_metrics(
173 "validated_blocks",
174 VALIDATED_BLOCKS_CACHE_SIZE,
175 ),
176 chain_config,
177 messages_in_tipset_cache: Default::default(),
178 last_clean_lookup_repair_head: Default::default(),
179 })
180 }
181
182 pub fn repair_tipset_lookup(&self) -> anyhow::Result<usize> {
187 let head = self.heaviest_tipset();
188 if self.last_clean_lookup_repair_head.load().as_deref() == Some(head.key()) {
189 return Ok(0);
190 }
191 let n_repaired = self.chain_index.repair_tipset_lookup_window(
192 &head,
193 self.chain_config.policy.chain_finality,
194 self.ec_calculator_finalized_epoch(),
195 )?;
196 if n_repaired == 0 {
197 self.last_clean_lookup_repair_head
198 .store(Some(Arc::new(head.key().clone())));
199 }
200 Ok(n_repaired)
201 }
202
203 pub fn set_f3_finalized_tipset(&self, ts: Tipset) {
205 self.f3_finalized_tipset.store(Some(ts.into()));
206 }
207
208 pub fn f3_finalized_tipset(&self) -> Option<Tipset> {
210 self.f3_finalized_tipset
211 .load()
212 .as_ref()
213 .map(|ts| ts.as_ref().shallow_clone())
214 }
215
216 pub fn ec_calculator_finalized_epoch(&self) -> ChainEpoch {
218 self.ec_calculator_finalized_epoch
219 .load(atomic::Ordering::Acquire)
220 }
221
222 pub fn messages_in_tipset_cache(&self) -> &MessagesInTipsetCache {
224 &self.messages_in_tipset_cache
225 }
226
227 pub fn set_heaviest_tipset(&self, head: Tipset) -> Result<(), Error> {
229 head.key().save(self.db())?;
230 self.db().set_heaviest_tipset_key(head.key())?;
231
232 let finalized_epoch = ChainGetTipSetFinalityStatus::get_ec_finality_epoch(
233 self.chain_index(),
234 self.chain_config(),
235 &head,
236 );
237 self.ec_calculator_finalized_epoch
238 .store(finalized_epoch, atomic::Ordering::Release);
239
240 if let Err(e) = self
242 .chain_index
243 .update_tipset_lookup_for_finalized_head(&head, finalized_epoch)
244 {
245 error!("failed to update tipset lookup table: {e:#?}");
246 }
247 if let Err(e) = self.chain_index.cleanup_stale_lookup_at_new_head(&head) {
249 error!("failed to cleanup stale null round lookups: {e:#?}");
250 }
251
252 let old_head = self.heaviest_tipset.swap(head.shallow_clone().into());
253 if old_head.key() != head.key() && self.head_changes.has_subscribers() {
254 let changes = match crate::rpc::chain::chain_get_path(self, old_head.key(), head.key())
255 {
256 Ok(changes) => changes,
257 Err(e) => {
258 if old_head.epoch() > 0 {
260 error!("failed to get chain path changes: {e:#}");
261 }
262 PathChanges {
264 applies: vec![head],
265 reverts: vec![],
266 }
267 }
268 };
269 if !changes.is_empty() {
270 self.head_changes.publish(changes);
271 }
272 }
273
274 Ok(())
275 }
276
277 pub fn add_to_tipset_tracker(&self, header: &CachingBlockHeader) {
279 self.tipset_tracker.add(header);
280 }
281
282 pub fn maybe_update_pending_head(&self, ts: &Tipset) -> Result<bool, Error> {
287 persist_objects(self.db(), ts.block_headers().iter())?;
288 let expanded = self.expand_tipset(ts.min_ticket_block().clone())?;
290 self.maybe_update_heaviest(expanded)
291 }
292
293 pub fn get_required_tipset_key(&self, hash: &EthHash) -> Result<TipsetKey, Error> {
295 Ok(TipsetKey::load(self.db(), &hash.to_cid())?)
296 }
297
298 pub fn put_mapping(&self, k: EthHash, v: Cid, timestamp: u64) -> Result<(), Error> {
300 self.db().write_obj(&k, &(v, timestamp))?;
301 Ok(())
302 }
303
304 pub fn put_mapping_if_newer(&self, k: EthHash, v: Cid, timestamp: u64) -> Result<(), Error> {
309 if let Some((_, existing_timestamp)) = self.db().read_obj::<(Cid, u64)>(&k)?
310 && existing_timestamp >= timestamp
311 {
312 return Ok(());
313 }
314 self.put_mapping(k, v, timestamp)
315 }
316
317 pub fn get_mapping(&self, hash: &EthHash) -> Result<Option<Cid>, Error> {
319 Ok(self.db().read_obj::<(Cid, u64)>(hash)?.map(|(cid, _)| cid))
320 }
321
322 fn expand_tipset(&self, header: CachingBlockHeader) -> Result<Tipset, Error> {
325 self.tipset_tracker.expand(header)
326 }
327
328 pub fn genesis_block_header(&self) -> &CachingBlockHeader {
330 self.genesis.min_ticket_block()
331 }
332
333 pub fn genesis_tipset(&self) -> Tipset {
335 self.genesis.shallow_clone()
336 }
337
338 pub fn heaviest_tipset(&self) -> Tipset {
340 self.heaviest_tipset.load().as_ref().shallow_clone()
341 }
342
343 pub fn subscribe_head_changes(&self) -> flume::Receiver<HeadChanges> {
346 self.head_changes.subscribe()
347 }
348
349 pub fn subscribe_head_changes_bounded(&self, cap: usize) -> flume::Receiver<HeadChanges> {
354 self.head_changes.subscribe_bounded(cap)
355 }
356
357 pub fn db(&self) -> &DbImpl {
359 self.chain_index().db()
360 }
361
362 pub fn db_owned(&self) -> DbImpl {
364 self.chain_index().db_owned()
365 }
366
367 pub fn chain_index(&self) -> &ChainIndex {
369 &self.chain_index
370 }
371
372 pub fn chain_config(&self) -> &Arc<ChainConfig> {
374 &self.chain_config
375 }
376
377 pub fn resolve_to_deterministic_address_at_finality(
384 &self,
385 addr: &Address,
386 ts: &Tipset,
387 ) -> anyhow::Result<AtFinalityResolution> {
388 use crate::shim::address::Protocol::*;
389 match addr.protocol() {
390 BLS | Secp256k1 | Delegated => Ok(AtFinalityResolution::ReorgStable(*addr)),
391 ID => {
392 let finality_deep = ts.epoch() > self.chain_config().policy.chain_finality;
393 let lookback_ts = if finality_deep {
394 self.chain_index().load_required_tipset_by_height_blocking(
395 ts.epoch() - self.chain_config().policy.chain_finality,
396 ts.shallow_clone(),
397 ResolveNullTipset::TakeOlder,
398 )?
399 } else {
400 ts.shallow_clone()
401 };
402 let state = StateTree::new_from_root(self.db(), lookback_ts.parent_state())?;
403 let resolved = state.resolve_to_deterministic_address(self.db(), *addr)?;
404 Ok(if finality_deep {
405 AtFinalityResolution::ReorgStable(resolved)
406 } else {
407 AtFinalityResolution::Unstable(resolved)
408 })
409 }
410 Actor => anyhow::bail!("Cannot resolve actor address to key address"),
411 }
412 }
413
414 #[tracing::instrument(skip_all)]
419 pub fn load_required_tipset_or_heaviest<'a>(
420 &self,
421 maybe_key: impl Into<Option<&'a TipsetKey>>,
422 ) -> Result<Tipset, Error> {
423 match maybe_key.into() {
424 Some(key) => self.chain_index.load_required_tipset(key),
425 None => Ok(self.heaviest_tipset()),
426 }
427 }
428
429 pub async fn load_child_tipset(&self, ts: &Tipset) -> Result<Option<Tipset>, Error> {
432 let head = self.heaviest_tipset();
433 if head.parents() == ts.key() {
434 Ok(Some(head))
435 } else if head.epoch() > ts.epoch() {
436 match self
437 .chain_index()
438 .tipset_by_height(ts.epoch() + 1, head, ResolveNullTipset::TakeNewer)
439 .await?
440 {
441 Some(maybe_child) if maybe_child.parents() == ts.key() => Ok(Some(maybe_child)),
442 _ => Ok(None),
443 }
444 } else {
445 Ok(None)
446 }
447 }
448
449 fn maybe_update_heaviest(&self, ts: Tipset) -> Result<bool, Error> {
453 let heaviest_weight = fil_cns::weight(self.db(), &self.heaviest_tipset())?;
455
456 let new_weight = fil_cns::weight(self.db(), &ts)?;
457 let curr_weight = heaviest_weight;
458
459 if new_weight > curr_weight {
460 self.set_heaviest_tipset(ts)?;
461 Ok(true)
462 } else {
463 Ok(false)
464 }
465 }
466
467 pub fn is_block_validated(&self, cid: &Cid) -> bool {
469 let validated = self.validated_blocks.get(cid).is_some();
470 if validated {
471 trace!("Block {cid} was previously validated");
472 }
473 validated
474 }
475
476 pub fn mark_block_as_validated(&self, cid: &Cid) {
478 self.validated_blocks.insert((*cid).into(), ());
479 }
480
481 pub fn unmark_block_as_validated(&self, cid: &Cid) {
482 self.validated_blocks.remove(cid);
483 }
484
485 pub fn messages_for_tipset(&self, ts: &Tipset) -> Result<Arc<Vec<ChainMessage>>, Error> {
488 Ok(self
489 .messages_in_tipset_cache()
490 .get_or_insert_with(ts.key(), || {
491 let bmsgs = BlockMessages::for_tipset(self.db(), ts)?;
492 anyhow::Ok(
493 bmsgs
494 .into_iter()
495 .flat_map(|bm| bm.messages)
496 .collect_vec()
497 .into(),
498 )
499 })?)
500 }
501
502 pub fn get_lookback_tipset_for_round_blocking(
511 chain_index: &ChainIndex,
512 chain_config: &Arc<ChainConfig>,
513 heaviest_tipset: &Tipset,
514 round: ChainEpoch,
515 ) -> Result<(Tipset, Cid), Error> {
516 let version = chain_config.network_version(round);
517 let lb = if version <= NetworkVersion::V3 {
518 ChainEpoch::from(10)
519 } else {
520 chain_config.policy.chain_finality
521 };
522 let lbr = if round > lb { round - lb } else { 0 };
525
526 if lbr >= heaviest_tipset.epoch() {
528 if version <= NetworkVersion::V3 || heaviest_tipset.epoch() == 0 {
532 let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
533 let beacon = Arc::new(chain_config.get_beacon_schedule(genesis_timestamp));
534 let ExecutedTipset { state_root, .. } =
535 crate::state_manager::apply_block_messages_blocking(
536 chain_index.shallow_clone(),
537 chain_config.shallow_clone(),
538 beacon,
539 &crate::shim::machine::GLOBAL_MULTI_ENGINE,
541 heaviest_tipset.clone(),
542 crate::state_manager::NO_CALLBACK,
543 VMTrace::NotTraced,
544 )
545 .map_err(|e| Error::Other(e.to_string()))?;
546 return Ok((heaviest_tipset.clone(), state_root));
547 } else {
548 return Err(Error::LookbackHeightOverflow {
549 lookback_height: lbr,
550 base_height: heaviest_tipset.epoch(),
551 });
552 }
553 }
554
555 let next_ts = chain_index
556 .load_required_tipset_by_height_blocking(
557 lbr + 1,
558 heaviest_tipset.clone(),
559 ResolveNullTipset::TakeNewer,
560 )
561 .map_err(|e| Error::Other(format!("Could not get tipset by height {e:?}")))?;
562 if lbr > next_ts.epoch() {
563 return Err(Error::Other(format!(
564 "failed to find non-null tipset {:?} {} which is known to exist, found {:?} {}",
565 heaviest_tipset.key(),
566 heaviest_tipset.epoch(),
567 next_ts.key(),
568 next_ts.epoch()
569 )));
570 }
571 let lbts = chain_index
572 .load_required_tipset(next_ts.parents())
573 .map_err(|e| Error::Other(format!("Could not get tipset from keys {e:?}")))?;
574 Ok((lbts, *next_ts.parent_state()))
575 }
576
577 pub async fn get_lookback_tipset_for_round(
578 chain_index: ChainIndex,
579 chain_config: Arc<ChainConfig>,
580 heaviest_tipset: Tipset,
581 round: ChainEpoch,
582 ) -> Result<(Tipset, Cid), Error> {
583 tokio::task::spawn_blocking(move || {
584 Self::get_lookback_tipset_for_round_blocking(
585 &chain_index,
586 &chain_config,
587 &heaviest_tipset,
588 round,
589 )
590 })
591 .await?
592 }
593
594 pub fn process_signed_messages(
601 &self,
602 messages: &[(SignedMessage, u64)],
603 compare_timestamps: bool,
604 ) -> anyhow::Result<()> {
605 let eth_txs: Vec<(EthHash, Cid, u64, usize)> = messages
606 .iter()
607 .enumerate()
608 .filter_map(|(i, (smsg, timestamp))| {
609 if let Ok((_, tx)) =
610 eth_tx_from_signed_eth_message(smsg, self.chain_config.eth_chain_id)
611 {
612 if let Ok(hash) = tx.eth_hash() {
613 Some((hash.into(), smsg.cid(), *timestamp, i))
615 } else {
616 None
617 }
618 } else {
619 None
620 }
621 })
622 .collect();
623 let filtered = filter_lowest_index(eth_txs);
624 let num_entries = filtered.len();
625
626 for (k, v, timestamp) in filtered.into_iter() {
628 trace!("Insert mapping {} => {}", k, v);
629 if compare_timestamps {
630 self.put_mapping_if_newer(k, v, timestamp)?;
631 } else {
632 self.put_mapping(k, v, timestamp)?;
633 }
634 }
635 trace!("Wrote {} entries in Ethereum mapping", num_entries);
636 Ok(())
637 }
638
639 pub fn headers_delegated_messages<'a>(
640 &self,
641 headers: impl Iterator<Item = &'a CachingBlockHeader>,
642 ) -> anyhow::Result<Vec<(SignedMessage, u64)>> {
643 let mut delegated_messages = vec![];
644
645 let filtered_headers =
648 headers.filter(|bh| bh.epoch >= self.chain_config.epoch(Height::Hygge));
649
650 for bh in filtered_headers {
651 if let Ok((_, secp_cids)) = block_messages(self.db(), bh) {
652 let mut messages: Vec<_> = secp_cids
653 .into_iter()
654 .filter(|msg| msg.is_delegated())
655 .map(|m| (m, bh.timestamp))
656 .collect();
657 delegated_messages.append(&mut messages);
658 }
659 }
660
661 Ok(delegated_messages)
662 }
663}
664
665fn filter_lowest_index(values: Vec<(EthHash, Cid, u64, usize)>) -> Vec<(EthHash, Cid, u64)> {
666 let map: HashMap<EthHash, (Cid, u64, usize)> = values.into_iter().fold(
667 HashMap::default(),
668 |mut acc, (hash, cid, timestamp, index)| {
669 acc.entry(hash)
670 .and_modify(|&mut (_, _, ref mut min_index)| {
671 if index < *min_index {
672 *min_index = index;
673 }
674 })
675 .or_insert((cid, timestamp, index));
676 acc
677 },
678 );
679
680 map.into_iter()
681 .map(|(hash, (cid, timestamp, _))| (hash, cid, timestamp))
682 .collect()
683}
684
685pub fn block_messages<DB>(
688 db: &DB,
689 bh: &CachingBlockHeader,
690) -> Result<(Vec<Message>, Vec<SignedMessage>), Error>
691where
692 DB: Blockstore,
693{
694 let (bls_cids, secpk_cids) = read_msg_cids(db, bh)?;
695
696 let bls_msgs: Vec<Message> = messages_from_cids(db, &bls_cids)?;
697 let secp_msgs: Vec<SignedMessage> = messages_from_cids(db, &secpk_cids)?;
698
699 Ok((bls_msgs, secp_msgs))
700}
701
702pub fn block_messages_from_cids<DB>(
704 db: &DB,
705 bls_cids: &[Cid],
706 secp_cids: &[Cid],
707) -> Result<(Vec<Message>, Vec<SignedMessage>), Error>
708where
709 DB: Blockstore,
710{
711 let bls_msgs: Vec<Message> = messages_from_cids(db, bls_cids)?;
712 let secp_msgs: Vec<SignedMessage> = messages_from_cids(db, secp_cids)?;
713
714 Ok((bls_msgs, secp_msgs))
715}
716
717pub fn read_msg_cids<DB>(
719 db: &DB,
720 block_header: &CachingBlockHeader,
721) -> Result<(Vec<Cid>, Vec<Cid>), Error>
722where
723 DB: Blockstore,
724{
725 let msg_cid = &block_header.messages;
726 if let Some(roots) = db.get_cbor::<TxMeta>(msg_cid)? {
727 let bls_cids = read_amt_cids(db, &roots.bls_message_root)?;
728 let secpk_cids = read_amt_cids(db, &roots.secp_message_root)?;
729 Ok((bls_cids, secpk_cids))
730 } else {
731 Err(Error::UndefinedKey(format!(
732 "no msg root with cid {msg_cid} at epoch {} in block {}",
733 block_header.epoch,
734 block_header.cid(),
735 )))
736 }
737}
738
739pub fn persist_objects<'a, DB, C>(
741 db: &DB,
742 headers: impl Iterator<Item = &'a C>,
743) -> Result<(), Error>
744where
745 DB: Blockstore,
746 C: 'a + Serialize,
747{
748 for chunk in &headers.chunks(256) {
749 db.bulk_put(chunk, DB::default_code())?;
750 }
751 Ok(())
752}
753
754fn read_amt_cids<DB>(db: &DB, root: &Cid) -> Result<Vec<Cid>, Error>
756where
757 DB: Blockstore,
758{
759 let amt = Amt::<Cid, _>::load(root, db)?;
760
761 let mut cids = Vec::with_capacity(amt.count() as usize);
762 amt.for_each_cacheless(|_, c| {
763 cids.push(*c);
764 Ok(())
765 })?;
766
767 Ok(cids)
768}
769
770pub fn get_chain_message<DB>(db: &DB, key: &Cid) -> Result<ChainMessage, Error>
773where
774 DB: Blockstore,
775{
776 db.get_cbor(key)?
777 .ok_or_else(|| Error::UndefinedKey(key.to_string()))
778}
779
780#[derive(derive_more::Deref)]
785pub struct MessagesInTipsetCache(SizeTrackingCache<TipsetKey, Arc<Vec<ChainMessage>>>);
786
787impl MessagesInTipsetCache {
788 pub fn new(capacity: NonZeroUsize) -> Self {
789 Self(SizeTrackingCache::new_with_metrics(
790 "msg_in_tipset",
791 capacity,
792 ))
793 }
794
795 fn read_cache_size() -> NonZeroUsize {
797 const DEFAULT: NonZeroUsize = nonzero!(8192usize); std::env::var("FOREST_MESSAGES_IN_TIPSET_CACHE_SIZE")
800 .ok()
801 .and_then(|s| s.parse().ok())
802 .unwrap_or(DEFAULT)
803 }
804}
805
806impl Default for MessagesInTipsetCache {
807 fn default() -> Self {
808 Self::new(Self::read_cache_size())
809 }
810}
811
812impl ShallowClone for MessagesInTipsetCache {
813 fn shallow_clone(&self) -> Self {
814 Self(self.deref().shallow_clone())
815 }
816}
817
818pub fn messages_from_cids<DB, T>(db: &DB, keys: &[Cid]) -> Result<Vec<T>, Error>
820where
821 DB: Blockstore,
822 T: DeserializeOwned,
823{
824 keys.iter().map(|k| message_from_cid(db, k)).collect()
825}
826
827pub fn message_from_cid<DB, T>(db: &DB, key: &Cid) -> Result<T, Error>
829where
830 DB: Blockstore,
831 T: DeserializeOwned,
832{
833 db.get_cbor(key)?
834 .ok_or_else(|| Error::UndefinedKey(key.to_string()))
835}
836
837pub fn get_parent_receipt(
839 db: &impl Blockstore,
840 block_header: &CachingBlockHeader,
841 i: usize,
842) -> Result<Option<Receipt>, Error> {
843 Ok(Receipt::get_receipt(
844 db,
845 &block_header.message_receipts,
846 i as u64,
847 )?)
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853 use crate::utils::multihash::prelude::*;
854 use crate::{blocks::RawBlockHeader, shim::address::Address};
855 use fvm_ipld_encoding::DAG_CBOR;
856
857 #[test]
858 fn genesis_test() {
859 let db = Arc::new(crate::db::MemoryDB::default());
860 let chain_config = Arc::new(ChainConfig::default());
861
862 let gen_block = CachingBlockHeader::new(RawBlockHeader {
863 miner_address: Address::new_id(0),
864 state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
865 epoch: 0,
866 weight: 2u32.into(),
867 messages: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
868 message_receipts: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
869 ..Default::default()
870 });
871 let gen_ts = Tipset::from(&gen_block);
872 let cs = ChainStore::new(db, chain_config, gen_ts.shallow_clone()).unwrap();
873
874 assert_eq!(cs.genesis_tipset(), gen_ts);
875 assert_eq!(cs.genesis_block_header(), &gen_block);
876 }
877
878 #[test]
879 fn block_validation_cache_basic() {
880 let db = DbImpl::from(Arc::new(crate::db::MemoryDB::default()));
881 let chain_config = Arc::new(ChainConfig::default());
882 let gen_block = CachingBlockHeader::new(RawBlockHeader {
883 miner_address: Address::new_id(0),
884 ..Default::default()
885 });
886
887 let cs = ChainStore::new(db, chain_config, gen_block).unwrap();
888
889 let cid = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&[1, 2, 3]));
890 assert!(!cs.is_block_validated(&cid));
891
892 cs.mark_block_as_validated(&cid);
893 assert!(cs.is_block_validated(&cid));
894 }
895
896 #[test]
897 fn put_mapping_if_newer_keeps_newest() {
898 let db = DbImpl::from(Arc::new(crate::db::MemoryDB::default()));
899 let chain_config = Arc::new(ChainConfig::default());
900 let gen_block = CachingBlockHeader::new(RawBlockHeader {
901 miner_address: Address::new_id(0),
902 ..Default::default()
903 });
904 let cs = ChainStore::new(db, chain_config, gen_block).unwrap();
905
906 let hash = EthHash::default();
907 let older = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&[1]));
908 let newer = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&[2]));
909
910 cs.put_mapping(hash, newer, 100).unwrap();
912
913 cs.put_mapping_if_newer(hash, older, 50).unwrap();
915 assert_eq!(cs.get_mapping(&hash).unwrap(), Some(newer));
916
917 cs.put_mapping_if_newer(hash, older, 100).unwrap();
919 assert_eq!(cs.get_mapping(&hash).unwrap(), Some(newer));
920
921 let newest = Cid::new_v1(DAG_CBOR, MultihashCode::Blake2b256.digest(&[3]));
923 cs.put_mapping_if_newer(hash, newest, 200).unwrap();
924 assert_eq!(cs.get_mapping(&hash).unwrap(), Some(newest));
925
926 let fresh_hash = EthHash(ethereum_types::H256::repeat_byte(0xab));
928 cs.put_mapping_if_newer(fresh_hash, older, 1).unwrap();
929 assert_eq!(cs.get_mapping(&fresh_hash).unwrap(), Some(older));
930 }
931
932 #[test]
933 fn test_messages_in_tipset_cache() {
934 let cache = MessagesInTipsetCache::new(nonzero!(2_usize));
935 let key1 = TipsetKey::from(nunny::vec![Cid::new_v1(
936 DAG_CBOR,
937 MultihashCode::Blake2b256.digest(&[1])
938 )]);
939 assert!(cache.get(&key1).is_none());
940
941 let msgs = Arc::new(vec![Message::default().into()]);
942 cache.insert(key1.clone(), msgs.clone());
943 assert_eq!(&msgs, &cache.get(&key1).unwrap());
944
945 let inserter_executed: std::sync::atomic::AtomicBool =
946 std::sync::atomic::AtomicBool::new(false);
947 let key_inserter = || {
948 inserter_executed.store(true, std::sync::atomic::Ordering::Relaxed);
949 anyhow::Ok(msgs.clone())
950 };
951
952 assert_eq!(
953 &msgs,
954 &cache.get_or_insert_with(&key1, key_inserter).unwrap()
955 );
956 assert!(!inserter_executed.load(std::sync::atomic::Ordering::Relaxed));
957
958 let key2 = TipsetKey::from(nunny::vec![Cid::new_v1(
959 DAG_CBOR,
960 MultihashCode::Blake2b256.digest(&[2])
961 )]);
962
963 assert!(cache.get(&key2).is_none());
964 assert_eq!(
965 &msgs,
966 &cache.get_or_insert_with(&key2, key_inserter).unwrap()
967 );
968 assert!(inserter_executed.load(std::sync::atomic::Ordering::Relaxed));
969 }
970}