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