Skip to main content

forest/state_manager/
execution.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::state_computation::{
5    TipsetExecutor, apply_block_messages_blocking, validate_tipsets_blocking,
6};
7use super::utils::structured;
8use super::*;
9use crate::interpreter::{CalledAt, VMTrace};
10use crate::rpc::state::{ApiInvocResult, MessageGasCost};
11use anyhow::{Context as _, bail};
12use num_traits::identities::Zero;
13use std::ops::RangeInclusive;
14
15impl StateManager {
16    /// Replays the given message and returns the result of executing the
17    /// indicated message, assuming it was executed in the indicated tipset.
18    pub async fn replay(&self, ts: Tipset, mcid: Cid) -> Result<ApiInvocResult, Error> {
19        let this = self.shallow_clone();
20        tokio::task::spawn_blocking(move || this.replay_blocking(ts, mcid)).await?
21    }
22
23    /// Blocking version of `replay`
24    pub fn replay_blocking(&self, ts: Tipset, mcid: Cid) -> Result<ApiInvocResult, Error> {
25        const REPLAY_HALT: &str = "replay_halt";
26
27        let mut api_invoc_result = None;
28        let callback = |ctx: MessageCallbackCtx<'_>| {
29            match ctx.at {
30                CalledAt::Applied | CalledAt::Reward
31                    if api_invoc_result.is_none() && ctx.cid == mcid =>
32                {
33                    api_invoc_result = Some(ApiInvocResult {
34                        msg_cid: ctx.message.cid(),
35                        msg: ctx.message.message().clone(),
36                        msg_rct: Some(ctx.apply_ret.msg_receipt()),
37                        error: ctx.apply_ret.failure_info().unwrap_or_default(),
38                        duration: ctx.duration.as_nanos().clamp(0, u128::from(u64::MAX)) as u64,
39                        gas_cost: MessageGasCost::new(ctx.message.message(), ctx.apply_ret)?,
40                        execution_trace: structured::parse_events(ctx.apply_ret.exec_trace())
41                            .unwrap_or_default(),
42                    });
43                    anyhow::bail!(REPLAY_HALT);
44                }
45                _ => Ok(()), // ignored
46            }
47        };
48        let result = self.compute_tipset_state_blocking(ts, Some(callback), VMTrace::Traced);
49        if let Err(error_message) = result
50            && error_message.to_string() != REPLAY_HALT
51        {
52            return Err(Error::Other(format!(
53                "unexpected error during execution : {error_message:}"
54            )));
55        }
56        api_invoc_result.ok_or_else(|| Error::Other("failed to replay".into()))
57    }
58
59    /// Replays a tipset up to a target message, capturing the state root before
60    /// and after execution.
61    pub async fn replay_for_prestate(
62        &self,
63        ts: Tipset,
64        target_message_cid: Cid,
65    ) -> Result<(Cid, ApiInvocResult, Cid), Error> {
66        let this = self.shallow_clone();
67        tokio::task::spawn_blocking(move || {
68            this.replay_for_prestate_blocking(ts, target_message_cid)
69        })
70        .await
71        .map_err(|e| Error::Other(format!("{e}")))?
72    }
73
74    fn replay_for_prestate_blocking(
75        &self,
76        ts: Tipset,
77        target_msg_cid: Cid,
78    ) -> Result<(Cid, ApiInvocResult, Cid), Error> {
79        if ts.epoch() == 0 {
80            return Err(Error::Other(
81                "cannot trace messages in the genesis block".into(),
82            ));
83        }
84
85        let genesis_timestamp = self.chain_store().genesis_block_header().timestamp;
86        let exec = TipsetExecutor::new(
87            self.chain_index().shallow_clone(),
88            self.chain_config().shallow_clone(),
89            self.beacon_schedule().shallow_clone(),
90            &self.engine,
91            ts.shallow_clone(),
92        );
93        let mut no_cb = NO_CALLBACK;
94        let (parent_state, epoch, block_messages) =
95            exec.prepare_parent_state_blocking(genesis_timestamp, VMTrace::NotTraced, &mut no_cb)?;
96
97        Ok(stacker::grow(64 << 20, || {
98            let mut vm =
99                exec.create_vm(parent_state, epoch, ts.min_timestamp(), VMTrace::NotTraced)?;
100            let mut processed = ahash::HashSet::default();
101
102            for block in block_messages.iter() {
103                let mut penalty = TokenAmount::zero();
104                let mut gas_reward = TokenAmount::zero();
105
106                for msg in block.messages.iter() {
107                    let cid = msg.cid();
108                    if processed.contains(&cid) {
109                        continue;
110                    }
111
112                    processed.insert(cid);
113
114                    if cid == target_msg_cid {
115                        let pre_root = vm.flush()?;
116                        let mut traced_vm =
117                            exec.create_vm(pre_root, epoch, ts.min_timestamp(), VMTrace::Traced)?;
118                        let (ret, duration) = traced_vm.apply_message(msg)?;
119                        let post_root = traced_vm.flush()?;
120
121                        return Ok((
122                            pre_root,
123                            ApiInvocResult {
124                                msg_cid: cid,
125                                msg: msg.message().clone(),
126                                msg_rct: Some(ret.msg_receipt()),
127                                error: ret.failure_info().unwrap_or_default(),
128                                duration: duration.as_nanos().clamp(0, u128::from(u64::MAX)) as u64,
129                                gas_cost: MessageGasCost::default(),
130                                execution_trace: structured::parse_events(ret.exec_trace())
131                                    .unwrap_or_default(),
132                            },
133                            post_root,
134                        ));
135                    }
136
137                    let (ret, _) = vm.apply_message(msg)?;
138                    gas_reward += ret.miner_tip();
139                    penalty += ret.penalty();
140                }
141
142                if let Some(rew_msg) =
143                    vm.reward_message(epoch, block.miner, block.win_count, penalty, gas_reward)?
144                {
145                    let (ret, _) = vm.apply_implicit_message(&rew_msg)?;
146                    if let Some(err) = ret.failure_info() {
147                        bail!(
148                            "failed to apply reward message for miner {}: {err}",
149                            block.miner
150                        );
151                    }
152
153                    // This is more of a sanity check, this should not be able to be hit.
154                    if !ret.msg_receipt().exit_code().is_success() {
155                        bail!(
156                            "reward application message failed (exit: {:?})",
157                            ret.msg_receipt().exit_code()
158                        );
159                    }
160                }
161            }
162
163            bail!("message {target_msg_cid} not found in tipset")
164        })?)
165    }
166
167    /// Validates all tipsets at epoch `start..=end` behind the heaviest tipset.
168    ///
169    /// Tipsets are processed sequentially. The compute-intensive work inside each
170    /// tipset (`bellperson` proof verification, FVM batch seal verification, etc.)
171    /// is already heavily rayon-parallelized. Parallelizing the outer loop actually introduces
172    /// some issues due to locks in the aforementioned crates. So don't do it.
173    ///
174    /// # What is validation?
175    /// Every state transition returns a new _state root_, which is typically retained in, e.g., snapshots.
176    /// For "full" snapshots, all state roots are retained.
177    /// For standard snapshots, the last 2000 or so state roots are retained.
178    ///
179    /// _receipts_ meanwhile, are typically ephemeral, but each tipset knows the _receipt root_
180    /// (hash) of the previous tipset.
181    ///
182    /// This function takes advantage of that fact to validate tipsets:
183    /// - `tipset[N]` claims that `receipt_root[N-1]` should be `0xDEADBEEF`
184    /// - find `tipset[N-1]`, and perform its state transition to get the actual `receipt_root`
185    /// - assert that they match
186    ///
187    /// See [`Self::compute_tipset_state_blocking`] for an explanation of state transitions.
188    #[tracing::instrument(skip(self))]
189    pub fn validate_range_blocking(&self, epochs: RangeInclusive<i64>) -> anyhow::Result<()> {
190        let heaviest = self.heaviest_tipset();
191        let heaviest_epoch = heaviest.epoch();
192        let end = self.chain_index().load_required_tipset_by_height_blocking(
193            *epochs.end(),
194            heaviest,
195            ResolveNullTipset::TakeOlder,
196        ).with_context(|| {
197            format!(
198                "couldn't get a tipset at height {} behind heaviest tipset at height {heaviest_epoch}",
199                *epochs.end(),
200            )})?;
201
202        // lookup tipset parents as we go along, iterating DOWN from `end`
203        let tipsets = end
204            .chain(self.db())
205            .take_while(|ts| ts.epoch() >= *epochs.start());
206
207        self.validate_tipsets_blocking(tipsets)
208    }
209
210    pub fn validate_tipsets_blocking<T>(&self, tipsets: T) -> anyhow::Result<()>
211    where
212        T: Iterator<Item = Tipset> + Send,
213    {
214        validate_tipsets_blocking(
215            self.chain_index(),
216            self.chain_config(),
217            self.beacon_schedule(),
218            &self.engine,
219            tipsets,
220        )
221    }
222
223    pub async fn execution_trace(
224        &self,
225        tipset: &Tipset,
226    ) -> anyhow::Result<(Cid, Vec<Arc<ApiInvocResult>>)> {
227        let key = tipset.key();
228        let (state_root, invoc_trace) = self
229            .trace_cache
230            .get_or_insert_async(key, self.execution_trace_inner(tipset.shallow_clone()))
231            .await?;
232        Ok((state_root.into(), invoc_trace))
233    }
234
235    async fn execution_trace_inner(
236        &self,
237        tipset: Tipset,
238    ) -> anyhow::Result<(CidWrapper, Vec<Arc<ApiInvocResult>>)> {
239        let this = self.shallow_clone();
240        tokio::task::spawn_blocking(move || this.execution_trace_inner_blocking(tipset))
241            .await
242            .context("tokio join error")?
243    }
244
245    fn execution_trace_inner_blocking(
246        &self,
247        tipset: Tipset,
248    ) -> anyhow::Result<(CidWrapper, Vec<Arc<ApiInvocResult>>)> {
249        let mut invoc_trace = vec![];
250
251        let callback = |ctx: MessageCallbackCtx<'_>| {
252            match ctx.at {
253                CalledAt::Applied | CalledAt::Reward => {
254                    invoc_trace.push(Arc::new(ApiInvocResult {
255                        msg_cid: ctx.message.cid(),
256                        msg: ctx.message.message().clone(),
257                        msg_rct: Some(ctx.apply_ret.msg_receipt()),
258                        error: ctx.apply_ret.failure_info().unwrap_or_default(),
259                        duration: ctx.duration.as_nanos().clamp(0, u128::from(u64::MAX)) as u64,
260                        gas_cost: MessageGasCost::new(ctx.message.message(), ctx.apply_ret)?,
261                        execution_trace: structured::parse_events(ctx.apply_ret.exec_trace())
262                            .unwrap_or_default(),
263                    }));
264                    Ok(())
265                }
266                _ => Ok(()), // ignored
267            }
268        };
269
270        let ExecutedTipset { state_root, .. } = apply_block_messages_blocking(
271            self.chain_index().shallow_clone(),
272            self.chain_config().shallow_clone(),
273            self.beacon_schedule().shallow_clone(),
274            &self.engine,
275            tipset,
276            Some(callback),
277            VMTrace::Traced,
278        )?;
279
280        Ok((state_root.into(), invoc_trace))
281    }
282}