Skip to main content

forest/interpreter/
vm.rs

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