Skip to main content

forest/interpreter/
vm.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::sync::Arc;
5
6use crate::blocks::Tipset;
7use crate::chain::block_messages;
8use crate::chain::index::ChainIndex;
9use crate::chain::store::Error;
10use crate::db::EthMappingsStore;
11use crate::interpreter::{
12    fvm2::ForestExternsV2, fvm3::ForestExterns as ForestExternsV3,
13    fvm4::ForestExterns as ForestExternsV4,
14};
15use crate::message::ChainMessage;
16use crate::message::MessageRead as _;
17use crate::networks::{ChainConfig, NetworkChain};
18use crate::shim::actors::{AwardBlockRewardParams, cron, reward};
19use crate::shim::{
20    address::Address,
21    econ::TokenAmount,
22    executor::{ApplyRet, Receipt, StampedEvent},
23    externs::{Rand, RandWrapper},
24    machine::MultiEngine,
25    message::{Message, Message_v3},
26    state_tree::ActorState,
27    version::NetworkVersion,
28};
29use ahash::{HashMap, HashMapExt, HashSet};
30use anyhow::bail;
31use cid::Cid;
32use fvm_ipld_blockstore::Blockstore;
33use fvm_ipld_encoding::{RawBytes, to_vec};
34use fvm_shared2::clock::ChainEpoch;
35use fvm2::{
36    executor::{DefaultExecutor as DefaultExecutor_v2, Executor as Executor_v2},
37    machine::{
38        DefaultMachine as DefaultMachine_v2, Machine as Machine_v2,
39        NetworkConfig as NetworkConfig_v2,
40    },
41};
42use fvm3::{
43    executor::{DefaultExecutor as DefaultExecutor_v3, Executor as Executor_v3},
44    machine::{
45        DefaultMachine as DefaultMachine_v3, Machine as Machine_v3,
46        NetworkConfig as NetworkConfig_v3,
47    },
48};
49use fvm4::{
50    executor::{DefaultExecutor as DefaultExecutor_v4, Executor as Executor_v4},
51    machine::{
52        DefaultMachine as DefaultMachine_v4, Machine as Machine_v4,
53        NetworkConfig as NetworkConfig_v4,
54    },
55};
56use num::Zero;
57use spire_enum::prelude::delegated_enum;
58use std::time::{Duration, Instant};
59
60pub(in crate::interpreter) type ForestMachineV2<DB> =
61    DefaultMachine_v2<Arc<DB>, ForestExternsV2<DB>>;
62pub(in crate::interpreter) type ForestMachineV3<DB> =
63    DefaultMachine_v3<Arc<DB>, ForestExternsV3<DB>>;
64pub(in crate::interpreter) type ForestMachineV4<DB> =
65    DefaultMachine_v4<Arc<DB>, ForestExternsV4<DB>>;
66
67type ForestKernelV2<DB> =
68    fvm2::DefaultKernel<fvm2::call_manager::DefaultCallManager<ForestMachineV2<DB>>>;
69type ForestKernelV3<DB> =
70    fvm3::DefaultKernel<fvm3::call_manager::DefaultCallManager<ForestMachineV3<DB>>>;
71type ForestKernelV4<DB> = fvm4::kernel::filecoin::DefaultFilecoinKernel<
72    fvm4::call_manager::DefaultCallManager<ForestMachineV4<DB>>,
73>;
74
75type ForestExecutorV2<DB> = DefaultExecutor_v2<ForestKernelV2<DB>>;
76type ForestExecutorV3<DB> = DefaultExecutor_v3<ForestKernelV3<DB>>;
77type ForestExecutorV4<DB> = DefaultExecutor_v4<ForestKernelV4<DB>>;
78
79pub type ApplyResult = anyhow::Result<(ApplyRet, Duration)>;
80
81pub type ApplyBlockResult = anyhow::Result<(
82    Vec<Receipt>,
83    Vec<Option<Vec<StampedEvent>>>,
84    Vec<Option<Cid>>,
85)>;
86
87/// Comes from <https://github.com/filecoin-project/lotus/blob/v1.23.2/chain/vm/fvm.go#L473>
88pub const IMPLICIT_MESSAGE_GAS_LIMIT: i64 = i64::MAX / 2;
89
90/// Contains all messages to process through the VM as well as miner information
91/// for block rewards.
92#[derive(Debug)]
93pub struct BlockMessages {
94    pub miner: Address,
95    pub messages: Vec<ChainMessage>,
96    pub win_count: i64,
97}
98
99impl BlockMessages {
100    /// Retrieves block messages to be passed through the VM and removes duplicate messages which appear in multiple blocks.
101    pub fn for_tipset(db: &impl Blockstore, ts: &Tipset) -> Result<Vec<BlockMessages>, Error> {
102        let mut applied = HashMap::new();
103        let mut select_msg = |m: ChainMessage| -> Option<ChainMessage> {
104            // The first match for a sender is guaranteed to have correct nonce
105            // the block isn't valid otherwise.
106            let entry = applied.entry(m.from()).or_insert_with(|| m.sequence());
107
108            if *entry != m.sequence() {
109                return None;
110            }
111
112            *entry += 1;
113            Some(m)
114        };
115
116        ts.block_headers()
117            .iter()
118            .map(|b| {
119                let (usm, sm) = block_messages(db, b)?;
120
121                let mut messages = Vec::with_capacity(usm.len() + sm.len());
122                messages.extend(usm.into_iter().filter_map(|m| select_msg(m.into())));
123                messages.extend(sm.into_iter().filter_map(|m| select_msg(m.into())));
124
125                Ok(BlockMessages {
126                    miner: b.miner_address,
127                    messages,
128                    win_count: b
129                        .election_proof
130                        .as_ref()
131                        .map(|e| e.win_count)
132                        .unwrap_or_default(),
133                })
134            })
135            .collect()
136    }
137}
138
139/// Interpreter which handles execution of state transitioning messages and
140/// returns receipts from the VM execution.
141#[delegated_enum(impl_conversions)]
142pub enum VM<DB: Blockstore + EthMappingsStore + Send + Sync + 'static> {
143    VM2(ForestExecutorV2<DB>),
144    VM3(ForestExecutorV3<DB>),
145    VM4(ForestExecutorV4<DB>),
146}
147
148pub struct ExecutionContext<DB> {
149    // This tipset identifies of the blockchain. It functions as a starting
150    // point when searching for ancestors. It may be any tipset as long as its
151    // epoch is at or higher than the epoch in `epoch`.
152    pub heaviest_tipset: Tipset,
153    // State-tree generated by the parent tipset.
154    pub state_tree_root: Cid,
155    // Epoch of the messages to be executed.
156    pub epoch: ChainEpoch,
157    // Source of deterministic randomness
158    pub rand: Box<dyn Rand>,
159    // https://spec.filecoin.io/systems/filecoin_vm/gas_fee/
160    pub base_fee: TokenAmount,
161    // https://filecoin.io/blog/filecoin-circulating-supply/
162    pub circ_supply: TokenAmount,
163    // The chain config is used to determine which consensus rules to use.
164    pub chain_config: Arc<ChainConfig>,
165    // Caching interface to the DB
166    pub chain_index: ChainIndex<DB>,
167    // UNIX timestamp for epoch
168    pub timestamp: u64,
169}
170
171impl<DB> VM<DB>
172where
173    DB: Blockstore + EthMappingsStore + Send + Sync,
174{
175    pub fn new(
176        ExecutionContext {
177            heaviest_tipset,
178            state_tree_root,
179            epoch,
180            rand,
181            base_fee,
182            circ_supply,
183            chain_config,
184            chain_index,
185            timestamp,
186        }: ExecutionContext<DB>,
187        multi_engine: &MultiEngine,
188        enable_tracing: VMTrace,
189    ) -> anyhow::Result<Self> {
190        let network_version = chain_config.network_version(epoch);
191        if network_version >= NetworkVersion::V21 {
192            let mut config = NetworkConfig_v4::new(network_version.into());
193            // ChainId defines the chain ID used in the Ethereum JSON-RPC endpoint.
194            config.chain_id((chain_config.eth_chain_id).into());
195            if let NetworkChain::Devnet(_) = chain_config.network {
196                config.enable_actor_debugging();
197            }
198
199            let engine = multi_engine.v4.get(&config)?;
200            let mut context = config.for_epoch(epoch, timestamp, state_tree_root);
201            context.set_base_fee(base_fee.into());
202            context.set_circulating_supply(circ_supply.into());
203            context.tracing = enable_tracing.is_traced();
204
205            let fvm: ForestMachineV4<DB> = ForestMachineV4::new(
206                &context,
207                Arc::clone(chain_index.db()),
208                ForestExternsV4::new(
209                    RandWrapper::from(rand),
210                    heaviest_tipset,
211                    epoch,
212                    state_tree_root,
213                    chain_index,
214                    chain_config,
215                ),
216            )?;
217            let exec: ForestExecutorV4<DB> = DefaultExecutor_v4::new(engine, fvm)?;
218            Ok(VM::VM4(exec))
219        } else if network_version >= NetworkVersion::V18 {
220            let mut config = NetworkConfig_v3::new(network_version.into());
221            // ChainId defines the chain ID used in the Ethereum JSON-RPC endpoint.
222            config.chain_id((chain_config.eth_chain_id).into());
223            if let NetworkChain::Devnet(_) = chain_config.network {
224                config.enable_actor_debugging();
225            }
226
227            let engine = multi_engine.v3.get(&config)?;
228            let mut context = config.for_epoch(epoch, timestamp, state_tree_root);
229            context.set_base_fee(base_fee.into());
230            context.set_circulating_supply(circ_supply.into());
231            context.tracing = enable_tracing.is_traced();
232
233            let fvm: ForestMachineV3<DB> = ForestMachineV3::new(
234                &context,
235                Arc::clone(chain_index.db()),
236                ForestExternsV3::new(
237                    RandWrapper::from(rand),
238                    heaviest_tipset,
239                    epoch,
240                    state_tree_root,
241                    chain_index,
242                    chain_config,
243                ),
244            )?;
245            let exec: ForestExecutorV3<DB> = DefaultExecutor_v3::new(engine, fvm)?;
246            Ok(VM::VM3(exec))
247        } else {
248            let config = NetworkConfig_v2::new(network_version.into());
249            let engine = multi_engine.v2.get(&config)?;
250            let mut context = config.for_epoch(epoch, state_tree_root);
251            context.set_base_fee(base_fee.into());
252            context.set_circulating_supply(circ_supply.into());
253            context.tracing = enable_tracing.is_traced();
254
255            let fvm: ForestMachineV2<DB> = ForestMachineV2::new(
256                &engine,
257                &context,
258                Arc::clone(chain_index.db()),
259                ForestExternsV2::new(
260                    RandWrapper::from(rand),
261                    heaviest_tipset,
262                    epoch,
263                    state_tree_root,
264                    chain_index,
265                    chain_config,
266                ),
267            )?;
268            let exec: ForestExecutorV2<DB> = DefaultExecutor_v2::new(fvm);
269            Ok(VM::VM2(exec))
270        }
271    }
272
273    /// Flush stores in VM and return state root.
274    pub fn flush(&mut self) -> anyhow::Result<Cid> {
275        Ok(delegate_vm!(self.flush()?))
276    }
277
278    /// Get actor state from an address. Will be resolved to ID address.
279    pub fn get_actor(&self, addr: &Address) -> anyhow::Result<Option<ActorState>> {
280        match self {
281            VM::VM2(fvm_executor) => Ok(fvm_executor
282                .state_tree()
283                .get_actor(&addr.into())?
284                .map(ActorState::from)),
285            VM::VM3(fvm_executor) => {
286                if let Some(id) = fvm_executor.state_tree().lookup_id(&addr.into())? {
287                    Ok(fvm_executor
288                        .state_tree()
289                        .get_actor(id)?
290                        .map(ActorState::from))
291                } else {
292                    Ok(None)
293                }
294            }
295            VM::VM4(fvm_executor) => {
296                if let Some(id) = fvm_executor.state_tree().lookup_id(&addr.into())? {
297                    Ok(fvm_executor
298                        .state_tree()
299                        .get_actor(id)?
300                        .map(ActorState::from))
301                } else {
302                    Ok(None)
303                }
304            }
305        }
306    }
307
308    pub fn run_cron(
309        &mut self,
310        epoch: ChainEpoch,
311        callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
312    ) -> anyhow::Result<()> {
313        let cron_msg: Message = Message_v3 {
314            from: Address::SYSTEM_ACTOR.into(),
315            to: Address::CRON_ACTOR.into(),
316            // Epoch as sequence is intentional
317            sequence: epoch as u64,
318            // Arbitrarily large gas limit for cron (matching Lotus value)
319            gas_limit: IMPLICIT_MESSAGE_GAS_LIMIT as u64,
320            method_num: cron::Method::EpochTick as u64,
321            params: Default::default(),
322            value: Default::default(),
323            version: Default::default(),
324            gas_fee_cap: Default::default(),
325            gas_premium: Default::default(),
326        }
327        .into();
328
329        let (ret, duration) = self.apply_implicit_message(&cron_msg)?;
330        if let Some(err) = ret.failure_info() {
331            anyhow::bail!("failed to apply block cron message: {}", err);
332        }
333
334        if let Some(mut callback) = callback {
335            callback(MessageCallbackCtx {
336                cid: cron_msg.cid(),
337                message: &cron_msg.into(),
338                apply_ret: &ret,
339                at: CalledAt::Cron,
340                duration,
341            })?;
342        }
343        Ok(())
344    }
345
346    /// Apply block messages from a Tipset.
347    /// Returns the receipts from the transactions.
348    pub fn apply_block_messages(
349        &mut self,
350        messages: &[BlockMessages],
351        epoch: ChainEpoch,
352        mut callback: Option<impl FnMut(MessageCallbackCtx<'_>) -> anyhow::Result<()>>,
353    ) -> ApplyBlockResult {
354        let mut receipts = Vec::new();
355        let mut events = Vec::new();
356        let mut events_roots: Vec<Option<Cid>> = Vec::new();
357        let mut processed = HashSet::default();
358
359        for block in messages.iter() {
360            let mut penalty = TokenAmount::zero();
361            let mut gas_reward = TokenAmount::zero();
362
363            let mut process_msg = |message: &ChainMessage| -> anyhow::Result<()> {
364                let cid = message.cid();
365                // Ensure no duplicate processing of a message
366                if processed.contains(&cid) {
367                    return Ok(());
368                }
369                let (ret, duration) = self.apply_message(message)?;
370
371                if let Some(cb) = &mut callback {
372                    cb(MessageCallbackCtx {
373                        cid,
374                        message,
375                        apply_ret: &ret,
376                        at: CalledAt::Applied,
377                        duration,
378                    })?;
379                }
380
381                // Update totals
382                gas_reward += ret.miner_tip();
383                penalty += ret.penalty();
384                let msg_receipt = ret.msg_receipt();
385                receipts.push(msg_receipt.clone());
386
387                events_roots.push(ret.msg_receipt().events_root());
388                if ret.msg_receipt().events_root().is_some() {
389                    events.push(Some(ret.events()));
390                } else {
391                    events.push(None);
392                }
393
394                // Add processed Cid to set of processed messages
395                processed.insert(cid);
396                Ok(())
397            };
398
399            for msg in block.messages.iter() {
400                process_msg(msg)?;
401            }
402
403            // Generate reward transaction for the miner of the block
404            if let Some(rew_msg) =
405                self.reward_message(epoch, block.miner, block.win_count, penalty, gas_reward)?
406            {
407                let (ret, duration) = self.apply_implicit_message(&rew_msg)?;
408                if let Some(err) = ret.failure_info() {
409                    anyhow::bail!(
410                        "failed to apply reward message for miner {}: {}",
411                        block.miner,
412                        err
413                    );
414                }
415                // This is more of a sanity check, this should not be able to be hit.
416                if !ret.msg_receipt().exit_code().is_success() {
417                    anyhow::bail!(
418                        "reward application message failed (exit: {:?})",
419                        ret.msg_receipt().exit_code()
420                    );
421                }
422
423                if let Some(callback) = &mut callback {
424                    callback(MessageCallbackCtx {
425                        cid: rew_msg.cid(),
426                        message: &rew_msg.into(),
427                        apply_ret: &ret,
428                        at: CalledAt::Reward,
429                        duration,
430                    })?
431                }
432            }
433        }
434
435        if let Err(e) = self.run_cron(epoch, callback.as_mut()) {
436            tracing::error!("End of epoch cron failed to run: {}", e);
437        }
438
439        Ok((receipts, events, events_roots))
440    }
441
442    /// Applies single message through VM and returns result from execution.
443    pub fn apply_implicit_message(&mut self, msg: &Message) -> ApplyResult {
444        let start = Instant::now();
445
446        // raw_length is not used for Implicit messages.
447        let raw_length = to_vec(msg).expect("encoding error").len();
448
449        let ret = match self {
450            VM::VM2(fvm_executor) => fvm_executor
451                .execute_message(msg.into(), fvm2::executor::ApplyKind::Implicit, raw_length)?
452                .into(),
453            VM::VM3(fvm_executor) => fvm_executor
454                .execute_message(msg.into(), fvm3::executor::ApplyKind::Implicit, raw_length)?
455                .into(),
456            VM::VM4(fvm_executor) => fvm_executor
457                .execute_message(msg.into(), fvm4::executor::ApplyKind::Implicit, raw_length)?
458                .into(),
459        };
460        Ok((ret, start.elapsed()))
461    }
462
463    /// Applies the state transition for a single message.
464    /// Returns `ApplyRet` structure which contains the message receipt and some
465    /// meta data.
466    pub fn apply_message(&mut self, msg: &ChainMessage) -> ApplyResult {
467        let start = Instant::now();
468
469        // Basic validity check
470        msg.message().check()?;
471
472        let unsigned = msg.message().clone();
473        let raw_length = to_vec(msg).expect("encoding error").len();
474        let ret: ApplyRet = match self {
475            VM::VM2(fvm_executor) => {
476                let ret = fvm_executor.execute_message(
477                    unsigned.into(),
478                    fvm2::executor::ApplyKind::Explicit,
479                    raw_length,
480                )?;
481
482                if fvm_executor.externs().bail() {
483                    bail!("encountered a database lookup error");
484                }
485
486                ret.into()
487            }
488            VM::VM3(fvm_executor) => {
489                let ret = fvm_executor.execute_message(
490                    unsigned.into(),
491                    fvm3::executor::ApplyKind::Explicit,
492                    raw_length,
493                )?;
494
495                if fvm_executor.externs().bail() {
496                    bail!("encountered a database lookup error");
497                }
498
499                ret.into()
500            }
501            VM::VM4(fvm_executor) => {
502                let ret = fvm_executor.execute_message(
503                    unsigned.into(),
504                    fvm4::executor::ApplyKind::Explicit,
505                    raw_length,
506                )?;
507
508                if fvm_executor.externs().bail() {
509                    bail!("encountered a database lookup error");
510                }
511
512                ret.into()
513            }
514        };
515        let duration = start.elapsed();
516
517        let exit_code = ret.msg_receipt().exit_code();
518
519        if !exit_code.is_success() {
520            tracing::debug!(?exit_code, "VM message execution failure.")
521        }
522
523        Ok((ret, duration))
524    }
525
526    pub(crate) fn reward_message(
527        &self,
528        epoch: ChainEpoch,
529        miner: Address,
530        win_count: i64,
531        penalty: TokenAmount,
532        gas_reward: TokenAmount,
533    ) -> anyhow::Result<Option<Message>> {
534        let params = RawBytes::serialize(AwardBlockRewardParams {
535            miner: miner.into(),
536            penalty: penalty.into(),
537            gas_reward: gas_reward.into(),
538            win_count,
539        })?;
540        let rew_msg = Message_v3 {
541            from: Address::SYSTEM_ACTOR.into(),
542            to: Address::REWARD_ACTOR.into(),
543            method_num: reward::Method::AwardBlockReward as u64,
544            params,
545            // Epoch as sequence is intentional
546            sequence: epoch as u64,
547            gas_limit: IMPLICIT_MESSAGE_GAS_LIMIT as u64,
548            value: Default::default(),
549            version: Default::default(),
550            gas_fee_cap: Default::default(),
551            gas_premium: Default::default(),
552        };
553        Ok(Some(rew_msg.into()))
554    }
555}
556
557#[derive(Debug, Clone)]
558pub struct MessageCallbackCtx<'a> {
559    pub cid: Cid,
560    pub message: &'a ChainMessage,
561    pub apply_ret: &'a ApplyRet,
562    pub at: CalledAt,
563    pub duration: Duration,
564}
565
566#[derive(Debug, Clone, Copy)]
567pub enum CalledAt {
568    Applied,
569    Reward,
570    Cron,
571}
572
573impl CalledAt {
574    /// Was [`VM::apply_message`] or [`VM::apply_implicit_message`] called?
575    pub fn apply_kind(&self) -> fvm3::executor::ApplyKind {
576        use fvm3::executor::ApplyKind;
577        match self {
578            CalledAt::Applied => ApplyKind::Explicit,
579            CalledAt::Reward | CalledAt::Cron => ApplyKind::Implicit,
580        }
581    }
582}
583
584/// Tracing a Filecoin VM has a performance penalty.
585/// This controls whether a VM should be traced or not when it is created.
586#[derive(Default, Clone, Copy)]
587pub enum VMTrace {
588    /// Collect trace for the given operation
589    Traced,
590    /// Do not collect trace
591    #[default]
592    NotTraced,
593}
594
595impl VMTrace {
596    /// Should tracing be collected?
597    pub fn is_traced(&self) -> bool {
598        matches!(self, VMTrace::Traced)
599    }
600}