Skip to main content

forest/shim/
executor.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::trace::ExecutionEvent;
5use crate::shim::{
6    econ::TokenAmount, fvm_shared_latest::ActorID, fvm_shared_latest::error::ExitCode,
7};
8use crate::utils::get_size::{GetSize, vec_heap_size_with_fn_helper};
9use cid::Cid;
10use fil_actors_shared::fvm_ipld_amt::{Amt, Amtv0};
11use fvm_ipld_blockstore::Blockstore;
12use fvm_ipld_encoding::RawBytes;
13use fvm_shared2::receipt::Receipt as Receipt_v2;
14use fvm_shared3::event::ActorEvent as ActorEvent_v3;
15use fvm_shared3::event::Entry as Entry_v3;
16use fvm_shared3::event::StampedEvent as StampedEvent_v3;
17pub use fvm_shared3::receipt::Receipt as Receipt_v3;
18use fvm_shared4::event::ActorEvent as ActorEvent_v4;
19use fvm_shared4::event::Entry as Entry_v4;
20use fvm_shared4::event::StampedEvent as StampedEvent_v4;
21use fvm_shared4::receipt::Receipt as Receipt_v4;
22use fvm2::executor::ApplyRet as ApplyRet_v2;
23use fvm3::executor::ApplyRet as ApplyRet_v3;
24use fvm4::executor::ApplyRet as ApplyRet_v4;
25use serde::Serialize;
26use spire_enum::prelude::delegated_enum;
27use std::borrow::Borrow as _;
28
29#[delegated_enum(impl_conversions)]
30#[derive(Clone, Debug)]
31pub enum ApplyRet {
32    V2(ApplyRet_v2),
33    V3(ApplyRet_v3),
34    V4(ApplyRet_v4),
35}
36
37impl ApplyRet {
38    pub fn failure_info(&self) -> Option<String> {
39        delegate_apply_ret!(self => |r| r.failure_info.as_ref().map(|failure| format!("{failure} (RetCode={})", self.exit_code())))
40    }
41
42    pub fn miner_tip(&self) -> TokenAmount {
43        delegate_apply_ret!(self.miner_tip.borrow().into())
44    }
45
46    pub fn penalty(&self) -> TokenAmount {
47        delegate_apply_ret!(self.penalty.borrow().into())
48    }
49
50    /// Clones the receipt, `return_data` included. To read a single scalar, prefer
51    /// [`Self::exit_code`] or [`Self::gas_used`].
52    pub fn msg_receipt(&self) -> Receipt {
53        delegate_apply_ret!(self.msg_receipt.clone().into())
54    }
55
56    /// The message's return payload, cloned without cloning the rest of the receipt.
57    pub fn return_data(&self) -> RawBytes {
58        delegate_apply_ret!(self => |r| r.msg_receipt.return_data.clone())
59    }
60
61    pub fn exit_code(&self) -> ExitCode {
62        ExitCode::new(delegate_apply_ret!(self => |r| r.msg_receipt.exit_code.value()))
63    }
64
65    pub fn gas_used(&self) -> u64 {
66        match self {
67            ApplyRet::V2(v2) => v2.msg_receipt.gas_used as u64,
68            ApplyRet::V3(v3) => v3.msg_receipt.gas_used,
69            ApplyRet::V4(v4) => v4.msg_receipt.gas_used,
70        }
71    }
72
73    pub fn refund(&self) -> TokenAmount {
74        delegate_apply_ret!(self.refund.borrow().into())
75    }
76
77    pub fn base_fee_burn(&self) -> TokenAmount {
78        delegate_apply_ret!(self.base_fee_burn.borrow().into())
79    }
80
81    pub fn over_estimation_burn(&self) -> TokenAmount {
82        delegate_apply_ret!(self.over_estimation_burn.borrow().into())
83    }
84
85    /// Whether any traced `CallReturn` carries `code`, without materializing the trace.
86    /// Mirrors Lotus's `traceContainsExitCode`. FVM2 `CallReturn`s carry no exit code, so they
87    /// never match.
88    pub fn trace_has_call_return_exit_code(&self, code: ExitCode) -> bool {
89        let want = code.value();
90        match self {
91            ApplyRet::V2(_) => false,
92            ApplyRet::V3(r) => r.exec_trace.iter().any(
93                |e| matches!(e, fvm3::trace::ExecutionEvent::CallReturn(c, _) if c.value() == want),
94            ),
95            ApplyRet::V4(r) => r.exec_trace.iter().any(
96                |e| matches!(e, fvm4::trace::ExecutionEvent::CallReturn(c, _) if c.value() == want),
97            ),
98        }
99    }
100
101    /// Consuming variant of [`Self::exec_trace`].
102    pub fn into_exec_trace(self) -> Vec<ExecutionEvent> {
103        delegate_apply_ret!(self => |r| r.exec_trace.into_iter().map(Into::into).collect())
104    }
105
106    pub fn exec_trace(&self) -> Vec<ExecutionEvent> {
107        delegate_apply_ret!(self => |r| r.exec_trace.iter().cloned().map(Into::into).collect())
108    }
109
110    pub fn into_receipt_and_events(self) -> (Receipt, Option<Vec<StampedEvent>>) {
111        match self {
112            ApplyRet::V2(v2) => (v2.msg_receipt.into(), None),
113            ApplyRet::V3(v3) => {
114                let events = v3
115                    .msg_receipt
116                    .events_root
117                    .is_some()
118                    .then(|| v3.events.into_iter().map(Into::into).collect());
119                (v3.msg_receipt.into(), events)
120            }
121            ApplyRet::V4(v4) => {
122                let events = v4
123                    .msg_receipt
124                    .events_root
125                    .is_some()
126                    .then(|| v4.events.into_iter().map(Into::into).collect());
127                (v4.msg_receipt.into(), events)
128            }
129        }
130    }
131}
132
133// Note: it's impossible to properly derive Deserialize.
134// To deserialize into `Receipt`, refer to `fn get_parent_receipt`
135#[delegated_enum(impl_conversions)]
136#[derive(Clone, Debug, Serialize)]
137#[serde(untagged)]
138pub enum Receipt {
139    V2(Receipt_v2),
140    V3(Receipt_v3),
141    V4(Receipt_v4),
142}
143
144impl GetSize for Receipt {
145    fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
146        delegate_receipt!(self.return_data.bytes().get_heap_size_with_tracker(tracker))
147    }
148}
149
150impl PartialEq for Receipt {
151    fn eq(&self, other: &Self) -> bool {
152        self.exit_code() == other.exit_code()
153            && self.return_data() == other.return_data()
154            && self.gas_used() == other.gas_used()
155            && self.events_root() == other.events_root()
156    }
157}
158
159impl Receipt {
160    /// Test-only constructor: a successful, empty receipt at the latest supported FVM version.
161    /// Lets tests build receipts without naming a concrete FVM version.
162    #[cfg(test)]
163    pub fn empty_success() -> Self {
164        Receipt::V4(Receipt_v4 {
165            exit_code: fvm_shared4::error::ExitCode::OK,
166            return_data: Default::default(),
167            gas_used: 0,
168            events_root: None,
169        })
170    }
171
172    pub fn exit_code(&self) -> ExitCode {
173        match self {
174            Receipt::V2(v2) => ExitCode::new(v2.exit_code.value()),
175            Receipt::V3(v3) => ExitCode::new(v3.exit_code.value()),
176            Receipt::V4(v4) => v4.exit_code,
177        }
178    }
179
180    pub fn return_data(&self) -> RawBytes {
181        delegate_receipt!(self.return_data.clone())
182    }
183
184    pub fn gas_used(&self) -> u64 {
185        match self {
186            Receipt::V2(v2) => v2.gas_used as u64,
187            Receipt::V3(v3) => v3.gas_used,
188            Receipt::V4(v4) => v4.gas_used,
189        }
190    }
191    pub fn events_root(&self) -> Option<Cid> {
192        match self {
193            Receipt::V2(_) => None,
194            Receipt::V3(v3) => v3.events_root,
195            Receipt::V4(v4) => v4.events_root,
196        }
197    }
198
199    pub fn get_receipt(
200        db: &impl Blockstore,
201        receipts: &Cid,
202        i: u64,
203    ) -> anyhow::Result<Option<Self>> {
204        // Try Receipt_v4 first. (Receipt_v4 and Receipt_v3 are identical, use v4 here)
205        if let Ok(amt) = Amtv0::load(receipts, db)
206            && let Ok(receipts) = amt.get(i)
207        {
208            return Ok(receipts.cloned().map(Receipt::V4));
209        }
210
211        // Fallback to Receipt_v2.
212        let amt = Amtv0::load(receipts, db)?;
213        let receipts = amt.get(i)?;
214        Ok(receipts.cloned().map(Receipt::V2))
215    }
216
217    pub fn get_receipts(db: &impl Blockstore, receipts_cid: Cid) -> anyhow::Result<Vec<Receipt>> {
218        let mut receipts = Vec::new();
219
220        // Try Receipt_v4 first. (Receipt_v4 and Receipt_v3 are identical, use v4 here)
221        if let Ok(amt) = Amtv0::<fvm_shared4::receipt::Receipt, _>::load(&receipts_cid, db) {
222            amt.for_each_cacheless(|_, receipt| {
223                receipts.push(Receipt::V4(receipt.clone()));
224                Ok(())
225            })?;
226        } else {
227            // Fallback to Receipt_v2.
228            let amt = Amtv0::<fvm_shared2::receipt::Receipt, _>::load(&receipts_cid, db)?;
229            amt.for_each_cacheless(|_, receipt| {
230                receipts.push(Receipt::V2(receipt.clone()));
231                Ok(())
232            })?;
233        }
234
235        Ok(receipts)
236    }
237}
238
239#[delegated_enum(impl_conversions)]
240#[derive(Clone, Debug)]
241pub enum Entry {
242    V3(Entry_v3),
243    V4(Entry_v4),
244}
245
246impl Entry {
247    #[cfg(test)]
248    pub fn new(
249        flags: crate::shim::fvm_shared_latest::event::Flags,
250        key: String,
251        codec: u64,
252        value: Vec<u8>,
253    ) -> Self {
254        Entry::V4(Entry_v4 {
255            flags,
256            key,
257            codec,
258            value,
259        })
260    }
261
262    pub fn into_parts(self) -> (u64, String, u64, Vec<u8>) {
263        delegate_entry!(self => |e| (e.flags.bits(), e.key, e.codec, e.value))
264    }
265
266    pub fn value(&self) -> &Vec<u8> {
267        delegate_entry!(self.value.borrow())
268    }
269
270    pub fn codec(&self) -> u64 {
271        delegate_entry!(self.codec)
272    }
273
274    pub fn key(&self) -> &String {
275        delegate_entry!(self.key.borrow())
276    }
277}
278
279#[delegated_enum(impl_conversions)]
280#[derive(Clone, Debug)]
281pub enum ActorEvent {
282    V3(ActorEvent_v3),
283    V4(ActorEvent_v4),
284}
285
286/// Event with extra information stamped by the FVM.
287#[delegated_enum(impl_conversions)]
288#[derive(Clone, Debug, Serialize)]
289#[serde(untagged)]
290pub enum StampedEvent {
291    V3(StampedEvent_v3),
292    V4(StampedEvent_v4),
293}
294
295impl GetSize for StampedEvent {
296    fn get_heap_size_with_tracker<T: get_size2::GetSizeTracker>(&self, tracker: T) -> (usize, T) {
297        delegate_stamped_event!(self => |e| vec_heap_size_with_fn_helper(&e.event.entries, tracker, |e, mut tr| {
298            (e.key.get_heap_size_with_tracker(&mut tr).0 + e.value.get_heap_size_with_tracker(&mut tr).0, tr)
299        }))
300    }
301}
302
303impl StampedEvent {
304    /// Test-only constructor: a stamped event at the latest supported FVM version with a single
305    /// `FLAG_INDEXED_ALL` entry whose key and value are `key`. Lets tests build events without
306    /// naming a concrete FVM version.
307    #[cfg(test)]
308    pub fn new_indexed(emitter: ActorID, key: &str) -> Self {
309        Self::V4(create_raw_event_v4(emitter, key))
310    }
311
312    /// Returns the ID of the actor that emitted this event.
313    pub fn emitter(&self) -> ActorID {
314        delegate_stamped_event!(self.emitter)
315    }
316
317    pub fn entries(&self) -> Vec<Entry> {
318        delegate_stamped_event!(self => |e| e.event.entries.iter().cloned().map(Into::into).collect())
319    }
320
321    pub fn into_entries(self) -> Vec<Entry> {
322        match self {
323            StampedEvent::V3(e) => e.event.entries.into_iter().map(Into::into).collect(),
324            StampedEvent::V4(e) => e.event.entries.into_iter().map(Into::into).collect(),
325        }
326    }
327
328    /// Loads events directly from the events AMT root CID.
329    /// Returns events in the exact order they are stored in the AMT.
330    pub fn get_events<DB: Blockstore>(
331        db: &DB,
332        events_root: &Cid,
333    ) -> anyhow::Result<Vec<StampedEvent>> {
334        let mut events = Vec::new();
335
336        // Try StampedEvent_v4 first (StampedEvent_v4 and StampedEvent_v3 are identical, use v4 here)
337        if let Ok(amt) = Amt::<StampedEvent_v4, _>::load(events_root, db) {
338            amt.for_each_cacheless(|_, event| {
339                events.push(StampedEvent::V4(event.clone()));
340                Ok(())
341            })?;
342        } else {
343            // Fallback to StampedEvent_v3
344            let amt = Amt::<StampedEvent_v3, _>::load(events_root, db)?;
345            amt.for_each_cacheless(|_, event| {
346                events.push(StampedEvent::V3(event.clone()));
347                Ok(())
348            })?;
349        }
350
351        Ok(events)
352    }
353}
354
355/// Builds a raw FVM4 stamped event with a single indexed entry whose key and value are `key`.
356/// Wrap in [`StampedEvent::V4`] for the shim-level type.
357#[cfg(test)]
358pub(crate) fn create_raw_event_v4(emitter: u64, key: &str) -> fvm_shared4::event::StampedEvent {
359    fvm_shared4::event::StampedEvent {
360        emitter,
361        event: fvm_shared4::event::ActorEvent {
362            entries: vec![fvm_shared4::event::Entry {
363                flags: fvm_shared4::event::Flags::FLAG_INDEXED_ALL,
364                key: key.to_string(),
365                codec: fvm_ipld_encoding::IPLD_RAW,
366                value: key.as_bytes().to_vec(),
367            }],
368        },
369    }
370}
371
372/// Builds a raw FVM3 stamped event with a single indexed entry whose key and value are `key`.
373/// Wrap in [`StampedEvent::V3`] for the shim-level type.
374#[cfg(test)]
375pub(crate) fn create_raw_event_v3(emitter: u64, key: &str) -> fvm_shared3::event::StampedEvent {
376    fvm_shared3::event::StampedEvent {
377        emitter,
378        event: fvm_shared3::event::ActorEvent {
379            entries: vec![fvm_shared3::event::Entry {
380                flags: fvm_shared3::event::Flags::FLAG_INDEXED_ALL,
381                key: key.to_string(),
382                codec: fvm_ipld_encoding::IPLD_RAW,
383                value: key.as_bytes().to_vec(),
384            }],
385        },
386    }
387}
388
389#[cfg(test)]
390impl quickcheck::Arbitrary for Receipt {
391    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
392        #[derive(derive_quickcheck_arbitrary::Arbitrary, Clone)]
393        enum Helper {
394            V2 {
395                exit_code: u32,
396                return_data: Vec<u8>,
397                gas_used: i64,
398            },
399            V3 {
400                exit_code: u32,
401                return_data: Vec<u8>,
402                gas_used: u64,
403                events_root: Option<::cid::Cid>,
404            },
405            V4 {
406                exit_code: u32,
407                return_data: Vec<u8>,
408                gas_used: u64,
409                events_root: Option<::cid::Cid>,
410            },
411        }
412        match Helper::arbitrary(g) {
413            Helper::V2 {
414                exit_code,
415                return_data,
416                gas_used,
417            } => Self::V2(Receipt_v2 {
418                exit_code: exit_code.into(),
419                return_data: return_data.into(),
420                gas_used,
421            }),
422            Helper::V3 {
423                exit_code,
424                return_data,
425                gas_used,
426                events_root,
427            } => Self::V3(Receipt_v3 {
428                exit_code: exit_code.into(),
429                return_data: return_data.into(),
430                gas_used,
431                events_root,
432            }),
433            Helper::V4 {
434                exit_code,
435                return_data,
436                gas_used,
437                events_root,
438            } => Self::V4(Receipt_v4 {
439                exit_code: exit_code.into(),
440                return_data: return_data.into(),
441                gas_used,
442                events_root,
443            }),
444        }
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use fvm4::trace::ExecutionEvent as E4;
452    use quickcheck_macros::quickcheck;
453
454    #[quickcheck]
455    fn receipt_cbor_serde_serialize(receipt: Receipt) {
456        let encoded = fvm_ipld_encoding::to_vec(&receipt).unwrap();
457        let encoded2 = match &receipt {
458            Receipt::V2(v) => fvm_ipld_encoding::to_vec(v),
459            Receipt::V3(v) => fvm_ipld_encoding::to_vec(v),
460            Receipt::V4(v) => fvm_ipld_encoding::to_vec(v),
461        }
462        .unwrap();
463        assert_eq!(encoded, encoded2);
464    }
465
466    /// Build an `ApplyRet` of the given variant carrying `exec_trace`; every other field is a
467    /// zero/empty placeholder (the `fvm` structs have no `Default` to lean on). Trailing
468    /// `field: value` pairs supply the fields that differ across versions.
469    macro_rules! apply_ret {
470        ($variant:ident, $inner:ident, $receipt:expr, $exec_trace:expr $(, $ef:ident: $ev:expr)*) => {
471            ApplyRet::$variant($inner {
472                msg_receipt: $receipt,
473                penalty: Default::default(),
474                miner_tip: Default::default(),
475                base_fee_burn: Default::default(),
476                over_estimation_burn: Default::default(),
477                refund: Default::default(),
478                gas_refund: 0,
479                gas_burned: 0,
480                failure_info: None,
481                exec_trace: $exec_trace,
482                $($ef: $ev,)*
483            })
484        };
485    }
486
487    #[test]
488    fn trace_has_call_return_exit_code_across_versions() {
489        let want = ExitCode::SYS_OUT_OF_GAS;
490        let miss = ExitCode::new(33);
491
492        // V4 matches only the wanted code (guards the old footgun that matched any `Some(_)`).
493        let v4 = |t| apply_ret!(V4, ApplyRet_v4, Receipt_v4 { exit_code: ExitCode::OK, return_data: RawBytes::default(), gas_used: 0, events_root: None }, t, events: vec![], return_codec: None);
494        assert!(v4(vec![E4::CallReturn(want, None)]).trace_has_call_return_exit_code(want));
495        assert!(!v4(vec![E4::CallReturn(want, None)]).trace_has_call_return_exit_code(miss));
496        assert!(
497            !v4(vec![E4::CallReturn(ExitCode::OK, None)]).trace_has_call_return_exit_code(want)
498        );
499        assert!(!v4(vec![]).trace_has_call_return_exit_code(want));
500
501        // V3 applies the same check to the fvm3 trace.
502        use fvm3::trace::ExecutionEvent as E3;
503        let v3 = |t| apply_ret!(V3, ApplyRet_v3, Receipt_v3 { exit_code: fvm_shared3::error::ExitCode::OK, return_data: RawBytes::default(), gas_used: 0, events_root: None }, t, events: vec![]);
504        let oog3 = fvm_shared3::error::ExitCode::SYS_OUT_OF_GAS;
505        assert!(v3(vec![E3::CallReturn(oog3, None)]).trace_has_call_return_exit_code(want));
506        assert!(!v3(vec![E3::CallReturn(oog3, None)]).trace_has_call_return_exit_code(miss));
507
508        // FVM2 `CallReturn` carries no exit code, so it never matches.
509        use fvm2::trace::ExecutionEvent as E2;
510        let v2 = apply_ret!(
511            V2,
512            ApplyRet_v2,
513            Receipt_v2 {
514                exit_code: fvm_shared2::error::ExitCode::OK,
515                return_data: RawBytes::default(),
516                gas_used: 0
517            },
518            vec![E2::CallReturn(RawBytes::default())]
519        );
520        assert!(!v2.trace_has_call_return_exit_code(want));
521    }
522}