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 load_executed_tipset_with_receipt(
82 &self,
83 msg_ts: &Tipset,
84 receipt_ts: &Tipset,
85 ) -> anyhow::Result<ExecutedTipset> {
86 self.cache
87 .get_or_insert_async(msg_ts.key(), async move {
88 self.load_executed_tipset_inner(
89 msg_ts,
90 Some(receipt_ts),
91 Self::rpc_state_recompute_policy(),
92 )
93 .await
94 })
95 .await
96 }
97
98 pub async fn load_executed_tipset(&self, ts: &Tipset) -> anyhow::Result<ExecutedTipset> {
100 self.load_executed_tipset_with_cache(ts, StateRecomputePolicy::Allowed)
101 .await
102 }
103
104 async fn load_executed_tipset_with_cache(
105 &self,
106 ts: &Tipset,
107 policy: StateRecomputePolicy,
108 ) -> anyhow::Result<ExecutedTipset> {
109 if ts.epoch() >= self.heaviest_tipset().epoch()
111 && let Some(cached) = self.cache.get(ts.key())
112 {
113 if StateTree::new_from_root(self.db(), &cached.state_root).is_ok() {
114 return Ok(cached);
115 } else {
116 self.cache.remove(ts.key());
117 }
118 }
119 self.cache
120 .get_or_insert_async(ts.key(), async move {
121 let receipt_ts = self.chain_store().load_child_tipset(ts).await?;
122 self.load_executed_tipset_inner(ts, receipt_ts.as_ref(), policy)
123 .await
124 })
125 .await
126 }
127
128 async fn load_executed_tipset_inner(
129 &self,
130 msg_ts: &Tipset,
131 receipt_ts: Option<&Tipset>,
133 policy: StateRecomputePolicy,
134 ) -> anyhow::Result<ExecutedTipset> {
135 let state_compute_disallow_error = || {
136 format!(
137 "failed to load tipset state output and recomputation is disallowed, epoch={}, key={}",
138 msg_ts.epoch(),
139 msg_ts.key()
140 )
141 };
142
143 if let Some(receipt_ts) = receipt_ts {
144 anyhow::ensure!(
145 msg_ts.key() == receipt_ts.parents(),
146 "message tipset should be the parent of message receipt tipset"
147 );
148 }
149 let allow_state_compute = matches!(policy, StateRecomputePolicy::Allowed);
150 let mut recomputed = false;
151 let (state_root, receipt_root, receipts) = match receipt_ts.and_then(|ts| {
152 let receipt_root = *ts.parent_message_receipts();
153 Receipt::get_receipts(self.cs.db(), receipt_root)
154 .ok()
155 .map(|r| (*ts.parent_state(), receipt_root, r))
156 }) {
157 Some((state_root, receipt_root, receipts)) => (state_root, receipt_root, receipts),
158 None => {
159 if !allow_state_compute {
160 anyhow::bail!(state_compute_disallow_error());
161 }
162 let state_output = self
163 .compute_tipset_state(msg_ts.shallow_clone(), NO_CALLBACK, VMTrace::NotTraced)
164 .await?;
165 recomputed = true;
166 (
167 state_output.state_root,
168 state_output.receipt_root,
169 Receipt::get_receipts(self.cs.db(), state_output.receipt_root)?,
170 )
171 }
172 };
173
174 let messages = self.chain_store().messages_for_tipset(msg_ts)?;
175 anyhow::ensure!(
176 messages.len() == receipts.len(),
177 "mismatching message and receipt counts ({} messages, {} receipts)",
178 messages.len(),
179 receipts.len()
180 );
181 let mut executed_messages = Vec::with_capacity(messages.len());
182 for (message, receipt) in messages.iter().cloned().zip(receipts) {
183 let events = if let Some(events_root) = receipt.events_root() {
184 Some(match StampedEvent::get_events(self.cs.db(), &events_root) {
185 Ok(events) => events,
186 Err(e) if recomputed => return Err(e),
187 Err(_) => {
188 if !allow_state_compute {
189 anyhow::bail!(state_compute_disallow_error());
190 }
191 self.compute_tipset_state(
192 msg_ts.shallow_clone(),
193 NO_CALLBACK,
194 VMTrace::NotTraced,
195 )
196 .await?;
197 recomputed = true;
198 StampedEvent::get_events(self.cs.db(), &events_root)?
199 }
200 })
201 } else {
202 None
203 };
204 executed_messages.push(ExecutedMessage {
205 message,
206 receipt,
207 events,
208 });
209 }
210
211 if recomputed
213 && let Err(e) = crate::rpc::eth::store_block_logs_bloom(
214 self,
215 msg_ts,
216 &state_root,
217 &executed_messages,
218 )
219 {
220 warn!(
221 "failed to store block logs bloom for tipset {}: {e:#}",
222 msg_ts.key()
223 );
224 }
225
226 Ok(ExecutedTipset {
227 state_root,
228 receipt_root,
229 executed_messages: Arc::new(executed_messages),
230 })
231 }
232
233 pub async fn compute_tipset_state(
255 &self,
256 tipset: Tipset,
257 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()> + Send + 'static>,
258 enable_tracing: VMTrace,
259 ) -> Result<ExecutedTipset, Error> {
260 let this = self.shallow_clone();
261 tokio::task::spawn_blocking(move || {
262 this.compute_tipset_state_blocking(tipset, callback, enable_tracing)
263 })
264 .await?
265 }
266
267 pub fn compute_tipset_state_blocking(
269 &self,
270 tipset: Tipset,
271 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
272 enable_tracing: VMTrace,
273 ) -> Result<ExecutedTipset, Error> {
274 let epoch = tipset.epoch();
275 let has_callback = callback.is_some();
276 info!(
277 "Evaluating tipset: EPOCH={epoch}, blocks={}, tsk={}",
278 tipset.len(),
279 tipset.key(),
280 );
281 Ok(apply_block_messages_blocking(
282 self.chain_index().shallow_clone(),
283 self.chain_config().shallow_clone(),
284 self.beacon_schedule().shallow_clone(),
285 &self.engine,
286 tipset,
287 callback,
288 enable_tracing,
289 )
290 .map_err(|e| {
291 if has_callback {
292 e
293 } else {
294 e.context(format!("Failed to compute tipset state@{epoch}"))
295 }
296 })?)
297 }
298
299 #[instrument(skip_all)]
300 pub async fn compute_state(
301 &self,
302 height: ChainEpoch,
303 messages: Vec<Message>,
304 tipset: Tipset,
305 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()> + Send + 'static>,
306 enable_tracing: VMTrace,
307 ) -> Result<ExecutedTipset, Error> {
308 let this = self.shallow_clone();
309 tokio::task::spawn_blocking(move || {
310 this.compute_state_blocking(height, messages, tipset, callback, enable_tracing)
311 })
312 .await?
313 }
314
315 #[tracing::instrument(skip_all)]
317 pub fn compute_state_blocking(
318 &self,
319 height: ChainEpoch,
320 messages: Vec<Message>,
321 tipset: Tipset,
322 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
323 enable_tracing: VMTrace,
324 ) -> Result<ExecutedTipset, Error> {
325 Ok(compute_state_blocking(
326 height,
327 messages,
328 tipset,
329 self.chain_index().shallow_clone(),
330 self.chain_config().shallow_clone(),
331 self.beacon_schedule().shallow_clone(),
332 &self.engine,
333 callback,
334 enable_tracing,
335 )?)
336 }
337}
338
339pub fn validate_tipsets_blocking<T>(
340 chain_index: &ChainIndex,
341 chain_config: &Arc<ChainConfig>,
342 beacon: &Arc<BeaconSchedule>,
343 engine: &MultiEngine,
344 tipsets: T,
345) -> anyhow::Result<()>
346where
347 T: Iterator<Item = Tipset> + Send,
348{
349 for (child, parent) in tipsets.tuple_windows() {
354 info!(height = parent.epoch(), "compute parent state");
355 let ExecutedTipset {
356 state_root: actual_state,
357 receipt_root: actual_receipt,
358 ..
359 } = apply_block_messages_blocking(
360 chain_index.shallow_clone(),
361 chain_config.shallow_clone(),
362 beacon.shallow_clone(),
363 engine,
364 parent,
365 NO_CALLBACK,
366 VMTrace::NotTraced,
367 )
368 .context("couldn't compute tipset state")?;
369 let expected_receipt = child.min_ticket_block().message_receipts;
370 let expected_state = child.parent_state();
371 if (expected_state, expected_receipt) != (&actual_state, actual_receipt) {
372 error!(
373 height = child.epoch(),
374 ?expected_state,
375 ?expected_receipt,
376 ?actual_state,
377 ?actual_receipt,
378 "state mismatch"
379 );
380 bail!("state mismatch");
381 }
382 }
383 Ok(())
384}
385
386pub(in crate::state_manager) struct TipsetExecutor<'a> {
391 tipset: Tipset,
392 rand: ChainRand,
393 chain_config: Arc<ChainConfig>,
394 chain_index: ChainIndex,
395 genesis_info: GenesisInfo,
396 engine: &'a MultiEngine,
397}
398
399impl<'a> TipsetExecutor<'a> {
400 pub(in crate::state_manager) fn new(
401 chain_index: ChainIndex,
402 chain_config: Arc<ChainConfig>,
403 beacon: Arc<BeaconSchedule>,
404 engine: &'a MultiEngine,
405 tipset: Tipset,
406 ) -> Self {
407 let rand = ChainRand::new(
408 chain_config.shallow_clone(),
409 tipset.shallow_clone(),
410 chain_index.shallow_clone(),
411 beacon,
412 );
413 let genesis_info = GenesisInfo::from_chain_config(chain_config.shallow_clone());
414 Self {
415 tipset,
416 rand,
417 chain_config,
418 chain_index,
419 genesis_info,
420 engine,
421 }
422 }
423
424 pub(in crate::state_manager) fn create_vm(
425 &self,
426 state_root: Cid,
427 epoch: ChainEpoch,
428 timestamp: u64,
429 trace: VMTrace,
430 ) -> anyhow::Result<VM> {
431 let circ_supply = self.genesis_info.get_vm_circulating_supply(
432 epoch,
433 self.chain_index.db(),
434 &state_root,
435 )?;
436 VM::new(
437 ExecutionContext {
438 heaviest_tipset: self.tipset.shallow_clone(),
439 state_tree_root: state_root,
440 epoch,
441 rand: Box::new(self.rand.shallow_clone()),
442 base_fee: self.tipset.min_ticket_block().parent_base_fee.clone(),
443 circ_supply,
444 chain_config: self.chain_config.shallow_clone(),
445 chain_index: self.chain_index.shallow_clone(),
446 timestamp,
447 },
448 self.engine,
449 trace,
450 )
451 }
452
453 pub(in crate::state_manager) fn prepare_parent_state_blocking<F>(
456 &self,
457 genesis_timestamp: u64,
458 null_epoch_trace: VMTrace,
459 cron_callback: &mut Option<F>,
460 ) -> anyhow::Result<(Cid, ChainEpoch, Vec<BlockMessages>)>
461 where
462 F: FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>,
463 {
464 use crate::shim::clock::EPOCH_DURATION_SECONDS;
465
466 let mut parent_state = *self.tipset.parent_state();
467 let parent_epoch = self
468 .chain_index
469 .load_required_tipset(self.tipset.parents())?
470 .epoch();
471 let epoch = self.tipset.epoch();
472
473 for epoch_i in parent_epoch..epoch {
474 if epoch_i > parent_epoch {
475 let timestamp = genesis_timestamp + ((EPOCH_DURATION_SECONDS * epoch_i) as u64);
476 parent_state = stacker::grow(64 << 20, || -> anyhow::Result<Cid> {
477 let mut vm =
478 self.create_vm(parent_state, epoch_i, timestamp, null_epoch_trace)?;
479 if let Err(e) = vm.run_cron(epoch_i, cron_callback.as_mut()) {
480 error!("Beginning of epoch cron failed to run: {e:#}");
481 return Err(e);
482 }
483 vm.flush()
484 })?;
485 }
486 if let Some(new_state) = run_state_migrations(
487 epoch_i,
488 &self.chain_config,
489 self.chain_index.db(),
490 &parent_state,
491 )? {
492 parent_state = new_state;
493 }
494 }
495
496 let block_messages = BlockMessages::for_tipset(self.chain_index.db(), &self.tipset)?;
497 Ok((parent_state, epoch, block_messages))
498 }
499}
500
501#[allow(clippy::too_many_arguments)]
578pub fn apply_block_messages_blocking(
579 chain_index: ChainIndex,
580 chain_config: Arc<ChainConfig>,
581 beacon: Arc<BeaconSchedule>,
582 engine: &MultiEngine,
583 tipset: Tipset,
584 mut callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
585 enable_tracing: VMTrace,
586) -> anyhow::Result<ExecutedTipset> {
587 let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
596 if tipset.epoch() == 0 {
597 let message_receipts = tipset.min_ticket_block().message_receipts;
602 return Ok(ExecutedTipset {
603 state_root: *tipset.parent_state(),
604 receipt_root: message_receipts,
605 executed_messages: vec![].into(),
606 });
607 }
608
609 let exec = TipsetExecutor::new(
610 chain_index.shallow_clone(),
611 chain_config,
612 beacon,
613 engine,
614 tipset.shallow_clone(),
615 );
616
617 let (parent_state, epoch, block_messages) =
620 exec.prepare_parent_state_blocking(genesis_timestamp, enable_tracing, &mut callback)?;
621
622 stacker::grow(64 << 20, || -> anyhow::Result<ExecutedTipset> {
625 let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?;
626
627 let (receipts, events, events_roots) =
629 vm.apply_block_messages(&block_messages, epoch, callback)?;
630
631 let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts.iter())?;
633
634 for (events, events_root) in events.iter().zip(events_roots.iter()) {
636 if let Some(events) = events {
637 let event_root =
638 events_root.context("events root should be present when events present")?;
639 let derived_event_root = Amt::new_from_iter_with_bit_width(
641 chain_index.db(),
642 EVENTS_AMT_BITWIDTH,
643 events.iter(),
644 )
645 .map_err(|e| Error::Other(format!("failed to store events AMT: {e}")))?;
646
647 ensure!(
649 derived_event_root == event_root,
650 "Events AMT root mismatch: derived={derived_event_root}, actual={event_root}."
651 );
652 }
653 }
654
655 let state_root = vm.flush()?;
656
657 let messages: Vec<ChainMessage> = block_messages
659 .into_iter()
660 .flat_map(|bm| bm.messages)
661 .collect_vec();
662 anyhow::ensure!(
663 messages.len() == receipts.len() && messages.len() == events.len(),
664 "length of messages, receipts, and events should match",
665 );
666 Ok(ExecutedTipset {
667 state_root,
668 receipt_root,
669 executed_messages: messages
670 .into_iter()
671 .zip(receipts)
672 .zip(events)
673 .map(|((message, receipt), events)| ExecutedMessage {
674 message,
675 receipt,
676 events,
677 })
678 .collect_vec()
679 .into(),
680 })
681 })
682}
683
684#[allow(clippy::too_many_arguments)]
685pub(in crate::state_manager) fn compute_state_blocking(
686 _height: ChainEpoch,
687 messages: Vec<Message>,
688 tipset: Tipset,
689 chain_index: ChainIndex,
690 chain_config: Arc<ChainConfig>,
691 beacon: Arc<BeaconSchedule>,
692 engine: &MultiEngine,
693 callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
694 enable_tracing: VMTrace,
695) -> anyhow::Result<ExecutedTipset> {
696 if !messages.is_empty() {
697 anyhow::bail!("Applying messages is not yet implemented.");
698 }
699
700 let output = apply_block_messages_blocking(
701 chain_index,
702 chain_config,
703 beacon,
704 engine,
705 tipset,
706 callback,
707 enable_tracing,
708 )?;
709
710 Ok(output)
711}