Skip to main content

forest/rpc/methods/
chain.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4pub mod types;
5use types::*;
6
7#[cfg(test)]
8use crate::blocks::RawBlockHeader;
9use crate::blocks::{Block, CachingBlockHeader, Tipset, TipsetKey};
10use crate::chain::index::ResolveNullTipset;
11use crate::chain::{ChainStore, ExportOptions, FilecoinSnapshotVersion, HeadChange};
12use crate::chain_sync::{get_full_tipset, load_full_tipset};
13use crate::cid_collections::{CidHashSet, FileBackedCidHashSet};
14use crate::db::EthMappingsStore;
15use crate::ipld::DfsIter;
16use crate::ipld::{CHAIN_EXPORT_STATUS, cancel_export, end_export, start_export};
17use crate::lotus_json::{HasLotusJson, LotusJson, lotus_json_with_self};
18#[cfg(test)]
19use crate::lotus_json::{assert_all_snapshots, assert_unchanged_via_json};
20use crate::message::{ChainMessage, SignedMessage};
21use crate::rpc::eth::Block as EthBlock;
22use crate::rpc::eth::{
23    EthLog, TxInfo, eth_logs_with_filter, types::ApiHeaders, types::EthFilterSpec,
24};
25use crate::rpc::f3::F3ExportLatestSnapshot;
26use crate::rpc::types::*;
27use crate::rpc::{ApiPaths, Ctx, EthEventHandler, Permission, RpcMethod, ServerError};
28use crate::shim::clock::ChainEpoch;
29use crate::shim::error::ExitCode;
30use crate::shim::executor::Receipt;
31use crate::shim::message::Message;
32use crate::utils::ShallowClone;
33use crate::utils::db::CborStoreExt as _;
34use crate::utils::io::VoidAsyncWriter;
35use crate::utils::misc::env::is_env_truthy;
36use anyhow::{Context as _, Result};
37use cid::Cid;
38use enumflags2::{BitFlags, make_bitflags};
39use fvm_ipld_blockstore::Blockstore;
40use fvm_ipld_encoding::{CborStore, RawBytes};
41use hex::ToHex;
42use ipld_core::ipld::Ipld;
43use itertools::Itertools as _;
44use jsonrpsee::types::Params;
45use jsonrpsee::types::error::ErrorObjectOwned;
46use num::BigInt;
47use schemars::JsonSchema;
48use serde::{Deserialize, Serialize};
49use sha2::Sha256;
50use std::fs::File;
51use std::sync::Arc;
52use std::{collections::VecDeque, path::PathBuf, sync::LazyLock};
53use tokio::sync::{
54    Mutex,
55    broadcast::{self, Receiver as Subscriber},
56};
57use tokio::task::JoinHandle;
58use tokio_util::sync::CancellationToken;
59
60const HEAD_CHANNEL_CAPACITY: usize = 10;
61
62/// [`SAFE_HEIGHT_DISTANCE`] is the distance from the latest tipset, i.e. "heaviest", that
63/// is considered to be safe from re-orgs at an increasingly diminishing
64/// probability.
65///
66/// This is used to determine the safe tipset when using the "safe" tag in
67/// [`TipsetSelector`] or via Eth JSON-RPC APIs. Note that "safe" doesn't guarantee
68/// finality, but rather a high probability of not being reverted. For guaranteed
69/// finality, use the "finalized" tag.
70///
71/// This constant is experimental and may change in the future.
72/// Discussion on this current value and a tracking item to document the
73/// probabilistic impact of various values is in
74/// https://github.com/filecoin-project/go-f3/issues/944
75pub const SAFE_HEIGHT_DISTANCE: ChainEpoch = 200;
76
77static CHAIN_EXPORT_LOCK: LazyLock<Mutex<Option<CancellationToken>>> =
78    LazyLock::new(|| Mutex::new(None));
79
80/// Subscribes to head changes from the chain store and broadcasts new blocks.
81///
82/// # Notes
83///
84/// Spawns an internal `tokio` task that can be aborted anytime via the returned `JoinHandle`,
85/// allowing manual cleanup if needed.
86pub(crate) fn new_heads<DB: Blockstore + EthMappingsStore + Send + Sync + 'static>(
87    data: Ctx<DB>,
88) -> (Subscriber<ApiHeaders>, JoinHandle<()>) {
89    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
90
91    let mut head_changes_rx = data.chain_store().subscribe_head_changes();
92
93    let handle = tokio::spawn(async move {
94        while let Ok(changes) = head_changes_rx.recv().await {
95            for ts in changes.applies {
96                // Convert the tipset to an Ethereum block with full transaction info
97                // Note: In Filecoin's Eth RPC, a tipset maps to a single Ethereum block
98                match EthBlock::from_filecoin_tipset(data.clone(), ts, TxInfo::Full).await {
99                    Ok(block) => {
100                        if let Err(e) = sender.send(ApiHeaders(block)) {
101                            tracing::error!("Failed to send headers: {}", e);
102                            return;
103                        }
104                    }
105                    Err(e) => {
106                        tracing::error!("Failed to convert tipset to eth block: {}", e);
107                    }
108                }
109            }
110        }
111    });
112
113    (receiver, handle)
114}
115
116/// Subscribes to head changes from the chain store and broadcasts new `Ethereum` logs.
117///
118/// # Notes
119///
120/// Spawns an internal `tokio` task that can be aborted anytime via the returned `JoinHandle`,
121/// allowing manual cleanup if needed.
122pub(crate) fn logs<DB: Blockstore + EthMappingsStore + Sync + Send + 'static>(
123    ctx: &Ctx<DB>,
124    filter: Option<EthFilterSpec>,
125) -> (Subscriber<Vec<EthLog>>, JoinHandle<()>) {
126    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
127
128    let mut head_changes_rx = ctx.chain_store().subscribe_head_changes();
129
130    let ctx = ctx.clone();
131
132    let handle = tokio::spawn(async move {
133        while let Ok(changes) = head_changes_rx.recv().await {
134            for ts in changes.applies {
135                match eth_logs_with_filter(&ctx, &ts, filter.clone()).await {
136                    Ok(logs) => {
137                        if !logs.is_empty()
138                            && let Err(e) = sender.send(logs)
139                        {
140                            tracing::error!("Failed to send logs for tipset {}: {}", ts.key(), e);
141                            break;
142                        }
143                    }
144                    Err(e) => {
145                        tracing::error!("Failed to fetch logs for tipset {}: {}", ts.key(), e);
146                    }
147                }
148            }
149        }
150    });
151
152    (receiver, handle)
153}
154
155pub enum ChainGetFinalizedTipset {}
156impl RpcMethod<0> for ChainGetFinalizedTipset {
157    const NAME: &'static str = "Filecoin.ChainGetFinalizedTipSet";
158    const PARAM_NAMES: [&'static str; 0] = [];
159    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::V1);
160    const PERMISSION: Permission = Permission::Read;
161    const DESCRIPTION: Option<&'static str> = Some(
162        "Returns the latest F3 finalized tipset, or falls back to EC finality if F3 is not operational on the node or if the F3 finalized tipset is further back than EC finalized tipset.",
163    );
164
165    type Params = ();
166    type Ok = Tipset;
167
168    async fn handle(
169        ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
170        (): Self::Params,
171        _: &http::Extensions,
172    ) -> Result<Self::Ok, ServerError> {
173        Ok(ChainGetTipSetV2::get_latest_finalized_tipset(&ctx).await?)
174    }
175}
176
177pub enum ChainGetMessage {}
178impl RpcMethod<1> for ChainGetMessage {
179    const NAME: &'static str = "Filecoin.ChainGetMessage";
180    const PARAM_NAMES: [&'static str; 1] = ["messageCid"];
181    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
182    const PERMISSION: Permission = Permission::Read;
183    const DESCRIPTION: Option<&'static str> = Some("Returns the message with the specified CID.");
184
185    type Params = (Cid,);
186    type Ok = Message;
187
188    async fn handle(
189        ctx: Ctx<impl Blockstore>,
190        (message_cid,): Self::Params,
191        _: &http::Extensions,
192    ) -> Result<Self::Ok, ServerError> {
193        let chain_message: ChainMessage = ctx
194            .store()
195            .get_cbor(&message_cid)?
196            .with_context(|| format!("can't find message with cid {message_cid}"))?;
197        let message = match chain_message {
198            ChainMessage::Signed(m) => Arc::unwrap_or_clone(m).into_message(),
199            ChainMessage::Unsigned(m) => Arc::unwrap_or_clone(m),
200        };
201
202        Ok(message)
203    }
204}
205
206/// Returns the events stored under the given event AMT root CID.
207/// Errors if the root CID cannot be found in the blockstore.
208pub enum ChainGetEvents {}
209impl RpcMethod<1> for ChainGetEvents {
210    const NAME: &'static str = "Filecoin.ChainGetEvents";
211    const PARAM_NAMES: [&'static str; 1] = ["rootCid"];
212    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
213    const PERMISSION: Permission = Permission::Read;
214    const DESCRIPTION: Option<&'static str> =
215        Some("Returns the events under the given event AMT root CID.");
216
217    type Params = (Cid,);
218    type Ok = Vec<Event>;
219    async fn handle(
220        ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
221        (root_cid,): Self::Params,
222        _: &http::Extensions,
223    ) -> Result<Self::Ok, ServerError> {
224        let events = EthEventHandler::get_events_by_event_root(&ctx, &root_cid)?;
225        Ok(events)
226    }
227}
228
229pub enum ChainGetParentMessages {}
230impl RpcMethod<1> for ChainGetParentMessages {
231    const NAME: &'static str = "Filecoin.ChainGetParentMessages";
232    const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
233    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
234    const PERMISSION: Permission = Permission::Read;
235    const DESCRIPTION: Option<&'static str> =
236        Some("Returns the messages included in the blocks of the parent tipset.");
237
238    type Params = (Cid,);
239    type Ok = Vec<ApiMessage>;
240
241    async fn handle(
242        ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
243        (block_cid,): Self::Params,
244        _: &http::Extensions,
245    ) -> Result<Self::Ok, ServerError> {
246        let store = ctx.store();
247        let block_header: CachingBlockHeader = store
248            .get_cbor(&block_cid)?
249            .with_context(|| format!("can't find block header with cid {block_cid}"))?;
250        if block_header.epoch == 0 {
251            Ok(vec![])
252        } else {
253            let parent_tipset = ctx
254                .chain_index()
255                .load_required_tipset(&block_header.parents)?;
256            load_api_messages_from_tipset(&ctx, parent_tipset.key()).await
257        }
258    }
259}
260
261pub enum ChainGetParentReceipts {}
262impl RpcMethod<1> for ChainGetParentReceipts {
263    const NAME: &'static str = "Filecoin.ChainGetParentReceipts";
264    const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
265    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
266    const PERMISSION: Permission = Permission::Read;
267    const DESCRIPTION: Option<&'static str> =
268        Some("Returns the message receipts included in the blocks of the parent tipset.");
269
270    type Params = (Cid,);
271    type Ok = Vec<ApiReceipt>;
272
273    async fn handle(
274        ctx: Ctx<impl Blockstore>,
275        (block_cid,): Self::Params,
276        _: &http::Extensions,
277    ) -> Result<Self::Ok, ServerError> {
278        let store = ctx.store();
279        let block_header: CachingBlockHeader = store
280            .get_cbor(&block_cid)?
281            .with_context(|| format!("can't find block header with cid {block_cid}"))?;
282        if block_header.epoch == 0 {
283            return Ok(vec![]);
284        }
285        let receipts = Receipt::get_receipts(store, block_header.message_receipts)
286            .map_err(|_| {
287                ErrorObjectOwned::owned::<()>(
288                    1,
289                    format!(
290                        "failed to root: ipld: could not find {}",
291                        block_header.message_receipts
292                    ),
293                    None,
294                )
295            })?
296            .iter()
297            .map(|r| ApiReceipt {
298                exit_code: r.exit_code().into(),
299                return_data: r.return_data(),
300                gas_used: r.gas_used(),
301                events_root: r.events_root(),
302            })
303            .collect_vec();
304
305        Ok(receipts)
306    }
307}
308
309pub enum ChainGetMessagesInTipset {}
310impl RpcMethod<1> for ChainGetMessagesInTipset {
311    const NAME: &'static str = "Filecoin.ChainGetMessagesInTipset";
312    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
313    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
314    const PERMISSION: Permission = Permission::Read;
315
316    type Params = (ApiTipsetKey,);
317    type Ok = Vec<ApiMessage>;
318
319    async fn handle(
320        ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
321        (ApiTipsetKey(tipset_key),): Self::Params,
322        _: &http::Extensions,
323    ) -> Result<Self::Ok, ServerError> {
324        let tipset = ctx
325            .chain_store()
326            .load_required_tipset_or_heaviest(&tipset_key)?;
327        load_api_messages_from_tipset(&ctx, tipset.key()).await
328    }
329}
330
331pub enum ChainPruneSnapshot {}
332impl RpcMethod<1> for ChainPruneSnapshot {
333    const NAME: &'static str = "Forest.SnapshotGC";
334    const PARAM_NAMES: [&'static str; 1] = ["blocking"];
335    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
336    const PERMISSION: Permission = Permission::Admin;
337
338    type Params = (bool,);
339    type Ok = ();
340
341    async fn handle(
342        _ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
343        (blocking,): Self::Params,
344        _: &http::Extensions,
345    ) -> Result<Self::Ok, ServerError> {
346        if let Some(gc) = crate::daemon::GLOBAL_SNAPSHOT_GC.get() {
347            let progress_rx = gc.trigger()?;
348            while blocking && progress_rx.recv_async().await.is_ok() {}
349            Ok(())
350        } else {
351            Err(anyhow::anyhow!("snapshot gc is not enabled").into())
352        }
353    }
354}
355
356pub enum ForestChainExport {}
357impl RpcMethod<1> for ForestChainExport {
358    const NAME: &'static str = "Forest.ChainExport";
359    const PARAM_NAMES: [&'static str; 1] = ["params"];
360    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
361    const PERMISSION: Permission = Permission::Read;
362
363    type Params = (ForestChainExportParams,);
364    type Ok = ApiExportResult;
365
366    async fn handle(
367        ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
368        (params,): Self::Params,
369        _: &http::Extensions,
370    ) -> Result<Self::Ok, ServerError> {
371        let ForestChainExportParams {
372            version,
373            epoch,
374            recent_roots,
375            output_path,
376            tipset_keys: ApiTipsetKey(tsk),
377            include_receipts,
378            include_events,
379            include_tipset_keys,
380            skip_checksum,
381            dry_run,
382        } = params;
383
384        let token = CancellationToken::new();
385        {
386            let mut guard = CHAIN_EXPORT_LOCK.lock().await;
387            if guard.is_some() {
388                return Err(
389                    anyhow::anyhow!("A chain export is still in progress. Cancel it with the export-cancel subcommand if needed.").into(),
390                );
391            }
392            *guard = Some(token.clone());
393        }
394        start_export();
395
396        let head = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?;
397        let start_ts = ctx.chain_index().load_required_tipset_by_height(
398            epoch,
399            head,
400            ResolveNullTipset::TakeOlder,
401        )?;
402
403        let options = ExportOptions {
404            skip_checksum,
405            include_receipts,
406            include_events,
407            include_tipset_keys,
408            seen: FileBackedCidHashSet::new(ctx.temp_dir.as_path())?,
409        };
410        let writer = if dry_run {
411            tokio_util::either::Either::Left(VoidAsyncWriter)
412        } else {
413            tokio_util::either::Either::Right(tokio::fs::File::create(&output_path).await?)
414        };
415        let result = match version {
416            FilecoinSnapshotVersion::V1 => {
417                let db = ctx.store_owned();
418
419                let chain_export = crate::chain::export::<Sha256, _>(
420                    &db,
421                    &start_ts,
422                    recent_roots,
423                    writer,
424                    options,
425                );
426
427                tokio::select! {
428                    result = chain_export => {
429                        result.map(|checksum_opt| ApiExportResult::Done(checksum_opt.map(|hash| hash.encode_hex())))
430                    },
431                    _ = token.cancelled() => {
432                        cancel_export();
433                        tracing::warn!("Snapshot export was cancelled");
434                        Ok(ApiExportResult::Cancelled)
435                    },
436                }
437            }
438            FilecoinSnapshotVersion::V2 => {
439                let db = ctx.store_owned();
440
441                let f3_snap_tmp_path = {
442                    let mut f3_snap_dir = output_path.clone();
443                    let mut builder = tempfile::Builder::new();
444                    let with_suffix = builder.suffix(".f3snap.bin");
445                    if f3_snap_dir.pop() {
446                        with_suffix.tempfile_in(&f3_snap_dir)
447                    } else {
448                        with_suffix.tempfile_in(".")
449                    }?
450                    .into_temp_path()
451                };
452                let f3_snap = {
453                    match F3ExportLatestSnapshot::run(f3_snap_tmp_path.display().to_string()).await
454                    {
455                        Ok(cid) => Some((cid, File::open(&f3_snap_tmp_path)?)),
456                        Err(e) => {
457                            tracing::error!("Failed to export F3 snapshot: {e:#}");
458                            None
459                        }
460                    }
461                };
462
463                let chain_export = crate::chain::export_v2::<Sha256, _, _>(
464                    &db,
465                    f3_snap,
466                    &start_ts,
467                    recent_roots,
468                    writer,
469                    options,
470                );
471
472                tokio::select! {
473                    result = chain_export => {
474                        result.map(|checksum_opt| ApiExportResult::Done(checksum_opt.map(|hash| hash.encode_hex())))
475                    },
476                    _ = token.cancelled() => {
477                        cancel_export();
478                        tracing::warn!("Snapshot export was cancelled");
479                        Ok(ApiExportResult::Cancelled)
480                    },
481                }
482            }
483        };
484        end_export();
485        // Clean up token
486        let mut guard = CHAIN_EXPORT_LOCK.lock().await;
487        *guard = None;
488        match result {
489            Ok(export_result) => Ok(export_result),
490            Err(e) => Err(anyhow::anyhow!(e).into()),
491        }
492    }
493}
494
495pub enum ForestChainExportStatus {}
496impl RpcMethod<0> for ForestChainExportStatus {
497    const NAME: &'static str = "Forest.ChainExportStatus";
498    const PARAM_NAMES: [&'static str; 0] = [];
499    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
500    const PERMISSION: Permission = Permission::Read;
501
502    type Params = ();
503    type Ok = ApiExportStatus;
504
505    async fn handle(
506        _ctx: Ctx<impl Blockstore>,
507        (): Self::Params,
508        _: &http::Extensions,
509    ) -> Result<Self::Ok, ServerError> {
510        let mutex = CHAIN_EXPORT_STATUS.lock();
511
512        let progress = if mutex.initial_epoch == 0 {
513            0.0
514        } else {
515            let p = 1.0 - ((mutex.epoch as f64) / (mutex.initial_epoch as f64));
516            if p.is_finite() {
517                p.clamp(0.0, 1.0)
518            } else {
519                0.0
520            }
521        };
522        // only two decimal places
523        let progress = (progress * 100.0).round() / 100.0;
524
525        let status = ApiExportStatus {
526            progress,
527            exporting: mutex.exporting,
528            cancelled: mutex.cancelled,
529            start_time: mutex.start_time,
530        };
531
532        Ok(status)
533    }
534}
535
536pub enum ForestChainExportCancel {}
537impl RpcMethod<0> for ForestChainExportCancel {
538    const NAME: &'static str = "Forest.ChainExportCancel";
539    const PARAM_NAMES: [&'static str; 0] = [];
540    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
541    const PERMISSION: Permission = Permission::Read;
542
543    type Params = ();
544    type Ok = bool;
545
546    async fn handle(
547        _ctx: Ctx<impl Blockstore>,
548        (): Self::Params,
549        _: &http::Extensions,
550    ) -> Result<Self::Ok, ServerError> {
551        if let Some(token) = CHAIN_EXPORT_LOCK.lock().await.as_ref() {
552            token.cancel();
553            return Ok(true);
554        }
555
556        Ok(false)
557    }
558}
559
560pub enum ForestChainExportDiff {}
561impl RpcMethod<1> for ForestChainExportDiff {
562    const NAME: &'static str = "Forest.ChainExportDiff";
563    const PARAM_NAMES: [&'static str; 1] = ["params"];
564    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
565    const PERMISSION: Permission = Permission::Read;
566
567    type Params = (ForestChainExportDiffParams,);
568    type Ok = ();
569
570    async fn handle(
571        ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
572        (params,): Self::Params,
573        _: &http::Extensions,
574    ) -> Result<Self::Ok, ServerError> {
575        let ForestChainExportDiffParams {
576            from,
577            to,
578            depth,
579            output_path,
580        } = params;
581
582        let _locked = CHAIN_EXPORT_LOCK.try_lock();
583        if _locked.is_err() {
584            return Err(
585                anyhow::anyhow!("Another chain export diff job is still in progress").into(),
586            );
587        }
588
589        let chain_finality = ctx.chain_config().policy.chain_finality;
590        if depth < chain_finality {
591            return Err(
592                anyhow::anyhow!(format!("depth must be greater than {chain_finality}")).into(),
593            );
594        }
595
596        let head = ctx.chain_store().heaviest_tipset();
597        let start_ts = ctx.chain_index().load_required_tipset_by_height(
598            from,
599            head,
600            ResolveNullTipset::TakeOlder,
601        )?;
602
603        crate::tool::subcommands::archive_cmd::do_export(
604            &ctx.store_owned(),
605            start_ts,
606            output_path,
607            None,
608            depth,
609            Some(to),
610            Some(chain_finality),
611            true,
612        )
613        .await?;
614
615        Ok(())
616    }
617}
618
619pub enum ChainExport {}
620impl RpcMethod<1> for ChainExport {
621    const NAME: &'static str = "Filecoin.ChainExport";
622    const PARAM_NAMES: [&'static str; 1] = ["params"];
623    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
624    const PERMISSION: Permission = Permission::Read;
625
626    type Params = (ChainExportParams,);
627    type Ok = ApiExportResult;
628
629    async fn handle(
630        ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
631        (ChainExportParams {
632            epoch,
633            recent_roots,
634            output_path,
635            tipset_keys,
636            skip_checksum,
637            dry_run,
638        },): Self::Params,
639        ext: &http::Extensions,
640    ) -> Result<Self::Ok, ServerError> {
641        ForestChainExport::handle(
642            ctx,
643            (ForestChainExportParams {
644                version: FilecoinSnapshotVersion::V1,
645                epoch,
646                recent_roots,
647                output_path,
648                tipset_keys,
649                include_receipts: false,
650                include_events: false,
651                include_tipset_keys: false,
652                skip_checksum,
653                dry_run,
654            },),
655            ext,
656        )
657        .await
658    }
659}
660
661pub enum ChainReadObj {}
662impl RpcMethod<1> for ChainReadObj {
663    const NAME: &'static str = "Filecoin.ChainReadObj";
664    const PARAM_NAMES: [&'static str; 1] = ["cid"];
665    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
666    const PERMISSION: Permission = Permission::Read;
667    const DESCRIPTION: Option<&'static str> = Some(
668        "Reads IPLD nodes referenced by the specified CID from the chain blockstore and returns raw bytes.",
669    );
670
671    type Params = (Cid,);
672    type Ok = Vec<u8>;
673
674    async fn handle(
675        ctx: Ctx<impl Blockstore>,
676        (cid,): Self::Params,
677        _: &http::Extensions,
678    ) -> Result<Self::Ok, ServerError> {
679        let bytes = ctx
680            .store()
681            .get(&cid)?
682            .with_context(|| format!("can't find object with cid={cid}"))?;
683        Ok(bytes)
684    }
685}
686
687pub enum ChainHasObj {}
688impl RpcMethod<1> for ChainHasObj {
689    const NAME: &'static str = "Filecoin.ChainHasObj";
690    const PARAM_NAMES: [&'static str; 1] = ["cid"];
691    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
692    const PERMISSION: Permission = Permission::Read;
693    const DESCRIPTION: Option<&'static str> =
694        Some("Checks if a given CID exists in the chain blockstore.");
695
696    type Params = (Cid,);
697    type Ok = bool;
698
699    async fn handle(
700        ctx: Ctx<impl Blockstore>,
701        (cid,): Self::Params,
702        _: &http::Extensions,
703    ) -> Result<Self::Ok, ServerError> {
704        Ok(ctx.store().get(&cid)?.is_some())
705    }
706}
707
708/// Returns statistics about the graph referenced by 'obj'.
709/// If 'base' is also specified, then the returned stat will be a diff between the two objects.
710pub enum ChainStatObj {}
711impl RpcMethod<2> for ChainStatObj {
712    const NAME: &'static str = "Filecoin.ChainStatObj";
713    const PARAM_NAMES: [&'static str; 2] = ["obj_cid", "base_cid"];
714    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
715    const PERMISSION: Permission = Permission::Read;
716
717    type Params = (Cid, Option<Cid>);
718    type Ok = ObjStat;
719
720    async fn handle(
721        ctx: Ctx<impl Blockstore>,
722        (obj_cid, base_cid): Self::Params,
723        _: &http::Extensions,
724    ) -> Result<Self::Ok, ServerError> {
725        let mut stats = ObjStat::default();
726        let mut seen = CidHashSet::default();
727        let mut walk = |cid, collect| {
728            let mut queue = VecDeque::new();
729            queue.push_back(cid);
730            while let Some(link_cid) = queue.pop_front() {
731                if !seen.insert(link_cid) {
732                    continue;
733                }
734                let data = ctx.store().get(&link_cid)?;
735                if let Some(data) = data {
736                    if collect {
737                        stats.links += 1;
738                        stats.size += data.len();
739                    }
740                    if matches!(link_cid.codec(), fvm_ipld_encoding::DAG_CBOR)
741                        && let Ok(ipld) =
742                            crate::utils::encoding::from_slice_with_fallback::<Ipld>(&data)
743                    {
744                        for ipld in DfsIter::new(ipld) {
745                            if let Ipld::Link(cid) = ipld {
746                                queue.push_back(cid);
747                            }
748                        }
749                    }
750                }
751            }
752            anyhow::Ok(())
753        };
754        if let Some(base_cid) = base_cid {
755            walk(base_cid, false)?;
756        }
757        walk(obj_cid, true)?;
758        Ok(stats)
759    }
760}
761
762pub enum ChainGetBlockMessages {}
763impl RpcMethod<1> for ChainGetBlockMessages {
764    const NAME: &'static str = "Filecoin.ChainGetBlockMessages";
765    const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
766    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
767    const PERMISSION: Permission = Permission::Read;
768    const DESCRIPTION: Option<&'static str> =
769        Some("Returns all messages from the specified block.");
770
771    type Params = (Cid,);
772    type Ok = BlockMessages;
773
774    async fn handle(
775        ctx: Ctx<impl Blockstore>,
776        (block_cid,): Self::Params,
777        _: &http::Extensions,
778    ) -> Result<Self::Ok, ServerError> {
779        let blk: CachingBlockHeader = ctx.store().get_cbor_required(&block_cid)?;
780        let (unsigned_cids, signed_cids) = crate::chain::read_msg_cids(ctx.store(), &blk)?;
781        let (bls_msg, secp_msg) =
782            crate::chain::block_messages_from_cids(ctx.store(), &unsigned_cids, &signed_cids)?;
783        let cids = unsigned_cids.into_iter().chain(signed_cids).collect();
784
785        let ret = BlockMessages {
786            bls_msg,
787            secp_msg,
788            cids,
789        };
790        Ok(ret)
791    }
792}
793
794pub enum ChainGetPath {}
795impl RpcMethod<2> for ChainGetPath {
796    const NAME: &'static str = "Filecoin.ChainGetPath";
797    const PARAM_NAMES: [&'static str; 2] = ["from", "to"];
798    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
799    const PERMISSION: Permission = Permission::Read;
800    const DESCRIPTION: Option<&'static str> =
801        Some("Returns the path between the two specified tipsets.");
802
803    type Params = (TipsetKey, TipsetKey);
804    type Ok = Vec<PathChange>;
805
806    async fn handle(
807        ctx: Ctx<impl Blockstore>,
808        (from, to): Self::Params,
809        _: &http::Extensions,
810    ) -> Result<Self::Ok, ServerError> {
811        Ok(chain_get_path(ctx.chain_store(), &from, &to)?.into_change_vec())
812    }
813}
814
815/// Find the path between two tipsets, as [`PathChanges`].
816///
817/// ```text
818/// 0 - A - B - C - D
819///     ^~~~~~~~> apply B, C
820///
821/// 0 - A - B - C - D
822///     <~~~~~~~^ revert C, B
823///
824///     <~~~~~~~~ revert C, B
825/// 0 - A - B  - C
826///     |
827///      -- B' - C'
828///      ~~~~~~~~> then apply B', C'
829/// ```
830///
831/// Exposes errors from the [`Blockstore`], and returns an error if there is no common ancestor.
832pub fn chain_get_path(
833    chain_store: &ChainStore<impl Blockstore>,
834    from: &TipsetKey,
835    to: &TipsetKey,
836) -> anyhow::Result<PathChanges> {
837    let finality = chain_store.chain_config().policy.chain_finality;
838    let mut to_revert = chain_store
839        .load_required_tipset_or_heaviest(from)
840        .context("couldn't load `from`")?;
841    let mut to_apply = chain_store
842        .load_required_tipset_or_heaviest(to)
843        .context("couldn't load `to`")?;
844
845    anyhow::ensure!(
846        (to_apply.epoch() - to_revert.epoch()).abs() <= finality,
847        "the gap between the new head ({}) and the old head ({}) is larger than chain finality ({finality})",
848        to_apply.epoch(),
849        to_revert.epoch()
850    );
851
852    let mut reverts = vec![];
853    let mut applies = vec![];
854
855    // This loop is guaranteed to terminate if the blockstore contain no cycles.
856    // This is currently computationally infeasible.
857    while to_revert != to_apply {
858        if to_revert.epoch() > to_apply.epoch() {
859            let next = chain_store
860                .load_required_tipset_or_heaviest(to_revert.parents())
861                .context("couldn't load ancestor of `from`")?;
862            reverts.push(to_revert);
863            to_revert = next;
864        } else {
865            let next = chain_store
866                .load_required_tipset_or_heaviest(to_apply.parents())
867                .context("couldn't load ancestor of `to`")?;
868            applies.push(to_apply);
869            to_apply = next;
870        }
871    }
872    applies.reverse();
873    Ok(PathChanges { reverts, applies })
874}
875
876/// Get tipset at epoch. Pick younger tipset if epoch points to a
877/// null-tipset. Only tipsets below the given `head` are searched. If `head`
878/// is null, the node will use the heaviest tipset.
879pub enum ChainGetTipSetByHeight {}
880impl RpcMethod<2> for ChainGetTipSetByHeight {
881    const NAME: &'static str = "Filecoin.ChainGetTipSetByHeight";
882    const PARAM_NAMES: [&'static str; 2] = ["height", "tipsetKey"];
883    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
884    const PERMISSION: Permission = Permission::Read;
885    const DESCRIPTION: Option<&'static str> = Some("Returns the tipset at the specified height.");
886
887    type Params = (ChainEpoch, ApiTipsetKey);
888    type Ok = Tipset;
889
890    async fn handle(
891        ctx: Ctx<impl Blockstore + EthMappingsStore>,
892        (height, ApiTipsetKey(tipset_key)): Self::Params,
893        _: &http::Extensions,
894    ) -> Result<Self::Ok, ServerError> {
895        let ts = ctx
896            .chain_store()
897            .load_required_tipset_or_heaviest(&tipset_key)?;
898        let tss = ctx.chain_index().load_required_tipset_by_height(
899            height,
900            ts,
901            ResolveNullTipset::TakeOlder,
902        )?;
903        Ok(tss)
904    }
905}
906
907pub enum ChainGetTipSetAfterHeight {}
908impl RpcMethod<2> for ChainGetTipSetAfterHeight {
909    const NAME: &'static str = "Filecoin.ChainGetTipSetAfterHeight";
910    const PARAM_NAMES: [&'static str; 2] = ["height", "tipsetKey"];
911    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
912    const PERMISSION: Permission = Permission::Read;
913    const DESCRIPTION: Option<&'static str> = Some(
914        "Looks back and returns the tipset at the specified epoch.
915    If there are no blocks at the given epoch,
916    returns the first non-nil tipset at a later epoch.",
917    );
918
919    type Params = (ChainEpoch, ApiTipsetKey);
920    type Ok = Tipset;
921
922    async fn handle(
923        ctx: Ctx<impl Blockstore + EthMappingsStore>,
924        (height, ApiTipsetKey(tipset_key)): Self::Params,
925        _: &http::Extensions,
926    ) -> Result<Self::Ok, ServerError> {
927        let ts = ctx
928            .chain_store()
929            .load_required_tipset_or_heaviest(&tipset_key)?;
930        let tss = ctx.chain_index().load_required_tipset_by_height(
931            height,
932            ts,
933            ResolveNullTipset::TakeNewer,
934        )?;
935        Ok(tss)
936    }
937}
938
939pub enum ChainGetGenesis {}
940impl RpcMethod<0> for ChainGetGenesis {
941    const NAME: &'static str = "Filecoin.ChainGetGenesis";
942    const PARAM_NAMES: [&'static str; 0] = [];
943    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
944    const PERMISSION: Permission = Permission::Read;
945
946    type Params = ();
947    type Ok = Option<Tipset>;
948
949    async fn handle(
950        ctx: Ctx<impl Blockstore>,
951        (): Self::Params,
952        _: &http::Extensions,
953    ) -> Result<Self::Ok, ServerError> {
954        let genesis = ctx.chain_store().genesis_block_header();
955        Ok(Some(Tipset::from(genesis)))
956    }
957}
958
959pub enum ChainHead {}
960impl RpcMethod<0> for ChainHead {
961    const NAME: &'static str = "Filecoin.ChainHead";
962    const PARAM_NAMES: [&'static str; 0] = [];
963    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
964    const PERMISSION: Permission = Permission::Read;
965    const DESCRIPTION: Option<&'static str> = Some("Returns the chain head (heaviest tipset).");
966
967    type Params = ();
968    type Ok = Tipset;
969
970    async fn handle(
971        ctx: Ctx<impl Blockstore>,
972        (): Self::Params,
973        _: &http::Extensions,
974    ) -> Result<Self::Ok, ServerError> {
975        let heaviest = ctx.chain_store().heaviest_tipset();
976        Ok(heaviest)
977    }
978}
979
980pub enum ChainGetBlock {}
981impl RpcMethod<1> for ChainGetBlock {
982    const NAME: &'static str = "Filecoin.ChainGetBlock";
983    const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
984    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
985    const PERMISSION: Permission = Permission::Read;
986    const DESCRIPTION: Option<&'static str> = Some("Returns the block with the specified CID.");
987
988    type Params = (Cid,);
989    type Ok = CachingBlockHeader;
990
991    async fn handle(
992        ctx: Ctx<impl Blockstore>,
993        (block_cid,): Self::Params,
994        _: &http::Extensions,
995    ) -> Result<Self::Ok, ServerError> {
996        let blk: CachingBlockHeader = ctx.store().get_cbor_required(&block_cid)?;
997        Ok(blk)
998    }
999}
1000
1001pub enum ChainGetTipSet {}
1002
1003impl RpcMethod<1> for ChainGetTipSet {
1004    const NAME: &'static str = "Filecoin.ChainGetTipSet";
1005    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1006    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V0 | V1 });
1007    const PERMISSION: Permission = Permission::Read;
1008    const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID.");
1009
1010    type Params = (ApiTipsetKey,);
1011    type Ok = Tipset;
1012
1013    async fn handle(
1014        ctx: Ctx<impl Blockstore>,
1015        (ApiTipsetKey(tsk),): Self::Params,
1016        _: &http::Extensions,
1017    ) -> Result<Self::Ok, ServerError> {
1018        if let Some(tsk) = &tsk {
1019            let ts = ctx.chain_index().load_required_tipset(tsk)?;
1020            Ok(ts)
1021        } else {
1022            // It contains Lotus error message `NewTipSet called with zero length array of blocks` for parity tests
1023            Err(anyhow::anyhow!(
1024                "TipsetKey cannot be empty (NewTipSet called with zero length array of blocks)"
1025            )
1026            .into())
1027        }
1028    }
1029}
1030
1031pub enum ChainGetTipSetV2 {}
1032
1033impl ChainGetTipSetV2 {
1034    pub async fn get_tipset_by_anchor(
1035        ctx: &Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1036        anchor: Option<&TipsetAnchor>,
1037    ) -> anyhow::Result<Tipset> {
1038        if let Some(anchor) = anchor {
1039            match (&anchor.key.0, &anchor.tag) {
1040                // Anchor is zero-valued. Fall back to heaviest tipset.
1041                (None, None) => Ok(ctx.state_manager.heaviest_tipset()),
1042                // Get tipset at the specified key.
1043                (Some(tsk), None) => Ok(ctx.chain_index().load_required_tipset(tsk)?),
1044                (None, Some(tag)) => Self::get_tipset_by_tag(ctx, *tag).await,
1045                _ => {
1046                    anyhow::bail!("invalid anchor")
1047                }
1048            }
1049        } else {
1050            // No anchor specified. Fall back to finalized tipset.
1051            Self::get_tipset_by_tag(ctx, TipsetTag::Finalized).await
1052        }
1053    }
1054
1055    pub async fn get_tipset_by_tag(
1056        ctx: &Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1057        tag: TipsetTag,
1058    ) -> anyhow::Result<Tipset> {
1059        match tag {
1060            TipsetTag::Latest => Ok(ctx.state_manager.heaviest_tipset()),
1061            TipsetTag::Finalized => Self::get_latest_finalized_tipset(ctx).await,
1062            TipsetTag::Safe => Self::get_latest_safe_tipset(ctx).await,
1063        }
1064    }
1065
1066    pub async fn get_latest_safe_tipset(
1067        ctx: &Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1068    ) -> anyhow::Result<Tipset> {
1069        let finalized = Self::get_latest_finalized_tipset(ctx).await?;
1070        let head = ctx.chain_store().heaviest_tipset();
1071        let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0);
1072        if finalized.epoch() >= safe_height {
1073            Ok(finalized)
1074        } else {
1075            Ok(ctx.chain_index().load_required_tipset_by_height(
1076                safe_height,
1077                head,
1078                ResolveNullTipset::TakeOlder,
1079            )?)
1080        }
1081    }
1082
1083    pub async fn get_latest_finalized_tipset(
1084        ctx: &Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1085    ) -> anyhow::Result<Tipset> {
1086        ChainGetTipSetFinalityStatus::get_finality_status(ctx)?
1087            .finalized_tip_set
1088            .context("failed to resolve finalized tipset")
1089    }
1090
1091    pub async fn get_tipset(
1092        ctx: &Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1093        selector: &TipsetSelector,
1094    ) -> anyhow::Result<Tipset> {
1095        selector.validate()?;
1096        // Get tipset by key.
1097        if let ApiTipsetKey(Some(tsk)) = &selector.key {
1098            let ts = ctx.chain_index().load_required_tipset(tsk)?;
1099            return Ok(ts);
1100        }
1101        // Get tipset by height.
1102        if let Some(height) = &selector.height {
1103            let anchor = Self::get_tipset_by_anchor(ctx, height.anchor.as_ref()).await?;
1104            let ts = ctx.chain_index().load_required_tipset_by_height(
1105                height.at,
1106                anchor,
1107                height.resolve_null_tipset_policy(),
1108            )?;
1109            return Ok(ts);
1110        }
1111        // Get tipset by tag, either latest or finalized.
1112        if let Some(tag) = &selector.tag {
1113            let ts = Self::get_tipset_by_tag(ctx, *tag).await?;
1114            return Ok(ts);
1115        }
1116        anyhow::bail!("no tipset found for selector")
1117    }
1118}
1119
1120impl RpcMethod<1> for ChainGetTipSetV2 {
1121    const NAME: &'static str = "Filecoin.ChainGetTipSet";
1122    const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"];
1123    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1124    const PERMISSION: Permission = Permission::Read;
1125    const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID.");
1126
1127    type Params = (TipsetSelector,);
1128    type Ok = Tipset;
1129
1130    async fn handle(
1131        ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1132        (selector,): Self::Params,
1133        _: &http::Extensions,
1134    ) -> Result<Self::Ok, ServerError> {
1135        Ok(Self::get_tipset(&ctx, &selector).await?)
1136    }
1137}
1138
1139pub enum ChainGetTipSetFinalityStatus {}
1140
1141impl ChainGetTipSetFinalityStatus {
1142    pub fn get_finality_status(
1143        ctx: &Ctx<impl Blockstore + EthMappingsStore>,
1144    ) -> anyhow::Result<ChainFinalityStatus> {
1145        let head = ctx.chain_store().heaviest_tipset();
1146        let (ec_finality_threshold_depth, ec_finalized_tip_set) =
1147            Self::get_ec_finality_threshold_depth_and_tipset_with_cache(ctx, head.shallow_clone())?;
1148        let f3_finalized_tip_set = ctx.chain_store().f3_finalized_tipset();
1149        let finalized_tip_set = match (&ec_finalized_tip_set, &f3_finalized_tip_set) {
1150            (Some(ec), Some(f3)) => {
1151                if ec.epoch() >= f3.epoch() {
1152                    Some(ec.shallow_clone())
1153                } else {
1154                    Some(f3.shallow_clone())
1155                }
1156            }
1157            (Some(ec), None) => Some(ec.shallow_clone()),
1158            (None, Some(f3)) => Some(f3.shallow_clone()),
1159            (None, None) => None,
1160        };
1161        Ok(ChainFinalityStatus {
1162            ec_finality_threshold_depth,
1163            ec_finalized_tip_set,
1164            f3_finalized_tip_set,
1165            finalized_tip_set,
1166            head,
1167        })
1168    }
1169
1170    pub fn get_ec_finality_threshold_depth_and_tipset_with_cache(
1171        ctx: &Ctx<impl Blockstore + EthMappingsStore>,
1172        head: Tipset,
1173    ) -> anyhow::Result<(i64, Option<Tipset>)> {
1174        static CACHE: parking_lot::Mutex<Option<(Tipset, i64, Option<Tipset>)>> =
1175            parking_lot::Mutex::new(None);
1176        let mut cache = CACHE.lock();
1177        if let Some((cached_head, cached_threshold, cached_tipset)) = &*cache
1178            && cached_head == &head
1179        {
1180            Ok((*cached_threshold, cached_tipset.shallow_clone()))
1181        } else {
1182            let (threshold, tipset) =
1183                Self::get_ec_finality_threshold_depth_and_tipset(ctx, head.shallow_clone())?;
1184            *cache = Some((head, threshold, tipset.shallow_clone()));
1185            Ok((threshold, tipset))
1186        }
1187    }
1188
1189    fn get_ec_finality_threshold_depth_and_tipset(
1190        ctx: &Ctx<impl Blockstore + EthMappingsStore>,
1191        head: Tipset,
1192    ) -> anyhow::Result<(i64, Option<Tipset>)> {
1193        use crate::chain::ec_finality::calculator::{
1194            DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE,
1195            find_threshold_depth,
1196        };
1197
1198        /// Number of extra epochs to fetch beyond [`chain_finality`] when
1199        /// building the chain sample for [`find_threshold_depth`].
1200        ///
1201        /// The extra 5 epochs act as a tail buffer to prevent out-of-bounds access,
1202        /// particularly when null rounds (epochs with zero blocks) are present, since
1203        /// they consume array slots without advancing the meaningful epoch count.
1204        const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5;
1205
1206        let finality = ctx.chain_config().policy.chain_finality;
1207        let chain_len = finality as usize + FINALITY_CHAIN_EXTRA_EPOCHS;
1208        let mut chain = Vec::with_capacity(chain_len);
1209        let mut ts = head.shallow_clone();
1210        while chain.len() < chain_len {
1211            chain.push(ts.len() as i64);
1212            if let Ok(parent) = ctx.chain_index().load_required_tipset(ts.parents()) {
1213                // insert 0 for null rounds
1214                if let Ok(n_null_tipsets_to_pad) = usize::try_from(ts.epoch() - parent.epoch() - 1)
1215                    && n_null_tipsets_to_pad > 0
1216                {
1217                    let target_len =
1218                        (chain.len().saturating_add(n_null_tipsets_to_pad)).min(chain_len);
1219                    chain.resize(target_len, 0);
1220                }
1221                ts = parent;
1222            } else {
1223                break;
1224            }
1225        }
1226        // Reverse to chronological order (oldest first).
1227        chain.reverse();
1228        let depth = match find_threshold_depth(
1229            &chain,
1230            finality,
1231            DEFAULT_BLOCKS_PER_EPOCH,
1232            DEFAULT_BYZANTINE_FRACTION,
1233            *DEFAULT_GUARANTEE,
1234        ) {
1235            Ok(threshold) => threshold,
1236            Err(e) => {
1237                tracing::error!(
1238                    "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}"
1239                );
1240                -1
1241            }
1242        };
1243        let finalized = if depth >= 0
1244            && let Ok(Some(ts)) = ctx.chain_index().tipset_by_height(
1245                (head.epoch() - depth).max(0),
1246                head.shallow_clone(),
1247                ResolveNullTipset::TakeOlder,
1248            ) {
1249            Some(ts)
1250        } else {
1251            let ec_finality_epoch =
1252                (head.epoch() - ctx.chain_config().policy.chain_finality).max(0);
1253            ctx.chain_index().tipset_by_height(
1254                ec_finality_epoch,
1255                head,
1256                ResolveNullTipset::TakeOlder,
1257            )?
1258        };
1259        Ok((depth, finalized))
1260    }
1261}
1262
1263impl RpcMethod<0> for ChainGetTipSetFinalityStatus {
1264    const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus";
1265    const PARAM_NAMES: [&'static str; 0] = [];
1266    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1267    const PERMISSION: Permission = Permission::Read;
1268    const DESCRIPTION: Option<&'static str> =
1269        Some("Returns a breakdown of how the node is currently determining finality.");
1270
1271    type Params = ();
1272    type Ok = ChainFinalityStatus;
1273
1274    async fn handle(
1275        ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1276        (): Self::Params,
1277        _: &http::Extensions,
1278    ) -> Result<Self::Ok, ServerError> {
1279        Ok(Self::get_finality_status(&ctx)?)
1280    }
1281}
1282
1283pub enum ChainSetHead {}
1284impl RpcMethod<1> for ChainSetHead {
1285    const NAME: &'static str = "Filecoin.ChainSetHead";
1286    const PARAM_NAMES: [&'static str; 1] = ["tsk"];
1287    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1288    const PERMISSION: Permission = Permission::Admin;
1289
1290    type Params = (TipsetKey,);
1291    type Ok = ();
1292
1293    async fn handle(
1294        ctx: Ctx<impl Blockstore>,
1295        (tsk,): Self::Params,
1296        _: &http::Extensions,
1297    ) -> Result<Self::Ok, ServerError> {
1298        // This is basically a port of the reference implementation at
1299        // https://github.com/filecoin-project/lotus/blob/v1.23.0/node/impl/full/chain.go#L321
1300
1301        let new_head = ctx.chain_index().load_required_tipset(&tsk)?;
1302        let mut current = ctx.chain_store().heaviest_tipset();
1303        while current.epoch() >= new_head.epoch() {
1304            for cid in current.key().to_cids() {
1305                ctx.chain_store().unmark_block_as_validated(&cid);
1306            }
1307            let parents = &current.block_headers().first().parents;
1308            current = ctx.chain_index().load_required_tipset(parents)?;
1309        }
1310        ctx.chain_store()
1311            .set_heaviest_tipset(new_head)
1312            .map_err(Into::into)
1313    }
1314}
1315
1316pub enum ChainGetMinBaseFee {}
1317impl RpcMethod<1> for ChainGetMinBaseFee {
1318    const NAME: &'static str = "Forest.ChainGetMinBaseFee";
1319    const PARAM_NAMES: [&'static str; 1] = ["lookback"];
1320    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1321    const PERMISSION: Permission = Permission::Read;
1322
1323    type Params = (u32,);
1324    type Ok = String;
1325
1326    async fn handle(
1327        ctx: Ctx<impl Blockstore>,
1328        (lookback,): Self::Params,
1329        _: &http::Extensions,
1330    ) -> Result<Self::Ok, ServerError> {
1331        let mut current = ctx.chain_store().heaviest_tipset();
1332        let mut min_base_fee = current.block_headers().first().parent_base_fee.clone();
1333
1334        for _ in 0..lookback {
1335            let parents = &current.block_headers().first().parents;
1336            current = ctx.chain_index().load_required_tipset(parents)?;
1337
1338            min_base_fee =
1339                min_base_fee.min(current.block_headers().first().parent_base_fee.to_owned());
1340        }
1341
1342        Ok(min_base_fee.atto().to_string())
1343    }
1344}
1345
1346pub enum ChainTipSetWeight {}
1347impl RpcMethod<1> for ChainTipSetWeight {
1348    const NAME: &'static str = "Filecoin.ChainTipSetWeight";
1349    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1350    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1351    const PERMISSION: Permission = Permission::Read;
1352    const DESCRIPTION: Option<&'static str> = Some("Returns the weight of the specified tipset.");
1353
1354    type Params = (ApiTipsetKey,);
1355    type Ok = BigInt;
1356
1357    async fn handle(
1358        ctx: Ctx<impl Blockstore>,
1359        (ApiTipsetKey(tipset_key),): Self::Params,
1360        _: &http::Extensions,
1361    ) -> Result<Self::Ok, ServerError> {
1362        let ts = ctx
1363            .chain_store()
1364            .load_required_tipset_or_heaviest(&tipset_key)?;
1365        let weight = crate::fil_cns::weight(ctx.store(), &ts)?;
1366        Ok(weight)
1367    }
1368}
1369
1370pub enum ChainGetTipsetByParentState {}
1371impl RpcMethod<1> for ChainGetTipsetByParentState {
1372    const NAME: &'static str = "Forest.ChainGetTipsetByParentState";
1373    const PARAM_NAMES: [&'static str; 1] = ["parentState"];
1374    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1375    const PERMISSION: Permission = Permission::Read;
1376
1377    type Params = (Cid,);
1378    type Ok = Option<Tipset>;
1379
1380    async fn handle(
1381        ctx: Ctx<impl Blockstore>,
1382        (parent_state,): Self::Params,
1383        _: &http::Extensions,
1384    ) -> Result<Self::Ok, ServerError> {
1385        Ok(ctx
1386            .chain_store()
1387            .heaviest_tipset()
1388            .chain(ctx.store())
1389            .find(|ts| ts.parent_state() == &parent_state)
1390            .shallow_clone())
1391    }
1392}
1393
1394pub const CHAIN_NOTIFY: &str = "Filecoin.ChainNotify";
1395pub(crate) fn chain_notify<DB: Blockstore>(
1396    _params: Params<'_>,
1397    data: &crate::rpc::RPCState<DB>,
1398) -> Subscriber<Vec<ApiHeadChange>> {
1399    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
1400
1401    // As soon as the channel is created, send the current tipset
1402    let current = data.chain_store().heaviest_tipset();
1403    let (change, tipset) = ("current".into(), current);
1404    sender
1405        .send(vec![ApiHeadChange { change, tipset }])
1406        .expect("receiver is not dropped");
1407
1408    let mut head_changes_rx = data.chain_store().subscribe_head_changes();
1409
1410    tokio::spawn(async move {
1411        // Skip first message
1412        let _ = head_changes_rx.recv().await;
1413        while let Ok(changes) = head_changes_rx.recv().await {
1414            let api_changes = changes
1415                .into_change_vec()
1416                .into_iter()
1417                .map(From::from)
1418                .collect();
1419            if sender.send(api_changes).is_err() {
1420                break;
1421            }
1422        }
1423    });
1424    receiver
1425}
1426
1427async fn load_api_messages_from_tipset<DB: Blockstore + Send + Sync + 'static>(
1428    ctx: &crate::rpc::RPCState<DB>,
1429    tipset_keys: &TipsetKey,
1430) -> Result<Vec<ApiMessage>, ServerError> {
1431    static SHOULD_BACKFILL: LazyLock<bool> = LazyLock::new(|| {
1432        let enabled = is_env_truthy("FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK");
1433        if enabled {
1434            tracing::warn!(
1435                "Full tipset backfilling from network is enabled via FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK, excessive disk and bandwidth usage is expected."
1436            );
1437        }
1438        enabled
1439    });
1440    let full_tipset = if *SHOULD_BACKFILL {
1441        get_full_tipset(
1442            &ctx.sync_network_context,
1443            ctx.chain_store(),
1444            None,
1445            tipset_keys,
1446        )
1447        .await?
1448    } else {
1449        load_full_tipset(ctx.chain_store(), tipset_keys)?
1450    };
1451    let blocks = full_tipset.into_blocks();
1452    let mut messages = vec![];
1453    let mut seen = CidHashSet::default();
1454    for Block {
1455        bls_messages,
1456        secp_messages,
1457        ..
1458    } in blocks
1459    {
1460        for message in bls_messages {
1461            let cid = message.cid();
1462            if seen.insert(cid) {
1463                messages.push(ApiMessage { cid, message });
1464            }
1465        }
1466
1467        for msg in secp_messages {
1468            let cid = msg.cid();
1469            if seen.insert(cid) {
1470                messages.push(ApiMessage {
1471                    cid,
1472                    message: msg.message,
1473                });
1474            }
1475        }
1476    }
1477
1478    Ok(messages)
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1482pub struct BlockMessages {
1483    #[serde(rename = "BlsMessages", with = "crate::lotus_json")]
1484    #[schemars(with = "LotusJson<Vec<Message>>")]
1485    pub bls_msg: Vec<Message>,
1486    #[serde(rename = "SecpkMessages", with = "crate::lotus_json")]
1487    #[schemars(with = "LotusJson<Vec<SignedMessage>>")]
1488    pub secp_msg: Vec<SignedMessage>,
1489    #[serde(rename = "Cids", with = "crate::lotus_json")]
1490    #[schemars(with = "LotusJson<Vec<Cid>>")]
1491    pub cids: Vec<Cid>,
1492}
1493lotus_json_with_self!(BlockMessages);
1494
1495#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
1496#[serde(rename_all = "PascalCase")]
1497pub struct ApiReceipt {
1498    // Exit status of message execution
1499    pub exit_code: ExitCode,
1500    // `Return` value if the exit code is zero
1501    #[serde(rename = "Return", with = "crate::lotus_json")]
1502    #[schemars(with = "LotusJson<RawBytes>")]
1503    pub return_data: RawBytes,
1504    // Non-negative value of GasUsed
1505    pub gas_used: u64,
1506    #[serde(with = "crate::lotus_json")]
1507    #[schemars(with = "LotusJson<Option<Cid>>")]
1508    pub events_root: Option<Cid>,
1509}
1510
1511lotus_json_with_self!(ApiReceipt);
1512
1513#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
1514#[serde(rename_all = "PascalCase")]
1515pub struct ApiMessage {
1516    #[serde(with = "crate::lotus_json")]
1517    #[schemars(with = "LotusJson<Cid>")]
1518    pub cid: Cid,
1519    #[serde(with = "crate::lotus_json")]
1520    #[schemars(with = "LotusJson<Message>")]
1521    pub message: Message,
1522}
1523
1524lotus_json_with_self!(ApiMessage);
1525
1526#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1527pub struct ForestChainExportParams {
1528    pub version: FilecoinSnapshotVersion,
1529    pub epoch: ChainEpoch,
1530    pub recent_roots: i64,
1531    pub output_path: PathBuf,
1532    #[schemars(with = "LotusJson<ApiTipsetKey>")]
1533    #[serde(with = "crate::lotus_json")]
1534    pub tipset_keys: ApiTipsetKey,
1535    #[serde(default)]
1536    pub include_receipts: bool,
1537    #[serde(default)]
1538    pub include_events: bool,
1539    #[serde(default)]
1540    pub include_tipset_keys: bool,
1541    pub skip_checksum: bool,
1542    pub dry_run: bool,
1543}
1544lotus_json_with_self!(ForestChainExportParams);
1545
1546#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1547pub struct ForestChainExportDiffParams {
1548    pub from: ChainEpoch,
1549    pub to: ChainEpoch,
1550    pub depth: i64,
1551    pub output_path: PathBuf,
1552}
1553lotus_json_with_self!(ForestChainExportDiffParams);
1554
1555#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1556pub struct ChainExportParams {
1557    pub epoch: ChainEpoch,
1558    pub recent_roots: i64,
1559    pub output_path: PathBuf,
1560    #[schemars(with = "LotusJson<ApiTipsetKey>")]
1561    #[serde(with = "crate::lotus_json")]
1562    pub tipset_keys: ApiTipsetKey,
1563    pub skip_checksum: bool,
1564    pub dry_run: bool,
1565}
1566lotus_json_with_self!(ChainExportParams);
1567
1568#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, JsonSchema)]
1569#[serde(rename_all = "PascalCase")]
1570pub struct ApiHeadChange {
1571    #[serde(rename = "Type")]
1572    pub change: String,
1573    #[serde(rename = "Val", with = "crate::lotus_json")]
1574    #[schemars(with = "LotusJson<Tipset>")]
1575    pub tipset: Tipset,
1576}
1577lotus_json_with_self!(ApiHeadChange);
1578
1579impl From<HeadChange> for ApiHeadChange {
1580    fn from(change: HeadChange) -> Self {
1581        match change {
1582            HeadChange::Apply(tipset) => Self {
1583                change: "apply".into(),
1584                tipset,
1585            },
1586            HeadChange::Revert(tipset) => Self {
1587                change: "revert".into(),
1588                tipset,
1589            },
1590        }
1591    }
1592}
1593
1594#[derive(PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
1595#[serde(tag = "Type", content = "Val", rename_all = "snake_case")]
1596pub enum PathChange<T = Tipset> {
1597    Revert(T),
1598    Apply(T),
1599}
1600
1601impl<T: Clone> Clone for PathChange<T> {
1602    fn clone(&self) -> Self {
1603        match self {
1604            Self::Revert(i) => Self::Revert(i.clone()),
1605            Self::Apply(i) => Self::Apply(i.clone()),
1606        }
1607    }
1608}
1609
1610impl HasLotusJson for PathChange {
1611    type LotusJson = PathChange<<Tipset as HasLotusJson>::LotusJson>;
1612
1613    #[cfg(test)]
1614    fn snapshots() -> Vec<(serde_json::Value, Self)> {
1615        use serde_json::json;
1616        vec![(
1617            json!({
1618                "Type": "revert",
1619                "Val": {
1620                    "Blocks": [
1621                        {
1622                            "BeaconEntries": null,
1623                            "ForkSignaling": 0,
1624                            "Height": 0,
1625                            "Messages": { "/": "baeaaaaa" },
1626                            "Miner": "f00",
1627                            "ParentBaseFee": "0",
1628                            "ParentMessageReceipts": { "/": "baeaaaaa" },
1629                            "ParentStateRoot": { "/":"baeaaaaa" },
1630                            "ParentWeight": "0",
1631                            "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
1632                            "Timestamp": 0,
1633                            "WinPoStProof": null
1634                        }
1635                    ],
1636                    "Cids": [
1637                        { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
1638                    ],
1639                    "Height": 0
1640                }
1641            }),
1642            Self::Revert(RawBlockHeader::default().into()),
1643        )]
1644    }
1645
1646    fn into_lotus_json(self) -> Self::LotusJson {
1647        match self {
1648            PathChange::Revert(it) => PathChange::Revert(it.into_lotus_json()),
1649            PathChange::Apply(it) => PathChange::Apply(it.into_lotus_json()),
1650        }
1651    }
1652
1653    fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
1654        match lotus_json {
1655            PathChange::Revert(it) => PathChange::Revert(Tipset::from_lotus_json(it)),
1656            PathChange::Apply(it) => PathChange::Apply(Tipset::from_lotus_json(it)),
1657        }
1658    }
1659}
1660
1661#[derive(Debug)]
1662pub struct PathChanges<T = Tipset> {
1663    pub reverts: Vec<T>,
1664    pub applies: Vec<T>,
1665}
1666
1667impl<T: Clone> Clone for PathChanges<T> {
1668    fn clone(&self) -> Self {
1669        let Self { reverts, applies } = self;
1670        Self {
1671            reverts: reverts.clone(),
1672            applies: applies.clone(),
1673        }
1674    }
1675}
1676
1677impl<T> PathChanges<T> {
1678    pub fn into_change_vec(self) -> Vec<PathChange<T>> {
1679        let Self { reverts, applies } = self;
1680        reverts
1681            .into_iter()
1682            .map(PathChange::Revert)
1683            .chain(applies.into_iter().map(PathChange::Apply))
1684            .collect_vec()
1685    }
1686}
1687
1688#[cfg(test)]
1689impl<T> quickcheck::Arbitrary for PathChange<T>
1690where
1691    T: quickcheck::Arbitrary + ShallowClone,
1692{
1693    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
1694        let inner = T::arbitrary(g);
1695        g.choose(&[PathChange::Apply(inner.clone()), PathChange::Revert(inner)])
1696            .unwrap()
1697            .clone()
1698    }
1699}
1700
1701#[test]
1702fn snapshots() {
1703    assert_all_snapshots::<PathChange>()
1704}
1705
1706#[cfg(test)]
1707#[quickcheck_macros::quickcheck]
1708fn quickcheck(val: PathChange) {
1709    assert_unchanged_via_json(val)
1710}
1711
1712#[cfg(test)]
1713mod tests {
1714    use super::*;
1715    use crate::{
1716        blocks::{Chain4U, RawBlockHeader, chain4u},
1717        db::{
1718            MemoryDB,
1719            car::{AnyCar, ManyCar},
1720        },
1721        networks::{self, ChainConfig},
1722    };
1723    use PathChange::{Apply, Revert};
1724    use std::sync::Arc;
1725
1726    #[test]
1727    fn revert_to_ancestor_linear() {
1728        let store = ChainStore::calibnet();
1729        chain4u! {
1730            in store.blockstore();
1731            [_genesis = store.genesis_block_header()]
1732            -> [a] -> [b] -> [c, d] -> [e]
1733        };
1734
1735        // simple
1736        assert_path_change(&store, b, a, [Revert(&[b])]);
1737
1738        // from multi-member tipset
1739        assert_path_change(&store, [c, d], a, [Revert(&[c, d][..]), Revert(&[b])]);
1740
1741        // to multi-member tipset
1742        assert_path_change(&store, e, [c, d], [Revert(e)]);
1743
1744        // over multi-member tipset
1745        assert_path_change(&store, e, b, [Revert(&[e][..]), Revert(&[c, d])]);
1746    }
1747
1748    /// Mirror how lotus handles passing an incomplete `TipsetKey`s.
1749    /// Tested on lotus `1.23.2`
1750    #[test]
1751    fn incomplete_tipsets() {
1752        let store = ChainStore::calibnet();
1753        chain4u! {
1754            in store.blockstore();
1755            [_genesis = store.genesis_block_header()]
1756            -> [a, b] -> [c] -> [d, _e] // this pattern 2 -> 1 -> 2 can be found at calibnet epoch 1369126
1757        };
1758
1759        // apply to descendant with incomplete `from`
1760        assert_path_change(
1761            &store,
1762            a,
1763            c,
1764            [
1765                Revert(&[a][..]), // revert the incomplete tipset
1766                Apply(&[a, b]),   // apply the complete one
1767                Apply(&[c]),      // apply the destination
1768            ],
1769        );
1770
1771        // apply to descendant with incomplete `to`
1772        assert_path_change(&store, c, d, [Apply(d)]);
1773
1774        // revert to ancestor with incomplete `from`
1775        assert_path_change(&store, d, c, [Revert(d)]);
1776
1777        // revert to ancestor with incomplete `to`
1778        assert_path_change(
1779            &store,
1780            c,
1781            a,
1782            [
1783                Revert(&[c][..]),
1784                Revert(&[a, b]), // revert the complete tipset
1785                Apply(&[a]),     // apply the incomplete one
1786            ],
1787        );
1788    }
1789
1790    #[test]
1791    fn apply_to_descendant_linear() {
1792        let store = ChainStore::calibnet();
1793        chain4u! {
1794            in store.blockstore();
1795            [_genesis = store.genesis_block_header()]
1796            -> [a] -> [b] -> [c, d] -> [e]
1797        };
1798
1799        // simple
1800        assert_path_change(&store, a, b, [Apply(&[b])]);
1801
1802        // from multi-member tipset
1803        assert_path_change(&store, [c, d], e, [Apply(e)]);
1804
1805        // to multi-member tipset
1806        assert_path_change(&store, b, [c, d], [Apply([c, d])]);
1807
1808        // over multi-member tipset
1809        assert_path_change(&store, b, e, [Apply(&[c, d][..]), Apply(&[e])]);
1810    }
1811
1812    #[test]
1813    fn cross_fork_simple() {
1814        let store = ChainStore::calibnet();
1815        chain4u! {
1816            in store.blockstore();
1817            [_genesis = store.genesis_block_header()]
1818            -> [a] -> [b1] -> [c1]
1819        };
1820        chain4u! {
1821            from [a] in store.blockstore();
1822            [b2] -> [c2]
1823        };
1824
1825        // same height
1826        assert_path_change(&store, b1, b2, [Revert(b1), Apply(b2)]);
1827
1828        // different height
1829        assert_path_change(&store, b1, c2, [Revert(b1), Apply(b2), Apply(c2)]);
1830
1831        let _ = (a, c1);
1832    }
1833
1834    impl ChainStore<Chain4U<ManyCar>> {
1835        fn _load(genesis_car: &'static [u8], genesis_cid: Cid) -> Self {
1836            let db = Arc::new(Chain4U::with_blockstore(
1837                ManyCar::new(MemoryDB::default())
1838                    .with_read_only(AnyCar::new(genesis_car).unwrap())
1839                    .unwrap(),
1840            ));
1841            let genesis_block_header = db.get_cbor(&genesis_cid).unwrap().unwrap();
1842            ChainStore::new(
1843                db,
1844                Arc::new(MemoryDB::default()),
1845                Arc::new(MemoryDB::default()),
1846                Arc::new(ChainConfig::calibnet()),
1847                genesis_block_header,
1848            )
1849            .unwrap()
1850        }
1851        pub fn calibnet() -> Self {
1852            Self::_load(
1853                networks::calibnet::DEFAULT_GENESIS,
1854                *networks::calibnet::GENESIS_CID,
1855            )
1856        }
1857    }
1858
1859    /// Utility for writing ergonomic tests
1860    trait MakeTipset {
1861        fn make_tipset(self) -> Tipset;
1862    }
1863
1864    impl MakeTipset for &RawBlockHeader {
1865        fn make_tipset(self) -> Tipset {
1866            Tipset::from(CachingBlockHeader::new(self.clone()))
1867        }
1868    }
1869
1870    impl<const N: usize> MakeTipset for [&RawBlockHeader; N] {
1871        fn make_tipset(self) -> Tipset {
1872            self.as_slice().make_tipset()
1873        }
1874    }
1875
1876    impl<const N: usize> MakeTipset for &[&RawBlockHeader; N] {
1877        fn make_tipset(self) -> Tipset {
1878            self.as_slice().make_tipset()
1879        }
1880    }
1881
1882    impl MakeTipset for &[&RawBlockHeader] {
1883        fn make_tipset(self) -> Tipset {
1884            Tipset::new(self.iter().cloned().cloned()).unwrap()
1885        }
1886    }
1887
1888    #[track_caller]
1889    fn assert_path_change<T: MakeTipset>(
1890        store: &ChainStore<impl Blockstore>,
1891        from: impl MakeTipset,
1892        to: impl MakeTipset,
1893        expected: impl IntoIterator<Item = PathChange<T>>,
1894    ) {
1895        fn print(path_change: &PathChange) {
1896            let it = match path_change {
1897                Revert(it) => {
1898                    print!("Revert(");
1899                    it
1900                }
1901                Apply(it) => {
1902                    print!(" Apply(");
1903                    it
1904                }
1905            };
1906            println!(
1907                "epoch = {}, key.cid = {})",
1908                it.epoch(),
1909                it.key().cid().unwrap()
1910            )
1911        }
1912
1913        let actual = chain_get_path(store, from.make_tipset().key(), to.make_tipset().key())
1914            .unwrap()
1915            .into_change_vec();
1916        let expected = expected
1917            .into_iter()
1918            .map(|change| match change {
1919                PathChange::Revert(it) => PathChange::Revert(it.make_tipset()),
1920                PathChange::Apply(it) => PathChange::Apply(it.make_tipset()),
1921            })
1922            .collect_vec();
1923        if expected != actual {
1924            println!("SUMMARY");
1925            println!("=======");
1926            println!("expected:");
1927            for it in &expected {
1928                print(it)
1929            }
1930            println!();
1931            println!("actual:");
1932            for it in &actual {
1933                print(it)
1934            }
1935            println!("=======\n")
1936        }
1937        assert_eq!(
1938            expected, actual,
1939            "expected change (left) does not match actual change (right)"
1940        )
1941    }
1942}