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