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    /// Returns `ts`'s messages paired with their execution receipts, without loading events.
79    /// `receipt_ts` is `ts`'s child (whose `parent_message_receipts` is `ts`'s receipt root) when the
80    /// caller already knows it, avoiding a `load_child_tipset` lookup; `None` resolves it.
81    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    /// Load an executed tipset using an explicitly provided receipt (child) tipset instead of
120    /// resolving the child on the current heaviest chain. This is required when serving events
121    /// for tipsets that are no longer canonical.
122    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    /// Load an executed tipset, including state root, message receipts and events with caching.
140    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    /// Load an executed tipset without reading from or populating the cache. Errors on a missing
146    /// state output unless `allow_state_compute` is true.
147    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        // validate the existence of state trees for post-chain-head-epoch tipsets in case chain head is reset(e.g. manually or via GC).
168        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        // when `msg_ts` is the current head, `receipt_ts` is `None`
190        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        // Store the block logs bloom whenever this tipset was executed here.
270        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    /// Conceptually, a [`Tipset`] consists of _blocks_ which share an _epoch_.
292    /// Each _block_ contains _messages_, which are executed by the _Filecoin Virtual Machine_.
293    ///
294    /// VM message execution essentially looks like this:
295    /// ```text
296    /// state[N-900..N] * message = state[N+1]
297    /// ```
298    ///
299    /// The `state`s above are stored in the `IPLD Blockstore`, and can be referred to by
300    /// a [`Cid`] - the _state root_.
301    /// The previous 900 states (configurable, see
302    /// <https://docs.filecoin.io/reference/general/glossary/#finality>) can be
303    /// queried when executing a message, so a store needs at least that many.
304    /// (a snapshot typically contains 2000, for example).
305    ///
306    /// Each message costs FIL to execute - this is _gas_.
307    /// After execution, the message has a _receipt_, showing how much gas was spent.
308    /// This is similarly a [`Cid`] into the block store.
309    ///
310    /// For details, see the documentation for [`apply_block_messages`].
311    ///
312    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    /// Blocking version of `compute_tipset_state`
326    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    /// Blocking version of `compute_state`
374    #[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    // Validate one tipset at a time. Parallelizing the outer loop across tipsets
408    // might wedge the global rayon pool.
409    // Sequential outer iteration leaves the entire rayon pool free for that
410    // already-rich inner parallelism.
411    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
444/// Shared context for creating VMs and preparing tipset state.
445///
446/// Encapsulates randomness source, genesis info, VM construction,
447/// null-epoch cron handling, and state migrations.
448pub(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    /// Produces the state root ready for message execution by running
512    /// null-epoch `crons` and any pending state migrations.
513    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/// Messages are transactions that produce new states. The state (usually
560/// referred to as the 'state-tree') is a mapping from actor addresses to actor
561/// states. Each block contains the hash of the state-tree that should be used
562/// as the starting state when executing the block messages.
563///
564/// # Execution environment
565///
566/// Transaction execution has the following inputs:
567/// - a current state-tree (stored as IPLD in a key-value database). This
568///   reference is in [`Tipset::parent_state`].
569/// - up to 900 past state-trees. See
570///   <https://docs.filecoin.io/reference/general/glossary/#finality>.
571/// - up to 900 past tipset IDs.
572/// - a deterministic source of randomness.
573/// - the circulating supply of FIL (see
574///   <https://filecoin.io/blog/filecoin-circulating-supply/>). The circulating
575///   supply is determined by the epoch and the states of a few key actors.
576/// - the base fee (see <https://spec.filecoin.io/systems/filecoin_vm/gas_fee/>).
577///   This value is defined by `tipset.parent_base_fee`.
578/// - the genesis timestamp (UNIX epoch time when the first block was
579///   mined/created).
580/// - a chain configuration (maps epoch to network version, has chain specific
581///   settings).
582///
583/// The result of running a set of block messages is an index to the final
584/// state-tree and an index to an array of message receipts (listing gas used,
585/// return codes, etc).
586///
587/// # Cron and null tipsets
588///
589/// Once per epoch, after all messages have run, a special 'cron' transaction
590/// must be executed. The tasks of the 'cron' transaction include running batch
591/// jobs and keeping the state up-to-date with the current epoch.
592///
593/// It can happen that no blocks are mined in an epoch. The tipset for such an
594/// epoch is called a null tipset. A null tipset has no identity and cannot be
595/// directly executed. This is a problem for 'cron' which must run for every
596/// epoch, even if there are no messages. The fix is to run 'cron' if there are
597/// any null tipsets between the current epoch and the parent epoch.
598///
599/// Imagine the blockchain looks like this with a null tipset at epoch 9:
600///
601/// ```text
602/// ┌────────┐ ┌────┐ ┌───────┐  ┌───────┐
603/// │Epoch 10│ │Null│ │Epoch 8├──►Epoch 7├─►
604/// └───┬────┘ └────┘ └───▲───┘  └───────┘
605///     └─────────────────┘
606/// ```
607///
608/// The parent of tipset-epoch-10 is tipset-epoch-8. Before executing the
609/// messages in epoch 10, we have to run cron for epoch 9. However, running
610/// 'cron' requires the timestamp of the youngest block in the tipset (which
611/// doesn't exist because there are no blocks in the tipset). Lotus dictates that
612/// the timestamp of a null tipset is `30s * epoch` after the genesis timestamp.
613/// So, in the above example, if the genesis block was mined at time `X`, the
614/// null tipset for epoch 9 will have timestamp `X + 30 * 9`.
615///
616/// # Migrations
617///
618/// Migrations happen between network upgrades and modify the state tree. If a
619/// migration is scheduled for epoch 10, it will be run _after_ the messages for
620/// epoch 10. The tipset for epoch 11 will link the state-tree produced by the
621/// migration.
622///
623/// Example timeline with a migration at epoch 10:
624///   1. Tipset-epoch-10 executes, producing state-tree A.
625///   2. Migration consumes state-tree A and produces state-tree B.
626///   3. Tipset-epoch-11 executes, consuming state-tree B (rather than A).
627///
628/// Note: The migration actually happens when tipset-epoch-11 executes. This is
629///       because tipset-epoch-10 may be null and therefore not executed at all.
630///
631/// # Caching
632///
633/// Scanning the blockchain to find past tipsets and state-trees may be slow.
634/// The `ChainStore` caches recent tipsets to make these scans faster.
635#[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    // This function will:
646    // 1. handle the genesis block as a special case
647    // 2. run 'cron' for any null-tipsets between the current tipset and our parent tipset
648    // 3. run migrations
649    // 4. execute block messages
650    // 5. write the state-tree to the DB and return the CID
651
652    // step 1: special case for genesis block
653    let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
654    if tipset.epoch() == 0 {
655        // NB: This is here because the process that executes blocks requires that the
656        // block miner reference a valid miner in the state tree. Unless we create some
657        // magical genesis miner, this won't work properly, so we short circuit here
658        // This avoids the question of 'who gets paid the genesis block reward'
659        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    // step 2: running cron for any null-tipsets
676    // step 3: run migrations
677    let (parent_state, epoch, block_messages) =
678        exec.prepare_parent_state_blocking(genesis_timestamp, enable_tracing, &mut callback)?;
679
680    // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from
681    // FVM, but that introduces some constraints, and possible deadlocks.
682    stacker::grow(64 << 20, || -> anyhow::Result<ExecutedTipset> {
683        let mut vm = exec.create_vm(parent_state, epoch, tipset.min_timestamp(), enable_tracing)?;
684
685        // step 4: apply tipset messages
686        let (receipts, events, events_roots) =
687            vm.apply_block_messages(&block_messages, epoch, callback)?;
688
689        // step 5: construct receipt root from receipts
690        let receipt_root = Amtv0::new_from_iter(chain_index.db(), receipts.iter())?;
691
692        // step 6: store events AMTs in the blockstore
693        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                // Store the events AMT - the root CID should match the one computed by FVM
698                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                // Verify the stored root matches the FVM-computed root
706                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        // Update executed tipset cache
716        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    /// A `TxMeta` with empty message roots, so `messages_for_tipset` yields zero messages.
816    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    /// A single block with the given epoch, parents, message meta and receipt root. The nonzero
826    /// timestamp lets an epoch-0 block serve as a genesis (which must not be at time 0).
827    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        // `ts` (epoch 1, no messages) and `head` (epoch 2), its child and the chain head, so
856        // `load_child_tipset(ts)` resolves `head`.
857        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        // Cache hit -> Executed.
876        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        // Caller-supplied child -> Stored (assertion holds, receipts read, counts match).
891        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        // `None` resolves the child via `load_child_tipset` -> Stored.
897        assert!(matches!(
898            sm.tipset_message_receipts(&ts, None).await.unwrap(),
899            TipsetMessageReceipts::Stored(..)
900        ));
901
902        // `head` has no child, so `None` resolves to nothing and the loader fallback errors.
903        assert!(sm.tipset_message_receipts(&head, None).await.is_err());
904
905        // Receipt tipset that is not `ts`'s child -> parent-mismatch error.
906        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        // Receipt count != message count -> error.
922        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        // Unreadable receipt root -> falls back to the full loader (re-resolves the on-chain child).
938        // Keep last: this populates `ts`'s cache, which would mask the `Stored` cases above.
939        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}