Skip to main content

forest/state_manager/
utils.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::MinerActorStateLoad as _;
5use crate::prelude::*;
6use crate::shim::actors::miner;
7use crate::shim::{
8    actors::{is_account_actor, is_ethaccount_actor, is_placeholder_actor},
9    address::{Address, Payload},
10    randomness::Randomness,
11    sector::{ExtendedSectorInfo, RegisteredPoStProof, RegisteredSealProof},
12    state_tree::ActorState,
13    version::NetworkVersion,
14};
15use crate::state_manager::{StateManager, errors::*};
16use crate::utils::encoding::prover_id_from_u64;
17use fil_actors_shared::filecoin_proofs_api::post;
18use fil_actors_shared::fvm_ipld_bitfield::BitField;
19use fvm_ipld_encoding::bytes_32;
20
21impl StateManager {
22    /// Retrieves and generates a vector of sector info for the winning `PoSt`
23    /// verification.
24    pub fn get_sectors_for_winning_post(
25        &self,
26        st: &Cid,
27        nv: NetworkVersion,
28        miner_address: &Address,
29        rand: Randomness,
30    ) -> anyhow::Result<Vec<ExtendedSectorInfo>> {
31        let store = self.db();
32
33        let actor = self
34            .get_actor(miner_address, *st)?
35            .ok_or_else(|| Error::state("Miner actor address could not be resolved"))?;
36        let mas = miner::State::load(self.db(), actor.code, actor.state)?;
37
38        let proving_sectors = {
39            let mut proving_sectors = BitField::new();
40
41            if nv < NetworkVersion::V7 {
42                mas.for_each_deadline(&self.chain_config().policy, store, |_, deadline| {
43                    let mut fault_sectors = BitField::new();
44                    deadline.for_each(store, |_, partition: miner::Partition| {
45                        proving_sectors |= partition.all_sectors();
46                        fault_sectors |= partition.faulty_sectors();
47                        Ok(())
48                    })?;
49
50                    proving_sectors -= &fault_sectors;
51                    Ok(())
52                })?;
53            } else {
54                mas.for_each_deadline(&self.chain_config().policy, store, |_, deadline| {
55                    deadline.for_each(store, |_, partition: miner::Partition| {
56                        proving_sectors |= &partition.active_sectors();
57                        Ok(())
58                    })?;
59                    Ok(())
60                })?;
61            }
62            proving_sectors
63        };
64
65        let num_prov_sect = proving_sectors.len();
66
67        if num_prov_sect == 0 {
68            return Ok(Vec::new());
69        }
70
71        let info = mas.info(store)?;
72        let spt = RegisteredSealProof::from_sector_size(info.sector_size(), nv);
73
74        let wpt = spt.registered_winning_post_proof()?;
75
76        let m_id = miner_address.id()?;
77
78        let ids = generate_winning_post_sector_challenge(wpt.into(), m_id, rand, num_prov_sect)?;
79
80        let mut iter = proving_sectors.iter();
81
82        let mut selected_sectors = BitField::new();
83        for n in ids {
84            let sno = iter.nth(n as usize).ok_or_else(|| {
85                anyhow::anyhow!(
86                    "Error iterating over proving sectors, id {} does not exist",
87                    n
88                )
89            })?;
90            selected_sectors.set(sno);
91        }
92
93        let sectors = mas.load_sectors(store, Some(&selected_sectors))?;
94
95        let out = sectors
96            .into_iter()
97            .map(|s_info| ExtendedSectorInfo {
98                proof: s_info.seal_proof.into(),
99                sector_number: s_info.sector_number,
100                sector_key: s_info.sector_key_cid,
101                sealed_cid: s_info.sealed_cid,
102            })
103            .collect();
104
105        Ok(out)
106    }
107}
108
109pub fn is_valid_for_sending(network_version: NetworkVersion, actor: &ActorState) -> bool {
110    // Comments from Lotus:
111    // Before nv18 (Hygge), we only supported built-in account actors as senders.
112    //
113    // Note: this gate is probably superfluous, since:
114    // 1. Placeholder actors cannot be created before nv18.
115    // 2. EthAccount actors cannot be created before nv18.
116    // 3. Delegated addresses cannot be created before nv18.
117    //
118    // But it's a safeguard.
119    //
120    // Note 2: ad-hoc checks for network versions like this across the codebase
121    // will be problematic with networks with diverging version lineages
122    // (e.g. Hyperspace). We need to revisit this strategy entirely.
123    if network_version < NetworkVersion::V18 {
124        return is_account_actor(&actor.code);
125    }
126
127    // After nv18, we also support other kinds of senders.
128    if is_account_actor(&actor.code) || is_ethaccount_actor(&actor.code) {
129        return true;
130    }
131
132    // Allow placeholder actors with a delegated address and nonce 0 to send a
133    // message. These will be converted to an EthAccount actor on first send.
134    if !is_placeholder_actor(&actor.code)
135        || actor.sequence != 0
136        || actor.delegated_address.is_none()
137    {
138        return false;
139    }
140
141    // Only allow such actors to send if their delegated address is in the EAM's
142    // namespace.
143    if let Payload::Delegated(address) = actor
144        .delegated_address
145        .as_ref()
146        .expect("unfallible")
147        .payload()
148    {
149        address.namespace() == Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap()
150    } else {
151        false
152    }
153}
154
155/// Generates sector challenge indexes for use in winning PoSt verification.
156fn generate_winning_post_sector_challenge(
157    proof: RegisteredPoStProof,
158    prover_id: u64,
159    mut rand: Randomness,
160    eligible_sector_count: u64,
161) -> anyhow::Result<Vec<u64>> {
162    // Necessary to be valid bls12 381 element.
163    if let Some(b31) = rand.0.get_mut(31) {
164        *b31 &= 0x3f;
165    } else {
166        anyhow::bail!("rand should have at least 32 bytes");
167    }
168
169    post::generate_winning_post_sector_challenge(
170        proof.try_into()?,
171        &bytes_32(&rand.0),
172        eligible_sector_count,
173        prover_id_from_u64(prover_id),
174    )
175}
176
177pub mod state_compute {
178    #[cfg(test)]
179    use crate::utils::encoding::hex;
180    use crate::{
181        blocks::{FullTipset, Tipset},
182        chain::store::ChainStore,
183        chain_sync::load_full_tipset,
184        db::{
185            MemoryDB,
186            car::{AnyCar, ManyCar},
187        },
188        genesis::read_genesis_header,
189        interpreter::VMTrace,
190        networks::{ChainConfig, NetworkChain},
191        prelude::*,
192        state_manager::{ExecutedTipset, StateManager},
193        utils::net::{DownloadFileOption, download_file_with_cache},
194    };
195    use directories::ProjectDirs;
196    use sonic_rs::JsonValueTrait;
197    use std::{
198        path::{Path, PathBuf},
199        sync::LazyLock,
200        time::{Duration, Instant},
201    };
202    use tokio::io::AsyncReadExt;
203    use url::Url;
204
205    const DO_SPACE_ROOT: &str = "https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/";
206
207    #[allow(dead_code)]
208    pub async fn get_state_compute_snapshot(
209        chain: &NetworkChain,
210        epoch: ChainEpoch,
211    ) -> anyhow::Result<PathBuf> {
212        get_state_snapshot(chain, "state_compute", epoch).await
213    }
214
215    #[allow(dead_code)]
216    async fn get_state_validate_snapshot(
217        chain: &NetworkChain,
218        epoch: ChainEpoch,
219    ) -> anyhow::Result<PathBuf> {
220        get_state_snapshot(chain, "state_validate", epoch).await
221    }
222
223    #[allow(dead_code)]
224    pub async fn get_state_snapshot(
225        chain: &NetworkChain,
226        bucket: &str,
227        epoch: ChainEpoch,
228    ) -> anyhow::Result<PathBuf> {
229        let file = format!("{bucket}/{chain}_{epoch}.forest.car.zst");
230        get_state_snapshot_file(&file).await
231    }
232
233    pub async fn get_state_snapshot_file(file: &str) -> anyhow::Result<PathBuf> {
234        static SNAPSHOT_CACHE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
235            let project_dir = ProjectDirs::from("com", "ChainSafe", "Forest");
236            project_dir
237                .map(|d| d.cache_dir().to_path_buf())
238                .unwrap_or_else(std::env::temp_dir)
239                .join("state_compute_snapshots")
240        });
241
242        let url = Url::parse(&format!("{DO_SPACE_ROOT}{file}"))?;
243        let path = crate::utils::retry(
244            crate::utils::RetryArgs {
245                timeout: Some(Duration::from_secs(30)),
246                max_retries: Some(5),
247                delay: Some(Duration::from_secs(1)),
248            },
249            || {
250                download_file_with_cache(
251                    &url,
252                    &SNAPSHOT_CACHE_DIR,
253                    DownloadFileOption::NonResumable,
254                )
255            },
256        )
257        .await?
258        .path;
259        #[cfg(test)]
260        {
261            // To determine whether a test failure is caused by data corruption
262            println!(
263                "snapshot: {file}, sha256sum: {}",
264                hex::encode(crate::utils::hash::digest_file::<sha2::Sha256>(&path)?)
265            );
266        }
267        Ok(path)
268    }
269
270    pub async fn prepare_state_compute(
271        chain: &NetworkChain,
272        snapshot: &Path,
273    ) -> anyhow::Result<(StateManager, Tipset, Tipset)> {
274        let snap_car = AnyCar::try_from(snapshot)?;
275        let ts_next = snap_car.heaviest_tipset()?;
276        let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?);
277        let ts = Tipset::load_required(&db, ts_next.parents())?;
278        let chain_config = Arc::new(ChainConfig::from_chain(chain));
279        let genesis_header =
280            read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db)
281                .await?;
282        let chain_store = ChainStore::new(db, chain_config, genesis_header)?;
283        let state_manager = StateManager::new(chain_store)?;
284        Ok((state_manager, ts, ts_next))
285    }
286
287    pub async fn prepare_state_validate(
288        chain: &NetworkChain,
289        snapshot: &Path,
290    ) -> anyhow::Result<(StateManager, FullTipset)> {
291        let (sm, _, ts) = prepare_state_compute(chain, snapshot).await?;
292        let fts = load_full_tipset(sm.chain_store(), ts.key())?;
293        Ok((sm, fts))
294    }
295
296    pub async fn state_compute(
297        state_manager: &StateManager,
298        ts: Tipset,
299        ts_next: &Tipset,
300    ) -> anyhow::Result<()> {
301        let epoch = ts.epoch();
302        let expected_state_root = *ts_next.parent_state();
303        let expected_receipt_root = *ts_next.parent_message_receipts();
304        let start = Instant::now();
305        let ExecutedTipset {
306            state_root,
307            receipt_root,
308            ..
309        } = state_manager
310            .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced)
311            .await?;
312        tracing::info!(
313            "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, took {}.",
314            humantime::format_duration(start.elapsed())
315        );
316        anyhow::ensure!(
317            state_root == expected_state_root,
318            "state root mismatch, state_root: {state_root}, expected_state_root: {expected_state_root}"
319        );
320        anyhow::ensure!(
321            receipt_root == expected_receipt_root,
322            "receipt root mismatch, receipt_root: {receipt_root}, expected_receipt_root: {expected_receipt_root}"
323        );
324        Ok(())
325    }
326
327    pub async fn list_state_snapshot_files() -> anyhow::Result<Vec<String>> {
328        let url = Url::parse(&format!("{DO_SPACE_ROOT}?format=json&prefix=state_"))?;
329        let mut json_str = String::new();
330        crate::utils::net::reader(url.as_str(), DownloadFileOption::NonResumable, None)
331            .await?
332            .read_to_string(&mut json_str)
333            .await?;
334        let obj: sonic_rs::Object = sonic_rs::from_str(&json_str)?;
335        let files = obj
336            .iter()
337            .filter_map(|(k, v)| {
338                if k == "Contents"
339                    && let sonic_rs::ValueRef::Array(arr) = v.as_ref()
340                    && let Some(first) = arr.first()
341                    && let Some(file) = first.as_str()
342                    && file.ends_with(".car.zst")
343                {
344                    Some(file.to_string())
345                } else {
346                    None
347                }
348            })
349            .collect();
350        Ok(files)
351    }
352
353    #[cfg(test)]
354    mod tests {
355        //!
356        //! Test snapshots are generate by `forest-dev state` tool
357        //!
358
359        use super::*;
360        #[cfg(feature = "cargo-test")]
361        use crate::chain_sync::tipset_syncer::validate_tipset;
362
363        #[tokio::test(flavor = "multi_thread")]
364        async fn test_list_state_snapshot_files() {
365            let files = list_state_snapshot_files().await.unwrap();
366            println!("{files:?}");
367            assert!(files.len() > 1);
368            get_state_snapshot_file(&files[0]).await.unwrap();
369        }
370
371        include!(concat!(env!("OUT_DIR"), "/__state_compute_tests_gen.rs"));
372
373        #[allow(dead_code)]
374        async fn state_compute_test_run(chain: NetworkChain, epoch: ChainEpoch) {
375            let snapshot = get_state_compute_snapshot(&chain, epoch).await.unwrap();
376            let (sm, ts, ts_next) = prepare_state_compute(&chain, &snapshot).await.unwrap();
377            state_compute(&sm, ts, &ts_next).await.unwrap();
378        }
379
380        #[cfg(feature = "cargo-test")]
381        #[tokio::test(flavor = "multi_thread")]
382        #[fickle::fickle]
383        async fn cargo_test_state_validate_mainnet_5688000() {
384            let chain = NetworkChain::Mainnet;
385            let snapshot = get_state_validate_snapshot(&chain, 5688000).await.unwrap();
386            let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap();
387            validate_tipset(&sm, fts, None).await.unwrap();
388        }
389
390        #[cfg(feature = "cargo-test")]
391        #[tokio::test(flavor = "multi_thread")]
392        #[fickle::fickle]
393        async fn cargo_test_state_replay_uses_trace_cache_calibnet_3408952() {
394            let chain = NetworkChain::Calibnet;
395            let snapshot = get_state_compute_snapshot(&chain, 3408952).await.unwrap();
396            let (sm, ts, _) = prepare_state_compute(&chain, &snapshot).await.unwrap();
397            let msg_cid = sm
398                .chain_store()
399                .messages_for_tipset(&ts)
400                .unwrap()
401                .first()
402                .expect("test tipset must contain messages")
403                .cid();
404
405            let replayed = sm.replay(ts.clone(), msg_cid).await.unwrap();
406            assert_eq!(replayed.msg_cid, msg_cid);
407
408            let (_, trace) = sm
409                .trace_cache
410                .get(ts.key())
411                .expect("replay must populate the tipset trace cache");
412            let from_cache = trace
413                .iter()
414                .find(|r| r.msg_cid == msg_cid)
415                .expect("cached trace must contain the replayed message");
416            assert_eq!(**from_cache, replayed);
417
418            // A second replay of the same tipset must not re-execute it.
419            let misses = sm.trace_cache.misses();
420            let replayed_again = sm.replay(ts.clone(), msg_cid).await.unwrap();
421            assert_eq!(replayed_again, replayed);
422            assert_eq!(sm.trace_cache.misses(), misses);
423        }
424
425        // Shark state migration
426        #[cfg(feature = "cargo-test")]
427        #[tokio::test(flavor = "multi_thread")]
428        #[fickle::fickle]
429        async fn cargo_test_state_validate_calibnet_16802() {
430            let chain = NetworkChain::Calibnet;
431            let snapshot = get_state_validate_snapshot(&chain, 16802).await.unwrap();
432            let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap();
433            validate_tipset(&sm, fts, None).await.unwrap();
434        }
435
436        // Hygge state migration
437        #[cfg(feature = "cargo-test")]
438        #[tokio::test(flavor = "multi_thread")]
439        #[fickle::fickle]
440        async fn cargo_test_state_validate_calibnet_322356() {
441            let chain = NetworkChain::Calibnet;
442            let snapshot = get_state_validate_snapshot(&chain, 322356).await.unwrap();
443            let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap();
444            validate_tipset(&sm, fts, None).await.unwrap();
445        }
446    }
447}
448
449#[cfg(test)]
450mod test {
451    use crate::shim::{address::Address, econ::TokenAmount, state_tree::ActorState};
452    use cid::Cid;
453
454    use super::*;
455
456    #[test]
457    fn is_valid_for_sending_test() {
458        let create_actor = |code: &Cid, sequence: u64, delegated_address: Option<Address>| {
459            ActorState::new(
460                code.to_owned(),
461                // changing this cid will unleash unthinkable horrors upon the world
462                Cid::try_from("bafk2bzaceavfgpiw6whqigmskk74z4blm22nwjfnzxb4unlqz2e4wgcthulhu")
463                    .unwrap(),
464                TokenAmount::default(),
465                sequence,
466                delegated_address,
467            )
468        };
469
470        // calibnet actor version 10
471        let account_actor_cid =
472            Cid::try_from("bafk2bzaceavfgpiw6whqigmskk74z4blm22nwjfnzxb4unlqz2e4wg3c5ujpw")
473                .unwrap();
474        let ethaccount_actor_cid =
475            Cid::try_from("bafk2bzacebiyrhz32xwxi6xql67aaq5nrzeelzas472kuwjqmdmgwotpkj35e")
476                .unwrap();
477        let placeholder_actor_cid =
478            Cid::try_from("bafk2bzacedfvut2myeleyq67fljcrw4kkmn5pb5dpyozovj7jpoez5irnc3ro")
479                .unwrap();
480
481        // happy path for account actor
482        let actor = create_actor(&account_actor_cid, 0, None);
483        assert!(is_valid_for_sending(NetworkVersion::V17, &actor));
484
485        // eth account not allowed before v18, should fail
486        let actor = create_actor(&ethaccount_actor_cid, 0, None);
487        assert!(!is_valid_for_sending(NetworkVersion::V17, &actor));
488
489        // happy path for eth account
490        assert!(is_valid_for_sending(NetworkVersion::V18, &actor));
491
492        // no delegated address for placeholder actor, should fail
493        let actor = create_actor(&placeholder_actor_cid, 0, None);
494        assert!(!is_valid_for_sending(NetworkVersion::V18, &actor));
495
496        // happy path for the placeholder actor
497        let delegated_address = Address::new_delegated(
498            Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap(),
499            &[0; 20],
500        )
501        .ok();
502        let actor = create_actor(&placeholder_actor_cid, 0, delegated_address);
503        assert!(is_valid_for_sending(NetworkVersion::V18, &actor));
504
505        // sequence not 0, should fail
506        let actor = create_actor(&placeholder_actor_cid, 1, delegated_address);
507        assert!(!is_valid_for_sending(NetworkVersion::V18, &actor));
508
509        // delegated address not in EAM namespace, should fail
510        let delegated_address =
511            Address::new_delegated(Address::CHAOS_ACTOR.id().unwrap(), &[0; 20]).ok();
512        let actor = create_actor(&placeholder_actor_cid, 0, delegated_address);
513        assert!(!is_valid_for_sending(NetworkVersion::V18, &actor));
514    }
515}
516
517/// Parsed tree of [`fvm4::trace::ExecutionEvent`]s
518pub mod structured {
519    use crate::{
520        rpc::state::{ActorTrace, ExecutionTrace, GasTrace, MessageTrace, ReturnTrace, TraceIpld},
521        shim::kernel::ErrorNumber,
522    };
523    use std::collections::VecDeque;
524
525    use crate::shim::{
526        address::Address,
527        error::ExitCode,
528        gas::GasCharge,
529        kernel::SyscallError,
530        trace::{Call, CallReturn, ExecutionEvent},
531    };
532    use fvm_ipld_encoding::{RawBytes, ipld_block::IpldBlock};
533    use itertools::Either;
534
535    enum CallTreeReturn {
536        Return(CallReturn),
537        Abort(ExitCode),
538        Error(SyscallError),
539    }
540
541    #[derive(Debug, thiserror::Error)]
542    pub enum BuildExecutionTraceError {
543        #[error(
544            "every ExecutionEvent::Return | ExecutionEvent::CallError should be preceded by an ExecutionEvent::Call, but this one wasn't"
545        )]
546        UnexpectedReturn,
547        #[error(
548            "every ExecutionEvent::Call should have a corresponding ExecutionEvent::Return, but this one didn't"
549        )]
550        NoReturn,
551        #[error("unrecognised ExecutionEvent variant: {0:?}")]
552        UnrecognisedEvent(Box<dyn std::fmt::Debug + Send + Sync + 'static>),
553    }
554
555    /// Construct a single [`ExecutionTrace`]s from a linear array of [`ExecutionEvent`](fvm4::trace::ExecutionEvent)s.
556    ///
557    /// This function is so-called because it similar to the parse step in a traditional compiler:
558    /// ```text
559    /// text --lex-->     tokens     --parse-->   AST
560    ///               ExecutionEvent --parse--> ExecutionTrace
561    /// ```
562    ///
563    /// This function is notable in that [`GasCharge`](fvm4::gas::GasCharge)s which precede a [`ExecutionTrace`] at the root level
564    /// are attributed to that node.
565    ///
566    /// We call this "front loading", and is copied from [this (rather obscure) code in `filecoin-ffi`](https://github.com/filecoin-project/filecoin-ffi/blob/v1.23.0/rust/src/fvm/machine.rs#L209)
567    ///
568    /// ```text
569    /// GasCharge GasCharge Call GasCharge Call CallError CallReturn
570    /// ────┬──── ────┬──── ─┬── ────┬──── ─┬── ───┬───── ────┬─────
571    ///     │         │      │       │      │      │          │
572    ///     │         │      │       │      └─(T)──┘          │
573    ///     │         │      └───────┴───(T)───┴──────────────┘
574    ///     └─────────┴──────────────────►│
575    ///     ("front loaded" GasCharges)   │
576    ///                                  (T)
577    ///
578    /// (T): a ExecutionTrace node
579    /// ```
580    ///
581    /// Multiple call trees and trailing gas will be warned and ignored.
582    /// If no call tree is found, returns [`Ok(None)`]
583    pub fn parse_events(
584        events: Vec<ExecutionEvent>,
585    ) -> anyhow::Result<Option<ExecutionTrace>, BuildExecutionTraceError> {
586        let mut events = VecDeque::from(events);
587        let mut front_load_me = vec![];
588        let mut call_trees = vec![];
589
590        // we don't use a `for` loop so we can pass events them to inner parsers
591        while let Some(event) = events.pop_front() {
592            match event {
593                ExecutionEvent::GasCharge(gc) => front_load_me.push(gc),
594                ExecutionEvent::Call(call) => call_trees.push(ExecutionTrace::parse(call, {
595                    // if ExecutionTrace::parse took impl Iterator<Item = ExecutionEvent>
596                    // the compiler would infinitely recurse trying to resolve
597                    // &mut &mut &mut ..: Iterator
598                    // so use a VecDeque instead
599                    for gc in front_load_me.drain(..).rev() {
600                        events.push_front(ExecutionEvent::GasCharge(gc))
601                    }
602                    &mut events
603                })?),
604                ExecutionEvent::CallReturn(_)
605                | ExecutionEvent::CallAbort(_)
606                | ExecutionEvent::CallError(_) => {
607                    return Err(BuildExecutionTraceError::UnexpectedReturn);
608                }
609                ExecutionEvent::Log(_ignored) => {}
610                ExecutionEvent::InvokeActor(_cid) => {}
611                ExecutionEvent::Ipld { .. } => {}
612                ExecutionEvent::Unknown(u) => {
613                    return Err(BuildExecutionTraceError::UnrecognisedEvent(Box::new(u)));
614                }
615            }
616        }
617
618        if !front_load_me.is_empty() {
619            tracing::warn!(
620                "vm tracing: ignoring {} trailing gas charges",
621                front_load_me.len()
622            );
623        }
624
625        match call_trees.len() {
626            0 => Ok(None),
627            1 => Ok(Some(call_trees.remove(0))),
628            many => {
629                tracing::warn!(
630                    "vm tracing: ignoring {} call trees at the root level",
631                    many - 1
632                );
633                Ok(Some(call_trees.remove(0)))
634            }
635        }
636    }
637
638    impl ExecutionTrace {
639        /// ```text
640        ///    events: GasCharge Call CallError CallReturn ...
641        ///            ────┬──── ─┬── ───┬───── ────┬─────
642        ///                │      │      │          │
643        /// ┌──────┐       │      └─(T)──┘          │
644        /// │ Call ├───────┴───(T)───┴──────────────┘
645        /// └──────┘            |                   ▲
646        ///                     ▼                   │
647        ///              Returned ExecutionTrace    │
648        ///                                     parsing end
649        /// ```
650        fn parse(
651            call: Call,
652            events: &mut VecDeque<ExecutionEvent>,
653        ) -> Result<ExecutionTrace, BuildExecutionTraceError> {
654            let mut gas_charges = vec![];
655            let mut subcalls = vec![];
656            let mut actor_trace = None;
657            let mut logs = vec![];
658            let mut ipld_ops = vec![];
659
660            // we don't use a for loop over `events` so we can pass them to recursive calls
661            while let Some(event) = events.pop_front() {
662                let found_return = match event {
663                    ExecutionEvent::GasCharge(gc) => {
664                        gas_charges.push(to_gas_trace(gc));
665                        None
666                    }
667                    ExecutionEvent::Call(call) => {
668                        subcalls.push(Self::parse(call, events)?);
669                        None
670                    }
671                    ExecutionEvent::CallReturn(ret) => Some(CallTreeReturn::Return(ret)),
672                    ExecutionEvent::CallAbort(ab) => Some(CallTreeReturn::Abort(ab)),
673                    ExecutionEvent::CallError(e) => Some(CallTreeReturn::Error(e)),
674                    ExecutionEvent::Log(log) => {
675                        logs.push(log);
676                        None
677                    }
678                    ExecutionEvent::InvokeActor(cid) => {
679                        actor_trace = match cid {
680                            Either::Left(_cid) => None,
681                            Either::Right(actor) => Some(ActorTrace {
682                                id: actor.id,
683                                state: actor.state,
684                            }),
685                        };
686                        None
687                    }
688                    ExecutionEvent::Ipld { op, cid, size } => {
689                        ipld_ops.push(TraceIpld {
690                            op: op.into(),
691                            cid,
692                            size: size as u64,
693                        });
694                        None
695                    }
696                    // RUST: This should be caught at compile time with #[deny(non_exhaustive_omitted_patterns)]
697                    //       So that BuildExecutionTraceError::UnrecognisedEvent is never constructed
698                    //       But that lint is not yet stabilised: https://github.com/rust-lang/rust/issues/89554
699                    ExecutionEvent::Unknown(u) => {
700                        return Err(BuildExecutionTraceError::UnrecognisedEvent(Box::new(u)));
701                    }
702                };
703
704                // commonise the return branch
705                if let Some(ret) = found_return {
706                    return Ok(ExecutionTrace {
707                        msg: to_message_trace(call),
708                        msg_rct: to_return_trace(ret),
709                        gas_charges,
710                        subcalls,
711                        invoked_actor: actor_trace,
712                        logs,
713                        ipld_ops,
714                    });
715                }
716            }
717
718            Err(BuildExecutionTraceError::NoReturn)
719        }
720    }
721
722    fn to_message_trace(call: Call) -> MessageTrace {
723        let (bytes, codec) = to_bytes_codec(call.params);
724        MessageTrace {
725            from: Address::new_id(call.from),
726            to: call.to,
727            value: call.value,
728            method: call.method_num,
729            params: bytes,
730            params_codec: codec,
731            gas_limit: call.gas_limit,
732            read_only: call.read_only,
733        }
734    }
735
736    fn to_return_trace(ret: CallTreeReturn) -> ReturnTrace {
737        match ret {
738            CallTreeReturn::Return(return_code) => {
739                let exit_code = return_code.exit_code.unwrap_or(0.into());
740                let (bytes, codec) = to_bytes_codec(return_code.data);
741                ReturnTrace {
742                    exit_code,
743                    r#return: bytes,
744                    return_codec: codec,
745                }
746            }
747            CallTreeReturn::Abort(exit_code) => ReturnTrace {
748                exit_code,
749                r#return: RawBytes::default(),
750                return_codec: 0,
751            },
752            CallTreeReturn::Error(syscall_error) => match syscall_error.number {
753                ErrorNumber::InsufficientFunds => ReturnTrace {
754                    exit_code: ExitCode::from(6),
755                    r#return: RawBytes::default(),
756                    return_codec: 0,
757                },
758                _ => ReturnTrace {
759                    exit_code: ExitCode::from(0),
760                    r#return: RawBytes::default(),
761                    return_codec: 0,
762                },
763            },
764        }
765    }
766
767    fn to_bytes_codec(data: Either<RawBytes, Option<IpldBlock>>) -> (RawBytes, u64) {
768        match data {
769            Either::Left(l) => (l, 0),
770            Either::Right(r) => match r {
771                Some(b) => (RawBytes::from(b.data), b.codec),
772                None => (RawBytes::default(), 0),
773            },
774        }
775    }
776
777    fn to_gas_trace(gc: GasCharge) -> GasTrace {
778        GasTrace {
779            name: gc.name().into(),
780            total_gas: gc.total().round_up(),
781            compute_gas: gc.compute_gas().round_up(),
782            storage_gas: gc.other_gas().round_up(),
783            time_taken: gc.elapsed().as_nanos(),
784        }
785    }
786}