Skip to main content

forest/state_manager/
state_computation.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use 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    /// Load the state of a tipset, including state root, message receipts
21    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    /// Clears all cached state outputs and traces. Used after repairing corrupted
36    /// computation inputs (e.g. a stale tipset lookup entry): any cached result may have
37    /// been derived from the poisoned data, and the tainted ones cannot be told apart.
38    pub fn clear_tipset_state_caches(&self) {
39        self.cache.clear();
40        self.trace_cache.clear();
41    }
42
43    /// Verifies and repairs the tipset lookup table (see `ChainStore::repair_tipset_lookup`)
44    /// and clears the state caches when anything was repaired: results computed while the
45    /// entries were wrong may be tainted.
46    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    /// State recomputation policy for RPC methods: recomputation is disabled unless explicitly
55    /// enabled via the environment.
56    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    /// Load an executed tipset for RPC methods, with state computation unless explicitly enabled.
70    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    /// Load an executed tipset using an explicitly provided receipt (child) tipset instead of
79    /// resolving the child on the current heaviest chain. This is required when serving events
80    /// for tipsets that are no longer canonical.
81    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    /// Load an executed tipset, including state root, message receipts and events with caching.
99    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        // validate the existence of state trees for post-chain-head-epoch tipsets in case chain head is reset(e.g. manually or via GC).
110        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        // when `msg_ts` is the current head, `receipt_ts` is `None`
132        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        // Store the block logs bloom whenever this tipset was executed here.
212        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    /// Conceptually, a [`Tipset`] consists of _blocks_ which share an _epoch_.
234    /// Each _block_ contains _messages_, which are executed by the _Filecoin Virtual Machine_.
235    ///
236    /// VM message execution essentially looks like this:
237    /// ```text
238    /// state[N-900..N] * message = state[N+1]
239    /// ```
240    ///
241    /// The `state`s above are stored in the `IPLD Blockstore`, and can be referred to by
242    /// a [`Cid`] - the _state root_.
243    /// The previous 900 states (configurable, see
244    /// <https://docs.filecoin.io/reference/general/glossary/#finality>) can be
245    /// queried when executing a message, so a store needs at least that many.
246    /// (a snapshot typically contains 2000, for example).
247    ///
248    /// Each message costs FIL to execute - this is _gas_.
249    /// After execution, the message has a _receipt_, showing how much gas was spent.
250    /// This is similarly a [`Cid`] into the block store.
251    ///
252    /// For details, see the documentation for [`apply_block_messages`].
253    ///
254    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    /// Blocking version of `compute_tipset_state`
268    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    /// Blocking version of `compute_state`
316    #[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    // Validate one tipset at a time. Parallelizing the outer loop across tipsets
350    // might wedge the global rayon pool.
351    // Sequential outer iteration leaves the entire rayon pool free for that
352    // already-rich inner parallelism.
353    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
386/// Shared context for creating VMs and preparing tipset state.
387///
388/// Encapsulates randomness source, genesis info, VM construction,
389/// null-epoch cron handling, and state migrations.
390pub(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    /// Produces the state root ready for message execution by running
454    /// null-epoch `crons` and any pending state migrations.
455    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/// Messages are transactions that produce new states. The state (usually
502/// referred to as the 'state-tree') is a mapping from actor addresses to actor
503/// states. Each block contains the hash of the state-tree that should be used
504/// as the starting state when executing the block messages.
505///
506/// # Execution environment
507///
508/// Transaction execution has the following inputs:
509/// - a current state-tree (stored as IPLD in a key-value database). This
510///   reference is in [`Tipset::parent_state`].
511/// - up to 900 past state-trees. See
512///   <https://docs.filecoin.io/reference/general/glossary/#finality>.
513/// - up to 900 past tipset IDs.
514/// - a deterministic source of randomness.
515/// - the circulating supply of FIL (see
516///   <https://filecoin.io/blog/filecoin-circulating-supply/>). The circulating
517///   supply is determined by the epoch and the states of a few key actors.
518/// - the base fee (see <https://spec.filecoin.io/systems/filecoin_vm/gas_fee/>).
519///   This value is defined by `tipset.parent_base_fee`.
520/// - the genesis timestamp (UNIX epoch time when the first block was
521///   mined/created).
522/// - a chain configuration (maps epoch to network version, has chain specific
523///   settings).
524///
525/// The result of running a set of block messages is an index to the final
526/// state-tree and an index to an array of message receipts (listing gas used,
527/// return codes, etc).
528///
529/// # Cron and null tipsets
530///
531/// Once per epoch, after all messages have run, a special 'cron' transaction
532/// must be executed. The tasks of the 'cron' transaction include running batch
533/// jobs and keeping the state up-to-date with the current epoch.
534///
535/// It can happen that no blocks are mined in an epoch. The tipset for such an
536/// epoch is called a null tipset. A null tipset has no identity and cannot be
537/// directly executed. This is a problem for 'cron' which must run for every
538/// epoch, even if there are no messages. The fix is to run 'cron' if there are
539/// any null tipsets between the current epoch and the parent epoch.
540///
541/// Imagine the blockchain looks like this with a null tipset at epoch 9:
542///
543/// ```text
544/// ┌────────┐ ┌────┐ ┌───────┐  ┌───────┐
545/// │Epoch 10│ │Null│ │Epoch 8├──►Epoch 7├─►
546/// └───┬────┘ └────┘ └───▲───┘  └───────┘
547///     └─────────────────┘
548/// ```
549///
550/// The parent of tipset-epoch-10 is tipset-epoch-8. Before executing the
551/// messages in epoch 10, we have to run cron for epoch 9. However, running
552/// 'cron' requires the timestamp of the youngest block in the tipset (which
553/// doesn't exist because there are no blocks in the tipset). Lotus dictates that
554/// the timestamp of a null tipset is `30s * epoch` after the genesis timestamp.
555/// So, in the above example, if the genesis block was mined at time `X`, the
556/// null tipset for epoch 9 will have timestamp `X + 30 * 9`.
557///
558/// # Migrations
559///
560/// Migrations happen between network upgrades and modify the state tree. If a
561/// migration is scheduled for epoch 10, it will be run _after_ the messages for
562/// epoch 10. The tipset for epoch 11 will link the state-tree produced by the
563/// migration.
564///
565/// Example timeline with a migration at epoch 10:
566///   1. Tipset-epoch-10 executes, producing state-tree A.
567///   2. Migration consumes state-tree A and produces state-tree B.
568///   3. Tipset-epoch-11 executes, consuming state-tree B (rather than A).
569///
570/// Note: The migration actually happens when tipset-epoch-11 executes. This is
571///       because tipset-epoch-10 may be null and therefore not executed at all.
572///
573/// # Caching
574///
575/// Scanning the blockchain to find past tipsets and state-trees may be slow.
576/// The `ChainStore` caches recent tipsets to make these scans faster.
577#[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    // This function will:
588    // 1. handle the genesis block as a special case
589    // 2. run 'cron' for any null-tipsets between the current tipset and our parent tipset
590    // 3. run migrations
591    // 4. execute block messages
592    // 5. write the state-tree to the DB and return the CID
593
594    // step 1: special case for genesis block
595    let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
596    if tipset.epoch() == 0 {
597        // NB: This is here because the process that executes blocks requires that the
598        // block miner reference a valid miner in the state tree. Unless we create some
599        // magical genesis miner, this won't work properly, so we short circuit here
600        // This avoids the question of 'who gets paid the genesis block reward'
601        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    // step 2: running cron for any null-tipsets
618    // step 3: run migrations
619    let (parent_state, epoch, block_messages) =
620        exec.prepare_parent_state_blocking(genesis_timestamp, enable_tracing, &mut callback)?;
621
622    // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from
623    // FVM, but that introduces some constraints, and possible deadlocks.
624    stacker::grow(64 << 20, || -> anyhow::Result<ExecutedTipset> {
625        let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?;
626
627        // step 4: apply tipset messages
628        let (receipts, events, events_roots) =
629            vm.apply_block_messages(&block_messages, epoch, callback)?;
630
631        // step 5: construct receipt root from receipts
632        let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts.iter())?;
633
634        // step 6: store events AMTs in the blockstore
635        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                // Store the events AMT - the root CID should match the one computed by FVM
640                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                // Verify the stored root matches the FVM-computed root
648                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        // Update executed tipset cache
658        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}