Skip to main content

forest/state_manager/
message_simulation.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::circulating_supply::GenesisInfo;
5use super::utils::structured;
6use super::*;
7use crate::db::EthMappingsStore;
8use crate::interpreter::{ExecutionContext, IMPLICIT_MESSAGE_GAS_LIMIT, VM, VMTrace};
9use crate::message::{MessageRead as _, MessageReadWrite as _, SignedMessage};
10use crate::rpc::state::{ApiInvocResult, InvocResult, MessageGasCost};
11use crate::shim::address::Protocol;
12use crate::shim::crypto::{Signature, SignatureType};
13use crate::shim::executor::ApplyRet;
14use crate::shim::message::Message;
15use crate::utils::ShallowClone as _;
16use fvm_shared4::crypto::signature::SECP_SIG_LEN;
17use std::time::Duration;
18use tracing::instrument;
19
20impl<DB> StateManager<DB>
21where
22    DB: Blockstore + Send + Sync + 'static,
23{
24    #[instrument(skip(self, rand))]
25    fn call_raw(
26        &self,
27        state_cid: Option<Cid>,
28        msg: &Message,
29        rand: ChainRand<DB>,
30        tipset: &Tipset,
31    ) -> Result<ApiInvocResult, Error>
32    where
33        DB: EthMappingsStore,
34    {
35        let mut msg = msg.clone();
36
37        let state_cid = state_cid.unwrap_or(*tipset.parent_state());
38
39        let tipset_messages = self
40            .chain_store()
41            .messages_for_tipset(tipset)
42            .map_err(|err| Error::Other(err.to_string()))?;
43
44        let prior_messsages = tipset_messages
45            .iter()
46            .filter(|ts_msg| ts_msg.message().from() == msg.from());
47
48        // Handle state forks
49
50        let height = tipset.epoch();
51        let genesis_info = GenesisInfo::from_chain_config(self.chain_config().clone());
52        let mut vm = VM::new(
53            ExecutionContext {
54                heaviest_tipset: tipset.shallow_clone(),
55                state_tree_root: state_cid,
56                epoch: height,
57                rand: Box::new(rand),
58                base_fee: tipset.block_headers().first().parent_base_fee.clone(),
59                circ_supply: genesis_info.get_vm_circulating_supply(
60                    height,
61                    self.blockstore(),
62                    &state_cid,
63                )?,
64                chain_config: self.chain_config().shallow_clone(),
65                chain_index: self.chain_index().shallow_clone(),
66                timestamp: tipset.min_timestamp(),
67            },
68            &self.engine,
69            VMTrace::Traced,
70        )?;
71
72        for m in prior_messsages {
73            vm.apply_message(m)?;
74        }
75
76        // We flush to get the VM's view of the state tree after applying the above messages
77        // This is needed to get the correct nonce from the actor state to match the VM
78        let state_cid = vm.flush()?;
79
80        let state = StateTree::new_from_root(self.blockstore_owned(), &state_cid)?;
81
82        let from_actor = state
83            .get_actor(&msg.from())?
84            .ok_or_else(|| anyhow::anyhow!("actor not found"))?;
85        msg.set_sequence(from_actor.sequence);
86
87        // Implicit messages need to set a special gas limit
88        let mut msg = msg.clone();
89        msg.gas_limit = IMPLICIT_MESSAGE_GAS_LIMIT as u64;
90
91        let (apply_ret, duration) = vm.apply_implicit_message(&msg)?;
92
93        Ok(ApiInvocResult {
94            msg: msg.clone(),
95            msg_rct: Some(apply_ret.msg_receipt()),
96            msg_cid: msg.cid(),
97            error: apply_ret.failure_info().unwrap_or_default(),
98            duration: duration.as_nanos().clamp(0, u128::from(u64::MAX)) as u64,
99            gas_cost: MessageGasCost::default(),
100            execution_trace: structured::parse_events(apply_ret.exec_trace()).unwrap_or_default(),
101        })
102    }
103
104    /// runs the given message and returns its result without any persisted
105    /// changes.
106    pub fn call(&self, message: &Message, tipset: Option<Tipset>) -> Result<ApiInvocResult, Error>
107    where
108        DB: EthMappingsStore,
109    {
110        let ts = tipset.unwrap_or_else(|| self.heaviest_tipset());
111        let chain_rand = self.chain_rand(ts.shallow_clone());
112        self.call_raw(None, message, chain_rand, &ts)
113    }
114
115    /// Same as [`StateManager::call`] but runs the message on the given state and not
116    /// on the parent state of the tipset.
117    pub fn call_on_state(
118        &self,
119        state_cid: Cid,
120        message: &Message,
121        tipset: Option<Tipset>,
122    ) -> Result<ApiInvocResult, Error>
123    where
124        DB: EthMappingsStore,
125    {
126        let ts = tipset.unwrap_or_else(|| self.cs.heaviest_tipset());
127        let chain_rand = self.chain_rand(ts.shallow_clone());
128        self.call_raw(Some(state_cid), message, chain_rand, &ts)
129    }
130
131    pub async fn apply_on_state_with_gas(
132        self: &Arc<Self>,
133        tipset: Option<Tipset>,
134        msg: Message,
135        vm_flush: VMFlush,
136    ) -> anyhow::Result<(ApiInvocResult, Option<Cid>)>
137    where
138        DB: EthMappingsStore,
139    {
140        let ts = tipset.unwrap_or_else(|| self.heaviest_tipset());
141
142        let from_a = self.resolve_to_key_addr(&msg.from, &ts).await?;
143
144        // Pretend that the message is signed. This has an influence on the gas
145        // cost. We obviously can't generate a valid signature. Instead, we just
146        // fill the signature with zeros. The validity is not checked.
147        let mut chain_msg = match from_a.protocol() {
148            Protocol::Secp256k1 => SignedMessage::new_unchecked(
149                msg.clone(),
150                Signature::new_secp256k1(vec![0; SECP_SIG_LEN]),
151            )
152            .into(),
153            Protocol::Delegated => SignedMessage::new_unchecked(
154                msg.clone(),
155                // In Lotus, delegated signatures have the same length as SECP256k1.
156                // This may or may not change in the future.
157                Signature::new(SignatureType::Delegated, vec![0; SECP_SIG_LEN]),
158            )
159            .into(),
160            _ => msg.clone().into(),
161        };
162
163        let (_invoc_res, apply_ret, duration, state_root) = self
164            .call_with_gas(&mut chain_msg, &[], Some(ts), vm_flush)
165            .await?;
166
167        Ok((
168            ApiInvocResult {
169                msg_cid: msg.cid(),
170                msg,
171                msg_rct: Some(apply_ret.msg_receipt()),
172                error: apply_ret.failure_info().unwrap_or_default(),
173                duration: duration.as_nanos().clamp(0, u128::from(u64::MAX)) as u64,
174                gas_cost: MessageGasCost::default(),
175                execution_trace: structured::parse_events(apply_ret.exec_trace())
176                    .unwrap_or_default(),
177            },
178            state_root,
179        ))
180    }
181
182    /// Computes message on the given [Tipset] state, after applying other
183    /// messages and returns the values computed in the VM.
184    pub async fn call_with_gas(
185        self: &Arc<Self>,
186        message: &mut ChainMessage,
187        prior_messages: &[ChainMessage],
188        tipset: Option<Tipset>,
189        vm_flush: VMFlush,
190    ) -> Result<(InvocResult, ApplyRet, Duration, Option<Cid>), Error>
191    where
192        DB: EthMappingsStore,
193    {
194        let ts = tipset.unwrap_or_else(|| self.heaviest_tipset());
195        let TipsetState { state_root, .. } = self
196            .load_tipset_state(&ts)
197            .await
198            .map_err(|e| Error::Other(format!("Could not load tipset state: {e:#}")))?;
199        let chain_rand = self.chain_rand(ts.clone());
200
201        // Since we're simulating a future message, pretend we're applying it in the
202        // "next" tipset
203        let epoch = ts.epoch() + 1;
204        let genesis_info = GenesisInfo::from_chain_config(self.chain_config().clone());
205        // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from
206        // FVM, but that introduces some constraints, and possible deadlocks.
207        let (ret, duration, state_cid) = stacker::grow(64 << 20, || -> anyhow::Result<_> {
208            let mut vm = VM::new(
209                ExecutionContext {
210                    heaviest_tipset: ts.clone(),
211                    state_tree_root: state_root,
212                    epoch,
213                    rand: Box::new(chain_rand),
214                    base_fee: ts.block_headers().first().parent_base_fee.clone(),
215                    circ_supply: genesis_info.get_vm_circulating_supply(
216                        epoch,
217                        self.blockstore(),
218                        &state_root,
219                    )?,
220                    chain_config: self.chain_config().shallow_clone(),
221                    chain_index: self.chain_index().shallow_clone(),
222                    timestamp: ts.min_timestamp(),
223                },
224                &self.engine,
225                VMTrace::NotTraced,
226            )?;
227
228            for msg in prior_messages {
229                vm.apply_message(msg)?;
230            }
231            let from_actor = vm
232                .get_actor(&message.from())
233                .map_err(|e| Error::Other(format!("Could not get actor from state: {e:#}")))?
234                .ok_or_else(|| Error::Other("cant find actor in state tree".to_string()))?;
235
236            message.set_sequence(from_actor.sequence);
237            let (ret, duration) = vm.apply_message(message)?;
238            let state_root = match vm_flush {
239                VMFlush::Flush => Some(vm.flush()?),
240                VMFlush::Skip => None,
241            };
242            Ok((ret, duration, state_root))
243        })?;
244
245        Ok((
246            InvocResult::new(message.message().clone(), &ret),
247            ret,
248            duration,
249            state_cid,
250        ))
251    }
252}