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