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    /// Load an executed tipset without reading from or populating the cache. Errors on a missing
105    /// state output unless `allow_state_compute` is true.
106    pub async fn load_executed_tipset_uncached(
107        &self,
108        ts: &Tipset,
109        allow_state_compute: bool,
110    ) -> anyhow::Result<ExecutedTipset> {
111        let policy = if allow_state_compute {
112            StateRecomputePolicy::Allowed
113        } else {
114            StateRecomputePolicy::Disallowed
115        };
116        let receipt_ts = self.chain_store().load_child_tipset(ts).await?;
117        self.load_executed_tipset_inner(ts, receipt_ts.as_ref(), policy)
118            .await
119    }
120
121    async fn load_executed_tipset_with_cache(
122        &self,
123        ts: &Tipset,
124        policy: StateRecomputePolicy,
125    ) -> anyhow::Result<ExecutedTipset> {
126        // validate the existence of state trees for post-chain-head-epoch tipsets in case chain head is reset(e.g. manually or via GC).
127        if ts.epoch() >= self.heaviest_tipset().epoch()
128            && let Some(cached) = self.cache.get(ts.key())
129        {
130            if StateTree::new_from_root(self.db(), &cached.state_root).is_ok() {
131                return Ok(cached);
132            } else {
133                self.cache.remove(ts.key());
134            }
135        }
136        self.cache
137            .get_or_insert_async(ts.key(), async move {
138                let receipt_ts = self.chain_store().load_child_tipset(ts).await?;
139                self.load_executed_tipset_inner(ts, receipt_ts.as_ref(), policy)
140                    .await
141            })
142            .await
143    }
144
145    async fn load_executed_tipset_inner(
146        &self,
147        msg_ts: &Tipset,
148        // when `msg_ts` is the current head, `receipt_ts` is `None`
149        receipt_ts: Option<&Tipset>,
150        policy: StateRecomputePolicy,
151    ) -> anyhow::Result<ExecutedTipset> {
152        let state_compute_disallow_error = || {
153            format!(
154                "failed to load tipset state output and recomputation is disallowed, epoch={}, key={}",
155                msg_ts.epoch(),
156                msg_ts.key()
157            )
158        };
159
160        if let Some(receipt_ts) = receipt_ts {
161            anyhow::ensure!(
162                msg_ts.key() == receipt_ts.parents(),
163                "message tipset should be the parent of message receipt tipset"
164            );
165        }
166        let allow_state_compute = matches!(policy, StateRecomputePolicy::Allowed);
167        let mut recomputed = false;
168        let (state_root, receipt_root, receipts) = match receipt_ts.and_then(|ts| {
169            let receipt_root = *ts.parent_message_receipts();
170            Receipt::get_receipts(self.cs.db(), receipt_root)
171                .ok()
172                .map(|r| (*ts.parent_state(), receipt_root, r))
173        }) {
174            Some((state_root, receipt_root, receipts)) => (state_root, receipt_root, receipts),
175            None => {
176                if !allow_state_compute {
177                    anyhow::bail!(state_compute_disallow_error());
178                }
179                let state_output = self
180                    .compute_tipset_state(msg_ts.shallow_clone(), NO_CALLBACK, VMTrace::NotTraced)
181                    .await?;
182                recomputed = true;
183                (
184                    state_output.state_root,
185                    state_output.receipt_root,
186                    Receipt::get_receipts(self.cs.db(), state_output.receipt_root)?,
187                )
188            }
189        };
190
191        let messages = self.chain_store().messages_for_tipset(msg_ts)?;
192        anyhow::ensure!(
193            messages.len() == receipts.len(),
194            "mismatching message and receipt counts ({} messages, {} receipts)",
195            messages.len(),
196            receipts.len()
197        );
198        let mut executed_messages = Vec::with_capacity(messages.len());
199        for (message, receipt) in messages.iter().cloned().zip(receipts) {
200            let events = if let Some(events_root) = receipt.events_root() {
201                Some(match StampedEvent::get_events(self.cs.db(), &events_root) {
202                    Ok(events) => events,
203                    Err(e) if recomputed => return Err(e),
204                    Err(_) => {
205                        if !allow_state_compute {
206                            anyhow::bail!(state_compute_disallow_error());
207                        }
208                        self.compute_tipset_state(
209                            msg_ts.shallow_clone(),
210                            NO_CALLBACK,
211                            VMTrace::NotTraced,
212                        )
213                        .await?;
214                        recomputed = true;
215                        StampedEvent::get_events(self.cs.db(), &events_root)?
216                    }
217                })
218            } else {
219                None
220            };
221            executed_messages.push(ExecutedMessage {
222                message,
223                receipt,
224                events,
225            });
226        }
227
228        // Store the block logs bloom whenever this tipset was executed here.
229        if recomputed
230            && let Err(e) = crate::rpc::eth::store_block_logs_bloom(
231                self,
232                msg_ts,
233                &state_root,
234                &executed_messages,
235            )
236        {
237            warn!(
238                "failed to store block logs bloom for tipset {}: {e:#}",
239                msg_ts.key()
240            );
241        }
242
243        Ok(ExecutedTipset {
244            state_root,
245            receipt_root,
246            executed_messages: Arc::new(executed_messages),
247        })
248    }
249
250    /// Conceptually, a [`Tipset`] consists of _blocks_ which share an _epoch_.
251    /// Each _block_ contains _messages_, which are executed by the _Filecoin Virtual Machine_.
252    ///
253    /// VM message execution essentially looks like this:
254    /// ```text
255    /// state[N-900..N] * message = state[N+1]
256    /// ```
257    ///
258    /// The `state`s above are stored in the `IPLD Blockstore`, and can be referred to by
259    /// a [`Cid`] - the _state root_.
260    /// The previous 900 states (configurable, see
261    /// <https://docs.filecoin.io/reference/general/glossary/#finality>) can be
262    /// queried when executing a message, so a store needs at least that many.
263    /// (a snapshot typically contains 2000, for example).
264    ///
265    /// Each message costs FIL to execute - this is _gas_.
266    /// After execution, the message has a _receipt_, showing how much gas was spent.
267    /// This is similarly a [`Cid`] into the block store.
268    ///
269    /// For details, see the documentation for [`apply_block_messages`].
270    ///
271    pub async fn compute_tipset_state(
272        &self,
273        tipset: Tipset,
274        callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()> + Send + 'static>,
275        enable_tracing: VMTrace,
276    ) -> Result<ExecutedTipset, Error> {
277        let this = self.shallow_clone();
278        tokio::task::spawn_blocking(move || {
279            this.compute_tipset_state_blocking(tipset, callback, enable_tracing)
280        })
281        .await?
282    }
283
284    /// Blocking version of `compute_tipset_state`
285    pub fn compute_tipset_state_blocking(
286        &self,
287        tipset: Tipset,
288        callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
289        enable_tracing: VMTrace,
290    ) -> Result<ExecutedTipset, Error> {
291        let epoch = tipset.epoch();
292        let has_callback = callback.is_some();
293        info!(
294            "Evaluating tipset: EPOCH={epoch}, blocks={}, tsk={}",
295            tipset.len(),
296            tipset.key(),
297        );
298        Ok(apply_block_messages_blocking(
299            self.chain_index().shallow_clone(),
300            self.chain_config().shallow_clone(),
301            self.beacon_schedule().shallow_clone(),
302            &self.engine,
303            tipset,
304            callback,
305            enable_tracing,
306        )
307        .map_err(|e| {
308            if has_callback {
309                e
310            } else {
311                e.context(format!("Failed to compute tipset state@{epoch}"))
312            }
313        })?)
314    }
315
316    #[instrument(skip_all)]
317    pub async fn compute_state(
318        &self,
319        height: ChainEpoch,
320        messages: Vec<Message>,
321        tipset: Tipset,
322        callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()> + Send + 'static>,
323        enable_tracing: VMTrace,
324    ) -> Result<ExecutedTipset, Error> {
325        let this = self.shallow_clone();
326        tokio::task::spawn_blocking(move || {
327            this.compute_state_blocking(height, messages, tipset, callback, enable_tracing)
328        })
329        .await?
330    }
331
332    /// Blocking version of `compute_state`
333    #[tracing::instrument(skip_all)]
334    pub fn compute_state_blocking(
335        &self,
336        height: ChainEpoch,
337        messages: Vec<Message>,
338        tipset: Tipset,
339        callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
340        enable_tracing: VMTrace,
341    ) -> Result<ExecutedTipset, Error> {
342        Ok(compute_state_blocking(
343            height,
344            messages,
345            tipset,
346            self.chain_index().shallow_clone(),
347            self.chain_config().shallow_clone(),
348            self.beacon_schedule().shallow_clone(),
349            &self.engine,
350            callback,
351            enable_tracing,
352        )?)
353    }
354}
355
356pub fn validate_tipsets_blocking<T>(
357    chain_index: &ChainIndex,
358    chain_config: &Arc<ChainConfig>,
359    beacon: &Arc<BeaconSchedule>,
360    engine: &MultiEngine,
361    tipsets: T,
362) -> anyhow::Result<()>
363where
364    T: Iterator<Item = Tipset> + Send,
365{
366    // Validate one tipset at a time. Parallelizing the outer loop across tipsets
367    // might wedge the global rayon pool.
368    // Sequential outer iteration leaves the entire rayon pool free for that
369    // already-rich inner parallelism.
370    for (child, parent) in tipsets.tuple_windows() {
371        info!(height = parent.epoch(), "compute parent state");
372        let ExecutedTipset {
373            state_root: actual_state,
374            receipt_root: actual_receipt,
375            ..
376        } = apply_block_messages_blocking(
377            chain_index.shallow_clone(),
378            chain_config.shallow_clone(),
379            beacon.shallow_clone(),
380            engine,
381            parent,
382            NO_CALLBACK,
383            VMTrace::NotTraced,
384        )
385        .context("couldn't compute tipset state")?;
386        let expected_receipt = child.min_ticket_block().message_receipts;
387        let expected_state = child.parent_state();
388        if (expected_state, expected_receipt) != (&actual_state, actual_receipt) {
389            error!(
390                height = child.epoch(),
391                ?expected_state,
392                ?expected_receipt,
393                ?actual_state,
394                ?actual_receipt,
395                "state mismatch"
396            );
397            bail!("state mismatch");
398        }
399    }
400    Ok(())
401}
402
403/// Shared context for creating VMs and preparing tipset state.
404///
405/// Encapsulates randomness source, genesis info, VM construction,
406/// null-epoch cron handling, and state migrations.
407pub(in crate::state_manager) struct TipsetExecutor<'a> {
408    tipset: Tipset,
409    rand: ChainRand,
410    chain_config: Arc<ChainConfig>,
411    chain_index: ChainIndex,
412    genesis_info: GenesisInfo,
413    engine: &'a MultiEngine,
414}
415
416impl<'a> TipsetExecutor<'a> {
417    pub(in crate::state_manager) fn new(
418        chain_index: ChainIndex,
419        chain_config: Arc<ChainConfig>,
420        beacon: Arc<BeaconSchedule>,
421        engine: &'a MultiEngine,
422        tipset: Tipset,
423    ) -> Self {
424        let rand = ChainRand::new(
425            chain_config.shallow_clone(),
426            tipset.shallow_clone(),
427            chain_index.shallow_clone(),
428            beacon,
429        );
430        let genesis_info = GenesisInfo::from_chain_config(chain_config.shallow_clone());
431        Self {
432            tipset,
433            rand,
434            chain_config,
435            chain_index,
436            genesis_info,
437            engine,
438        }
439    }
440
441    pub(in crate::state_manager) fn create_vm(
442        &self,
443        state_root: Cid,
444        epoch: ChainEpoch,
445        timestamp: u64,
446        trace: VMTrace,
447    ) -> anyhow::Result<VM> {
448        let circ_supply = self.genesis_info.get_vm_circulating_supply(
449            epoch,
450            self.chain_index.db(),
451            &state_root,
452        )?;
453        VM::new(
454            ExecutionContext {
455                heaviest_tipset: self.tipset.shallow_clone(),
456                state_tree_root: state_root,
457                epoch,
458                rand: Box::new(self.rand.shallow_clone()),
459                base_fee: self.tipset.min_ticket_block().parent_base_fee.clone(),
460                circ_supply,
461                chain_config: self.chain_config.shallow_clone(),
462                chain_index: self.chain_index.shallow_clone(),
463                timestamp,
464            },
465            self.engine,
466            trace,
467        )
468    }
469
470    /// Produces the state root ready for message execution by running
471    /// null-epoch `crons` and any pending state migrations.
472    pub(in crate::state_manager) fn prepare_parent_state_blocking<F>(
473        &self,
474        genesis_timestamp: u64,
475        null_epoch_trace: VMTrace,
476        cron_callback: &mut Option<F>,
477    ) -> anyhow::Result<(Cid, ChainEpoch, Vec<BlockMessages>)>
478    where
479        F: FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>,
480    {
481        use crate::shim::clock::EPOCH_DURATION_SECONDS;
482
483        let mut parent_state = *self.tipset.parent_state();
484        let parent_epoch = self
485            .chain_index
486            .load_required_tipset(self.tipset.parents())?
487            .epoch();
488        let epoch = self.tipset.epoch();
489
490        for epoch_i in parent_epoch..epoch {
491            if epoch_i > parent_epoch {
492                let timestamp = genesis_timestamp + ((EPOCH_DURATION_SECONDS * epoch_i) as u64);
493                parent_state = stacker::grow(64 << 20, || -> anyhow::Result<Cid> {
494                    let mut vm =
495                        self.create_vm(parent_state, epoch_i, timestamp, null_epoch_trace)?;
496                    if let Err(e) = vm.run_cron(epoch_i, cron_callback.as_mut()) {
497                        error!("Beginning of epoch cron failed to run: {e:#}");
498                        return Err(e);
499                    }
500                    vm.flush()
501                })?;
502            }
503            if let Some(new_state) = run_state_migrations(
504                epoch_i,
505                &self.chain_config,
506                self.chain_index.db(),
507                &parent_state,
508            )? {
509                parent_state = new_state;
510            }
511        }
512
513        let block_messages = BlockMessages::for_tipset(self.chain_index.db(), &self.tipset)?;
514        Ok((parent_state, epoch, block_messages))
515    }
516}
517
518/// Messages are transactions that produce new states. The state (usually
519/// referred to as the 'state-tree') is a mapping from actor addresses to actor
520/// states. Each block contains the hash of the state-tree that should be used
521/// as the starting state when executing the block messages.
522///
523/// # Execution environment
524///
525/// Transaction execution has the following inputs:
526/// - a current state-tree (stored as IPLD in a key-value database). This
527///   reference is in [`Tipset::parent_state`].
528/// - up to 900 past state-trees. See
529///   <https://docs.filecoin.io/reference/general/glossary/#finality>.
530/// - up to 900 past tipset IDs.
531/// - a deterministic source of randomness.
532/// - the circulating supply of FIL (see
533///   <https://filecoin.io/blog/filecoin-circulating-supply/>). The circulating
534///   supply is determined by the epoch and the states of a few key actors.
535/// - the base fee (see <https://spec.filecoin.io/systems/filecoin_vm/gas_fee/>).
536///   This value is defined by `tipset.parent_base_fee`.
537/// - the genesis timestamp (UNIX epoch time when the first block was
538///   mined/created).
539/// - a chain configuration (maps epoch to network version, has chain specific
540///   settings).
541///
542/// The result of running a set of block messages is an index to the final
543/// state-tree and an index to an array of message receipts (listing gas used,
544/// return codes, etc).
545///
546/// # Cron and null tipsets
547///
548/// Once per epoch, after all messages have run, a special 'cron' transaction
549/// must be executed. The tasks of the 'cron' transaction include running batch
550/// jobs and keeping the state up-to-date with the current epoch.
551///
552/// It can happen that no blocks are mined in an epoch. The tipset for such an
553/// epoch is called a null tipset. A null tipset has no identity and cannot be
554/// directly executed. This is a problem for 'cron' which must run for every
555/// epoch, even if there are no messages. The fix is to run 'cron' if there are
556/// any null tipsets between the current epoch and the parent epoch.
557///
558/// Imagine the blockchain looks like this with a null tipset at epoch 9:
559///
560/// ```text
561/// ┌────────┐ ┌────┐ ┌───────┐  ┌───────┐
562/// │Epoch 10│ │Null│ │Epoch 8├──►Epoch 7├─►
563/// └───┬────┘ └────┘ └───▲───┘  └───────┘
564///     └─────────────────┘
565/// ```
566///
567/// The parent of tipset-epoch-10 is tipset-epoch-8. Before executing the
568/// messages in epoch 10, we have to run cron for epoch 9. However, running
569/// 'cron' requires the timestamp of the youngest block in the tipset (which
570/// doesn't exist because there are no blocks in the tipset). Lotus dictates that
571/// the timestamp of a null tipset is `30s * epoch` after the genesis timestamp.
572/// So, in the above example, if the genesis block was mined at time `X`, the
573/// null tipset for epoch 9 will have timestamp `X + 30 * 9`.
574///
575/// # Migrations
576///
577/// Migrations happen between network upgrades and modify the state tree. If a
578/// migration is scheduled for epoch 10, it will be run _after_ the messages for
579/// epoch 10. The tipset for epoch 11 will link the state-tree produced by the
580/// migration.
581///
582/// Example timeline with a migration at epoch 10:
583///   1. Tipset-epoch-10 executes, producing state-tree A.
584///   2. Migration consumes state-tree A and produces state-tree B.
585///   3. Tipset-epoch-11 executes, consuming state-tree B (rather than A).
586///
587/// Note: The migration actually happens when tipset-epoch-11 executes. This is
588///       because tipset-epoch-10 may be null and therefore not executed at all.
589///
590/// # Caching
591///
592/// Scanning the blockchain to find past tipsets and state-trees may be slow.
593/// The `ChainStore` caches recent tipsets to make these scans faster.
594#[allow(clippy::too_many_arguments)]
595pub fn apply_block_messages_blocking(
596    chain_index: ChainIndex,
597    chain_config: Arc<ChainConfig>,
598    beacon: Arc<BeaconSchedule>,
599    engine: &MultiEngine,
600    tipset: Tipset,
601    mut callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
602    enable_tracing: VMTrace,
603) -> anyhow::Result<ExecutedTipset> {
604    // This function will:
605    // 1. handle the genesis block as a special case
606    // 2. run 'cron' for any null-tipsets between the current tipset and our parent tipset
607    // 3. run migrations
608    // 4. execute block messages
609    // 5. write the state-tree to the DB and return the CID
610
611    // step 1: special case for genesis block
612    let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
613    if tipset.epoch() == 0 {
614        // NB: This is here because the process that executes blocks requires that the
615        // block miner reference a valid miner in the state tree. Unless we create some
616        // magical genesis miner, this won't work properly, so we short circuit here
617        // This avoids the question of 'who gets paid the genesis block reward'
618        let message_receipts = tipset.min_ticket_block().message_receipts;
619        return Ok(ExecutedTipset {
620            state_root: *tipset.parent_state(),
621            receipt_root: message_receipts,
622            executed_messages: vec![].into(),
623        });
624    }
625
626    let exec = TipsetExecutor::new(
627        chain_index.shallow_clone(),
628        chain_config,
629        beacon,
630        engine,
631        tipset.shallow_clone(),
632    );
633
634    // step 2: running cron for any null-tipsets
635    // step 3: run migrations
636    let (parent_state, epoch, block_messages) =
637        exec.prepare_parent_state_blocking(genesis_timestamp, enable_tracing, &mut callback)?;
638
639    // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from
640    // FVM, but that introduces some constraints, and possible deadlocks.
641    stacker::grow(64 << 20, || -> anyhow::Result<ExecutedTipset> {
642        let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?;
643
644        // step 4: apply tipset messages
645        let (receipts, events, events_roots) =
646            vm.apply_block_messages(&block_messages, epoch, callback)?;
647
648        // step 5: construct receipt root from receipts
649        let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts.iter())?;
650
651        // step 6: store events AMTs in the blockstore
652        for (events, events_root) in events.iter().zip(events_roots.iter()) {
653            if let Some(events) = events {
654                let event_root =
655                    events_root.context("events root should be present when events present")?;
656                // Store the events AMT - the root CID should match the one computed by FVM
657                let derived_event_root = Amt::new_from_iter_with_bit_width(
658                    chain_index.db(),
659                    EVENTS_AMT_BITWIDTH,
660                    events.iter(),
661                )
662                .map_err(|e| Error::Other(format!("failed to store events AMT: {e}")))?;
663
664                // Verify the stored root matches the FVM-computed root
665                ensure!(
666                    derived_event_root == event_root,
667                    "Events AMT root mismatch: derived={derived_event_root}, actual={event_root}."
668                );
669            }
670        }
671
672        let state_root = vm.flush()?;
673
674        // Update executed tipset cache
675        let messages: Vec<ChainMessage> = block_messages
676            .into_iter()
677            .flat_map(|bm| bm.messages)
678            .collect_vec();
679        anyhow::ensure!(
680            messages.len() == receipts.len() && messages.len() == events.len(),
681            "length of messages, receipts, and events should match",
682        );
683        Ok(ExecutedTipset {
684            state_root,
685            receipt_root,
686            executed_messages: messages
687                .into_iter()
688                .zip(receipts)
689                .zip(events)
690                .map(|((message, receipt), events)| ExecutedMessage {
691                    message,
692                    receipt,
693                    events,
694                })
695                .collect_vec()
696                .into(),
697        })
698    })
699}
700
701#[allow(clippy::too_many_arguments)]
702pub(in crate::state_manager) fn compute_state_blocking(
703    _height: ChainEpoch,
704    messages: Vec<Message>,
705    tipset: Tipset,
706    chain_index: ChainIndex,
707    chain_config: Arc<ChainConfig>,
708    beacon: Arc<BeaconSchedule>,
709    engine: &MultiEngine,
710    callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
711    enable_tracing: VMTrace,
712) -> anyhow::Result<ExecutedTipset> {
713    if !messages.is_empty() {
714        anyhow::bail!("Applying messages is not yet implemented.");
715    }
716
717    let output = apply_block_messages_blocking(
718        chain_index,
719        chain_config,
720        beacon,
721        engine,
722        tipset,
723        callback,
724        enable_tracing,
725    )?;
726
727    Ok(output)
728}