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