1use super::circulating_supply::GenesisInfo;
5use super::*;
6use crate::interpreter::{BlockMessages, ExecutionContext, VM, VMTrace};
7use crate::prelude::*;
8use crate::shim::message::Message;
9use crate::state_migration::run_state_migrations;
10use anyhow::{bail, ensure};
11use fil_actors_shared::fvm_ipld_amt::{Amt, Amtv0};
12use tracing::{error, info, instrument, warn};
13
14enum StateRecomputePolicy {
15 Allowed,
16 Disallowed,
17}
18
19impl StateManager {
20 pub async fn load_tipset_state(&self, ts: &Tipset) -> anyhow::Result<TipsetState> {
22 if let Some(state) = self.cache.get_map(ts.key(), |et| et.into()) {
23 Ok(state)
24 } else {
25 match self.chain_store().load_child_tipset(ts).await? {
26 Some(receipt_ts) => Ok(TipsetState {
27 state_root: *receipt_ts.parent_state(),
28 receipt_root: *receipt_ts.parent_message_receipts(),
29 }),
30 None => Ok(self.load_executed_tipset(ts).await?.into()),
31 }
32 }
33 }
34
35 pub fn clear_tipset_state_caches(&self) {
39 self.cache.clear();
40 self.trace_cache.clear();
41 }
42
43 pub fn repair_tipset_lookup(&self) -> anyhow::Result<usize> {
47 let n_repaired = self.cs.repair_tipset_lookup()?;
48 if n_repaired > 0 {
49 self.clear_tipset_state_caches();
50 }
51 Ok(n_repaired)
52 }
53
54 fn rpc_state_recompute_policy() -> StateRecomputePolicy {
57 crate::def_is_env_truthy!(
58 enable_state_computation,
59 "FOREST_ETH_RPC_COMPUTE_STATE_ON_INDEX_MISS"
60 );
61
62 if enable_state_computation() {
63 StateRecomputePolicy::Allowed
64 } else {
65 StateRecomputePolicy::Disallowed
66 }
67 }
68
69 pub async fn load_executed_tipset_for_rpc(
71 &self,
72 ts: &Tipset,
73 ) -> anyhow::Result<ExecutedTipset> {
74 self.load_executed_tipset_with_cache(ts, Self::rpc_state_recompute_policy())
75 .await
76 }
77
78 pub async fn tipset_message_receipts(
82 &self,
83 ts: &Tipset,
84 receipt_ts: Option<&Tipset>,
85 ) -> anyhow::Result<TipsetMessageReceipts> {
86 if let Some(cached) = self.cache.get(ts.key()) {
87 return Ok(TipsetMessageReceipts::Executed(cached.executed_messages));
88 }
89
90 let receipt_ts = match receipt_ts {
91 Some(child) => Some(child.shallow_clone()),
92 None => self.chain_store().load_child_tipset(ts).await?,
93 };
94 if let Some(child) = &receipt_ts {
95 anyhow::ensure!(
96 ts.key() == child.parents(),
97 "message tipset should be the parent of message receipt tipset"
98 );
99 if let Ok(receipts) =
100 Receipt::get_receipts(self.cs.db(), *child.parent_message_receipts())
101 {
102 let messages = self.chain_store().messages_for_tipset(ts)?;
103 anyhow::ensure!(
104 messages.len() == receipts.len(),
105 "mismatching message and receipt counts ({} messages, {} receipts)",
106 messages.len(),
107 receipts.len()
108 );
109 return Ok(TipsetMessageReceipts::Stored(messages, receipts));
110 }
111 }
112 Ok(TipsetMessageReceipts::Executed(
113 self.load_executed_tipset_for_rpc(ts)
114 .await?
115 .executed_messages,
116 ))
117 }
118
119 pub async fn load_executed_tipset_with_receipt(
123 &self,
124 msg_ts: &Tipset,
125 receipt_ts: &Tipset,
126 ) -> anyhow::Result<ExecutedTipset> {
127 self.cache
128 .get_or_insert_async(msg_ts.key(), async move {
129 self.load_executed_tipset_inner(
130 msg_ts,
131 Some(receipt_ts),
132 Self::rpc_state_recompute_policy(),
133 )
134 .await
135 })
136 .await
137 }
138
139 pub async fn load_executed_tipset(&self, ts: &Tipset) -> anyhow::Result<ExecutedTipset> {
141 self.load_executed_tipset_with_cache(ts, StateRecomputePolicy::Allowed)
142 .await
143 }
144
145 pub async fn load_executed_tipset_uncached(
148 &self,
149 ts: &Tipset,
150 allow_state_compute: bool,
151 ) -> anyhow::Result<ExecutedTipset> {
152 let policy = if allow_state_compute {
153 StateRecomputePolicy::Allowed
154 } else {
155 StateRecomputePolicy::Disallowed
156 };
157 let receipt_ts = self.chain_store().load_child_tipset(ts).await?;
158 self.load_executed_tipset_inner(ts, receipt_ts.as_ref(), policy)
159 .await
160 }
161
162 async fn load_executed_tipset_with_cache(
163 &self,
164 ts: &Tipset,
165 policy: StateRecomputePolicy,
166 ) -> anyhow::Result<ExecutedTipset> {
167 if ts.epoch() >= self.heaviest_tipset().epoch()
169 && let Some(cached) = self.cache.get(ts.key())
170 {
171 if StateTree::new_from_root(self.db(), &cached.state_root).is_ok() {
172 return Ok(cached);
173 } else {
174 self.cache.remove(ts.key());
175 }
176 }
177 self.cache
178 .get_or_insert_async(ts.key(), async move {
179 let receipt_ts = self.chain_store().load_child_tipset(ts).await?;
180 self.load_executed_tipset_inner(ts, receipt_ts.as_ref(), policy)
181 .await
182 })
183 .await
184 }
185
186 async fn load_executed_tipset_inner(
187 &self,
188 msg_ts: &Tipset,
189 receipt_ts: Option<&Tipset>,
191 policy: StateRecomputePolicy,
192 ) -> anyhow::Result<ExecutedTipset> {
193 let state_compute_disallow_error = || {
194 format!(
195 "failed to load tipset state output and recomputation is disallowed, epoch={}, key={}",
196 msg_ts.epoch(),
197 msg_ts.key()
198 )
199 };
200
201 if let Some(receipt_ts) = receipt_ts {
202 anyhow::ensure!(
203 msg_ts.key() == receipt_ts.parents(),
204 "message tipset should be the parent of message receipt tipset"
205 );
206 }
207 let allow_state_compute = matches!(policy, StateRecomputePolicy::Allowed);
208 let mut recomputed = false;
209 let (state_root, receipt_root, receipts) = match receipt_ts.and_then(|ts| {
210 let receipt_root = *ts.parent_message_receipts();
211 Receipt::get_receipts(self.cs.db(), receipt_root)
212 .ok()
213 .map(|r| (*ts.parent_state(), receipt_root, r))
214 }) {
215 Some((state_root, receipt_root, receipts)) => (state_root, receipt_root, receipts),
216 None => {
217 if !allow_state_compute {
218 anyhow::bail!(state_compute_disallow_error());
219 }
220 let state_output = self
221 .compute_tipset_state(msg_ts.shallow_clone(), NO_CALLBACK, VMTrace::NotTraced)
222 .await?;
223 recomputed = true;
224 (
225 state_output.state_root,
226 state_output.receipt_root,
227 Receipt::get_receipts(self.cs.db(), state_output.receipt_root)?,
228 )
229 }
230 };
231
232 let messages = self.chain_store().messages_for_tipset(msg_ts)?;
233 anyhow::ensure!(
234 messages.len() == receipts.len(),
235 "mismatching message and receipt counts ({} messages, {} receipts)",
236 messages.len(),
237 receipts.len()
238 );
239 let mut executed_messages = Vec::with_capacity(messages.len());
240 for (message, receipt) in messages.iter().cloned().zip(receipts) {
241 let events = if let Some(events_root) = receipt.events_root() {
242 Some(match StampedEvent::get_events(self.cs.db(), &events_root) {
243 Ok(events) => events,
244 Err(e) if recomputed => return Err(e),
245 Err(_) => {
246 if !allow_state_compute {
247 anyhow::bail!(state_compute_disallow_error());
248 }
249 self.compute_tipset_state(
250 msg_ts.shallow_clone(),
251 NO_CALLBACK,
252 VMTrace::NotTraced,
253 )
254 .await?;
255 recomputed = true;
256 StampedEvent::get_events(self.cs.db(), &events_root)?
257 }
258 })
259 } else {
260 None
261 };
262 executed_messages.push(ExecutedMessage {
263 message,
264 receipt,
265 events,
266 });
267 }
268
269 if recomputed
271 && let Err(e) = crate::rpc::eth::store_block_logs_bloom(
272 self,
273 msg_ts,
274 &state_root,
275 &executed_messages,
276 )
277 {
278 warn!(
279 "failed to store block logs bloom for tipset {}: {e:#}",
280 msg_ts.key()
281 );
282 }
283
284 Ok(ExecutedTipset {
285 state_root,
286 receipt_root,
287 executed_messages: Arc::new(executed_messages),
288 })
289 }
290
291 pub async fn compute_tipset_state(
313 &self,
314 tipset: Tipset,
315 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()> + Send + 'static>,
316 enable_tracing: VMTrace,
317 ) -> Result<ExecutedTipset, Error> {
318 let this = self.shallow_clone();
319 tokio::task::spawn_blocking(move || {
320 this.compute_tipset_state_blocking(tipset, callback, enable_tracing)
321 })
322 .await?
323 }
324
325 pub fn compute_tipset_state_blocking(
327 &self,
328 tipset: Tipset,
329 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
330 enable_tracing: VMTrace,
331 ) -> Result<ExecutedTipset, Error> {
332 let epoch = tipset.epoch();
333 let has_callback = callback.is_some();
334 info!(
335 "Evaluating tipset: EPOCH={epoch}, blocks={}, tsk={}",
336 tipset.len(),
337 tipset.key(),
338 );
339 Ok(apply_block_messages_blocking(
340 self.chain_index().shallow_clone(),
341 self.chain_config().shallow_clone(),
342 self.beacon_schedule().shallow_clone(),
343 &self.engine,
344 tipset,
345 callback,
346 enable_tracing,
347 )
348 .map_err(|e| {
349 if has_callback {
350 e
351 } else {
352 e.context(format!("Failed to compute tipset state@{epoch}"))
353 }
354 })?)
355 }
356
357 #[instrument(skip_all)]
358 pub async fn compute_state(
359 &self,
360 height: ChainEpoch,
361 messages: Vec<Message>,
362 tipset: Tipset,
363 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()> + Send + 'static>,
364 enable_tracing: VMTrace,
365 ) -> Result<ExecutedTipset, Error> {
366 let this = self.shallow_clone();
367 tokio::task::spawn_blocking(move || {
368 this.compute_state_blocking(height, messages, tipset, callback, enable_tracing)
369 })
370 .await?
371 }
372
373 #[tracing::instrument(skip_all)]
375 pub fn compute_state_blocking(
376 &self,
377 height: ChainEpoch,
378 messages: Vec<Message>,
379 tipset: Tipset,
380 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
381 enable_tracing: VMTrace,
382 ) -> Result<ExecutedTipset, Error> {
383 Ok(compute_state_blocking(
384 height,
385 messages,
386 tipset,
387 self.chain_index().shallow_clone(),
388 self.chain_config().shallow_clone(),
389 self.beacon_schedule().shallow_clone(),
390 &self.engine,
391 callback,
392 enable_tracing,
393 )?)
394 }
395}
396
397pub fn validate_tipsets_blocking<T>(
398 chain_index: &ChainIndex,
399 chain_config: &Arc<ChainConfig>,
400 beacon: &Arc<BeaconSchedule>,
401 engine: &MultiEngine,
402 tipsets: T,
403) -> anyhow::Result<()>
404where
405 T: Iterator<Item = Tipset> + Send,
406{
407 for (child, parent) in tipsets.tuple_windows() {
412 info!(height = parent.epoch(), "compute parent state");
413 let ExecutedTipset {
414 state_root: actual_state,
415 receipt_root: actual_receipt,
416 ..
417 } = apply_block_messages_blocking(
418 chain_index.shallow_clone(),
419 chain_config.shallow_clone(),
420 beacon.shallow_clone(),
421 engine,
422 parent,
423 NO_CALLBACK,
424 VMTrace::NotTraced,
425 )
426 .context("couldn't compute tipset state")?;
427 let expected_receipt = child.min_ticket_block().message_receipts;
428 let expected_state = child.parent_state();
429 if (expected_state, expected_receipt) != (&actual_state, actual_receipt) {
430 error!(
431 height = child.epoch(),
432 ?expected_state,
433 ?expected_receipt,
434 ?actual_state,
435 ?actual_receipt,
436 "state mismatch"
437 );
438 bail!("state mismatch");
439 }
440 }
441 Ok(())
442}
443
444pub(in crate::state_manager) struct TipsetExecutor<'a> {
449 tipset: Tipset,
450 rand: ChainRand,
451 chain_config: Arc<ChainConfig>,
452 chain_index: ChainIndex,
453 genesis_info: GenesisInfo,
454 engine: &'a MultiEngine,
455}
456
457impl<'a> TipsetExecutor<'a> {
458 pub(in crate::state_manager) fn new(
459 chain_index: ChainIndex,
460 chain_config: Arc<ChainConfig>,
461 beacon: Arc<BeaconSchedule>,
462 engine: &'a MultiEngine,
463 tipset: Tipset,
464 ) -> Self {
465 let rand = ChainRand::new(
466 chain_config.shallow_clone(),
467 tipset.shallow_clone(),
468 chain_index.shallow_clone(),
469 beacon,
470 );
471 let genesis_info = GenesisInfo::from_chain_config(chain_config.shallow_clone());
472 Self {
473 tipset,
474 rand,
475 chain_config,
476 chain_index,
477 genesis_info,
478 engine,
479 }
480 }
481
482 pub(in crate::state_manager) fn create_vm(
483 &self,
484 state_root: Cid,
485 epoch: ChainEpoch,
486 timestamp: u64,
487 trace: VMTrace,
488 ) -> anyhow::Result<VM> {
489 let circ_supply = self.genesis_info.get_vm_circulating_supply(
490 epoch,
491 self.chain_index.db(),
492 &state_root,
493 )?;
494 VM::new(
495 ExecutionContext {
496 heaviest_tipset: self.tipset.shallow_clone(),
497 state_tree_root: state_root,
498 epoch,
499 rand: Box::new(self.rand.shallow_clone()),
500 base_fee: self.tipset.min_ticket_block().parent_base_fee.clone(),
501 circ_supply,
502 chain_config: self.chain_config.shallow_clone(),
503 chain_index: self.chain_index.shallow_clone(),
504 timestamp,
505 },
506 self.engine,
507 trace,
508 )
509 }
510
511 pub(in crate::state_manager) fn prepare_parent_state_blocking<F>(
514 &self,
515 genesis_timestamp: u64,
516 null_epoch_trace: VMTrace,
517 cron_callback: &mut Option<F>,
518 ) -> anyhow::Result<(Cid, ChainEpoch, Vec<BlockMessages>)>
519 where
520 F: FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>,
521 {
522 use crate::shim::clock::EPOCH_DURATION_SECONDS;
523
524 let mut parent_state = *self.tipset.parent_state();
525 let parent_epoch = self
526 .chain_index
527 .load_required_tipset(self.tipset.parents())?
528 .epoch();
529 let epoch = self.tipset.epoch();
530
531 for epoch_i in parent_epoch..epoch {
532 if epoch_i > parent_epoch {
533 let timestamp = genesis_timestamp + ((EPOCH_DURATION_SECONDS * epoch_i) as u64);
534 parent_state = stacker::grow(64 << 20, || -> anyhow::Result<Cid> {
535 let mut vm =
536 self.create_vm(parent_state, epoch_i, timestamp, null_epoch_trace)?;
537 if let Err(e) = vm.run_cron(epoch_i, cron_callback.as_mut()) {
538 error!("Beginning of epoch cron failed to run: {e:#}");
539 return Err(e);
540 }
541 vm.flush()
542 })?;
543 }
544 if let Some(new_state) = run_state_migrations(
545 epoch_i,
546 &self.chain_config,
547 self.chain_index.db(),
548 &parent_state,
549 )? {
550 parent_state = new_state;
551 }
552 }
553
554 let block_messages = BlockMessages::for_tipset(self.chain_index.db(), &self.tipset)?;
555 Ok((parent_state, epoch, block_messages))
556 }
557}
558
559#[allow(clippy::too_many_arguments)]
636pub fn apply_block_messages_blocking(
637 chain_index: ChainIndex,
638 chain_config: Arc<ChainConfig>,
639 beacon: Arc<BeaconSchedule>,
640 engine: &MultiEngine,
641 tipset: Tipset,
642 mut callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
643 enable_tracing: VMTrace,
644) -> anyhow::Result<ExecutedTipset> {
645 let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
654 if tipset.epoch() == 0 {
655 let message_receipts = tipset.min_ticket_block().message_receipts;
660 return Ok(ExecutedTipset {
661 state_root: *tipset.parent_state(),
662 receipt_root: message_receipts,
663 executed_messages: vec![].into(),
664 });
665 }
666
667 let exec = TipsetExecutor::new(
668 chain_index.shallow_clone(),
669 chain_config,
670 beacon,
671 engine,
672 tipset.shallow_clone(),
673 );
674
675 let (parent_state, epoch, block_messages) =
678 exec.prepare_parent_state_blocking(genesis_timestamp, enable_tracing, &mut callback)?;
679
680 stacker::grow(64 << 20, || -> anyhow::Result<ExecutedTipset> {
683 let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?;
684
685 let (receipts, events, events_roots) =
687 vm.apply_block_messages(&block_messages, epoch, callback)?;
688
689 let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts.iter())?;
691
692 for (events, events_root) in events.iter().zip(events_roots.iter()) {
694 if let Some(events) = events {
695 let event_root =
696 events_root.context("events root should be present when events present")?;
697 let derived_event_root = Amt::new_from_iter_with_bit_width(
699 chain_index.db(),
700 EVENTS_AMT_BITWIDTH,
701 events.iter(),
702 )
703 .map_err(|e| Error::Other(format!("failed to store events AMT: {e}")))?;
704
705 ensure!(
707 derived_event_root == event_root,
708 "Events AMT root mismatch: derived={derived_event_root}, actual={event_root}."
709 );
710 }
711 }
712
713 let state_root = vm.flush()?;
714
715 let messages: Vec<ChainMessage> = block_messages
717 .into_iter()
718 .flat_map(|bm| bm.messages)
719 .collect_vec();
720 anyhow::ensure!(
721 messages.len() == receipts.len() && messages.len() == events.len(),
722 "length of messages, receipts, and events should match",
723 );
724 Ok(ExecutedTipset {
725 state_root,
726 receipt_root,
727 executed_messages: messages
728 .into_iter()
729 .zip(receipts)
730 .zip(events)
731 .map(|((message, receipt), events)| ExecutedMessage {
732 message,
733 receipt,
734 events,
735 })
736 .collect_vec()
737 .into(),
738 })
739 })
740}
741
742#[allow(clippy::too_many_arguments)]
743pub(in crate::state_manager) fn compute_state_blocking(
744 _height: ChainEpoch,
745 messages: Vec<Message>,
746 tipset: Tipset,
747 chain_index: ChainIndex,
748 chain_config: Arc<ChainConfig>,
749 beacon: Arc<BeaconSchedule>,
750 engine: &MultiEngine,
751 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
752 enable_tracing: VMTrace,
753) -> anyhow::Result<ExecutedTipset> {
754 if !messages.is_empty() {
755 anyhow::bail!("Applying messages is not yet implemented.");
756 }
757
758 let output = apply_block_messages_blocking(
759 chain_index,
760 chain_config,
761 beacon,
762 engine,
763 tipset,
764 callback,
765 enable_tracing,
766 )?;
767
768 Ok(output)
769}
770
771#[cfg(test)]
772mod tests {
773 use super::*;
774 use crate::blocks::{CachingBlockHeader, RawBlockHeader, TipsetKey, TxMeta};
775 use crate::utils::db::CborStoreExt as _;
776
777 #[test]
778 fn tipset_message_receipts_iter_pairs_in_order() {
779 let msg_count = 3u64;
780 let messages = (0..msg_count)
781 .map(|i| {
782 ChainMessage::Unsigned(Arc::new(Message {
783 sequence: i,
784 ..Default::default()
785 }))
786 })
787 .collect_vec();
788 let receipts = (0..msg_count)
789 .map(|i| Receipt::with_gas_used((i + 1) * 10))
790 .collect_vec();
791 let expected = (0..msg_count).map(|i| (i, (i + 1) * 10)).collect_vec();
792
793 let executed = TipsetMessageReceipts::Executed(Arc::new(
794 messages
795 .iter()
796 .zip(receipts.iter())
797 .map(|(m, r)| ExecutedMessage {
798 message: m.clone(),
799 receipt: r.clone(),
800 events: None,
801 })
802 .collect(),
803 ));
804 let stored = TipsetMessageReceipts::Stored(Arc::new(messages), receipts);
805
806 for variant in [&executed, &stored] {
807 let got = variant
808 .iter()
809 .map(|(m, r)| (m.message().sequence, r.gas_used()))
810 .collect_vec();
811 assert_eq!(got, expected);
812 }
813 }
814
815 fn empty_message_meta(db: &impl Blockstore) -> Cid {
817 let empty = Amtv0::<Cid, _>::new(db).flush().unwrap();
818 db.put_cbor_default(&TxMeta {
819 bls_message_root: empty,
820 secp_message_root: empty,
821 })
822 .unwrap()
823 }
824
825 fn block(
828 epoch: ChainEpoch,
829 parents: TipsetKey,
830 messages: Cid,
831 receipts: Cid,
832 ) -> CachingBlockHeader {
833 CachingBlockHeader::new(RawBlockHeader {
834 parents,
835 epoch,
836 messages,
837 message_receipts: receipts,
838 timestamp: 1,
839 ..Default::default()
840 })
841 }
842
843 #[tokio::test]
844 async fn tipset_message_receipts_covers_all_paths() {
845 use crate::chain::ChainStore;
846 use crate::db::MemoryDB;
847 use crate::networks::ChainConfig;
848
849 let db = Arc::new(MemoryDB::default());
850 let genesis = block(0, TipsetKey::default(), Cid::default(), Cid::default());
851 db.put_cbor_default(&genesis).unwrap();
852 let cs = ChainStore::new(db.clone(), Arc::new(ChainConfig::default()), genesis).unwrap();
853 let genesis_key = cs.genesis_tipset().key().clone();
854
855 let ts = Tipset::from(block(
858 1,
859 genesis_key.clone(),
860 empty_message_meta(&db),
861 Cid::default(),
862 ));
863 let head = Tipset::from(block(
864 2,
865 ts.key().clone(),
866 Cid::default(),
867 Receipt::store_receipts(&db, 0).unwrap(),
868 ));
869 for b in ts.block_headers().iter().chain(head.block_headers().iter()) {
870 db.put_cbor_default(b).unwrap();
871 }
872 cs.set_heaviest_tipset(head.clone()).unwrap();
873 let sm = StateManager::new(cs).unwrap();
874
875 sm.cache.insert(
877 ts.key().clone(),
878 ExecutedTipset {
879 state_root: Cid::default(),
880 receipt_root: Cid::default(),
881 executed_messages: Arc::new(vec![]),
882 },
883 );
884 assert!(matches!(
885 sm.tipset_message_receipts(&ts, None).await.unwrap(),
886 TipsetMessageReceipts::Executed(_)
887 ));
888 sm.cache.remove(ts.key());
889
890 assert!(matches!(
892 sm.tipset_message_receipts(&ts, Some(&head)).await.unwrap(),
893 TipsetMessageReceipts::Stored(m, r) if m.is_empty() && r.is_empty()
894 ));
895
896 assert!(matches!(
898 sm.tipset_message_receipts(&ts, None).await.unwrap(),
899 TipsetMessageReceipts::Stored(..)
900 ));
901
902 assert!(sm.tipset_message_receipts(&head, None).await.is_err());
904
905 let wrong = Tipset::from(block(
907 2,
908 genesis_key,
909 Cid::default(),
910 Receipt::store_receipts(&db, 0).unwrap(),
911 ));
912 assert!(
913 sm.tipset_message_receipts(&ts, Some(&wrong))
914 .await
915 .err()
916 .expect("expected error")
917 .to_string()
918 .contains("should be the parent")
919 );
920
921 let extra = Tipset::from(block(
923 2,
924 ts.key().clone(),
925 Cid::default(),
926 Receipt::store_receipts(&db, 1).unwrap(),
927 ));
928 assert!(
929 sm.tipset_message_receipts(&ts, Some(&extra))
930 .await
931 .err()
932 .expect("expected error")
933 .to_string()
934 .contains("mismatching message and receipt counts")
935 );
936
937 let unreadable = Tipset::from(block(2, ts.key().clone(), Cid::default(), Cid::default()));
940 assert!(matches!(
941 sm.tipset_message_receipts(&ts, Some(&unreadable))
942 .await
943 .unwrap(),
944 TipsetMessageReceipts::Executed(_)
945 ));
946 }
947}