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