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