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::ipld::DfsIter;
15use crate::ipld::{CHAIN_EXPORT_STATUS, cancel_export, end_export, start_export};
16use crate::lotus_json::{HasLotusJson, LotusJson, lotus_json_with_self};
17#[cfg(test)]
18use crate::lotus_json::{assert_all_snapshots, assert_unchanged_via_json};
19use crate::message::{ChainMessage, SignedMessage};
20use crate::rpc::eth::Block as EthBlock;
21use crate::rpc::eth::{
22    EthLog, TxInfo, eth_logs_with_filter, types::ApiHeaders, types::EthFilterSpec,
23};
24use crate::rpc::f3::F3ExportLatestSnapshot;
25use crate::rpc::types::*;
26use crate::rpc::{ApiPaths, Ctx, EthEventHandler, Permission, RpcMethod, ServerError};
27use crate::shim::clock::ChainEpoch;
28use crate::shim::error::ExitCode;
29use crate::shim::executor::Receipt;
30use crate::shim::message::Message;
31use crate::utils::ShallowClone;
32use crate::utils::db::CborStoreExt as _;
33use crate::utils::io::VoidAsyncWriter;
34use crate::utils::misc::env::is_env_truthy;
35use anyhow::{Context as _, Result};
36use cid::Cid;
37use enumflags2::{BitFlags, make_bitflags};
38use fvm_ipld_blockstore::Blockstore;
39use fvm_ipld_encoding::{CborStore, RawBytes};
40use hex::ToHex;
41use ipld_core::ipld::Ipld;
42use itertools::Itertools as _;
43use jsonrpsee::types::Params;
44use jsonrpsee::types::error::ErrorObjectOwned;
45use num::BigInt;
46use schemars::JsonSchema;
47use serde::{Deserialize, Serialize};
48use sha2::Sha256;
49use std::fs::File;
50use std::sync::Arc;
51use std::{collections::VecDeque, path::PathBuf, sync::LazyLock};
52use tokio::sync::{
53    Mutex,
54    broadcast::{self, Receiver as Subscriber},
55};
56use tokio::task::JoinHandle;
57use tokio_util::sync::CancellationToken;
58
59const HEAD_CHANNEL_CAPACITY: usize = 10;
60
61/// [`SAFE_HEIGHT_DISTANCE`] is the distance from the latest tipset, i.e. "heaviest", that
62/// is considered to be safe from re-orgs at an increasingly diminishing
63/// probability.
64///
65/// This is used to determine the safe tipset when using the "safe" tag in
66/// [`TipsetSelector`] or via Eth JSON-RPC APIs. Note that "safe" doesn't guarantee
67/// finality, but rather a high probability of not being reverted. For guaranteed
68/// finality, use the "finalized" tag.
69///
70/// This constant is experimental and may change in the future.
71/// Discussion on this current value and a tracking item to document the
72/// probabilistic impact of various values is in
73/// https://github.com/filecoin-project/go-f3/issues/944
74pub const SAFE_HEIGHT_DISTANCE: ChainEpoch = 200;
75
76static CHAIN_EXPORT_LOCK: LazyLock<Mutex<Option<CancellationToken>>> =
77    LazyLock::new(|| Mutex::new(None));
78
79/// Subscribes to head changes from the chain store and broadcasts new blocks.
80///
81/// # Notes
82///
83/// Spawns an internal `tokio` task that can be aborted anytime via the returned `JoinHandle`,
84/// allowing manual cleanup if needed.
85pub(crate) fn new_heads<DB: Blockstore + Send + Sync + 'static>(
86    data: Ctx<DB>,
87) -> (Subscriber<ApiHeaders>, JoinHandle<()>) {
88    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
89
90    let mut head_changes_rx = data.chain_store().subscribe_head_changes();
91
92    let handle = tokio::spawn(async move {
93        while let Ok(changes) = head_changes_rx.recv().await {
94            for ts in changes.applies {
95                // Convert the tipset to an Ethereum block with full transaction info
96                // Note: In Filecoin's Eth RPC, a tipset maps to a single Ethereum block
97                match EthBlock::from_filecoin_tipset(data.clone(), ts, TxInfo::Full).await {
98                    Ok(block) => {
99                        if let Err(e) = sender.send(ApiHeaders(block)) {
100                            tracing::error!("Failed to send headers: {}", e);
101                            return;
102                        }
103                    }
104                    Err(e) => {
105                        tracing::error!("Failed to convert tipset to eth block: {}", e);
106                    }
107                }
108            }
109        }
110    });
111
112    (receiver, handle)
113}
114
115/// Subscribes to head changes from the chain store and broadcasts new `Ethereum` logs.
116///
117/// # Notes
118///
119/// Spawns an internal `tokio` task that can be aborted anytime via the returned `JoinHandle`,
120/// allowing manual cleanup if needed.
121pub(crate) fn logs<DB: Blockstore + Sync + Send + 'static>(
122    ctx: &Ctx<DB>,
123    filter: Option<EthFilterSpec>,
124) -> (Subscriber<Vec<EthLog>>, JoinHandle<()>) {
125    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
126
127    let mut head_changes_rx = ctx.chain_store().subscribe_head_changes();
128
129    let ctx = ctx.clone();
130
131    let handle = tokio::spawn(async move {
132        while let Ok(changes) = head_changes_rx.recv().await {
133            for ts in changes.applies {
134                match eth_logs_with_filter(&ctx, &ts, filter.clone(), None).await {
135                    Ok(logs) => {
136                        if !logs.is_empty()
137                            && let Err(e) = sender.send(logs)
138                        {
139                            tracing::error!("Failed to send logs for tipset {}: {}", ts.key(), e);
140                            break;
141                        }
142                    }
143                    Err(e) => {
144                        tracing::error!("Failed to fetch logs for tipset {}: {}", ts.key(), e);
145                    }
146                }
147            }
148        }
149    });
150
151    (receiver, handle)
152}
153
154pub enum ChainGetFinalizedTipset {}
155impl RpcMethod<0> for ChainGetFinalizedTipset {
156    const NAME: &'static str = "Filecoin.ChainGetFinalizedTipSet";
157    const PARAM_NAMES: [&'static str; 0] = [];
158    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::V1);
159    const PERMISSION: Permission = Permission::Read;
160    const DESCRIPTION: Option<&'static str> = Some(
161        "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.",
162    );
163
164    type Params = ();
165    type Ok = Tipset;
166
167    async fn handle(
168        ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
169        (): Self::Params,
170        _: &http::Extensions,
171    ) -> Result<Self::Ok, ServerError> {
172        Ok(ChainGetTipSetV2::get_latest_finalized_tipset(&ctx).await?)
173    }
174}
175
176pub enum ChainGetMessage {}
177impl RpcMethod<1> for ChainGetMessage {
178    const NAME: &'static str = "Filecoin.ChainGetMessage";
179    const PARAM_NAMES: [&'static str; 1] = ["messageCid"];
180    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
181    const PERMISSION: Permission = Permission::Read;
182    const DESCRIPTION: Option<&'static str> = Some("Returns the message with the specified CID.");
183
184    type Params = (Cid,);
185    type Ok = FlattenedApiMessage;
186
187    async fn handle(
188        ctx: Ctx<impl Blockstore>,
189        (message_cid,): Self::Params,
190        _: &http::Extensions,
191    ) -> Result<Self::Ok, ServerError> {
192        let chain_message: ChainMessage = ctx
193            .store()
194            .get_cbor(&message_cid)?
195            .with_context(|| format!("can't find message with cid {message_cid}"))?;
196        let message = match chain_message {
197            ChainMessage::Signed(m) => Arc::unwrap_or_clone(m).into_message(),
198            ChainMessage::Unsigned(m) => Arc::unwrap_or_clone(m),
199        };
200
201        let cid = message.cid();
202        Ok(FlattenedApiMessage { message, cid })
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 + 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 + 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 + 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>,
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>,
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 + 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 + 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 + 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 + 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 + 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 + 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(ctx: &Ctx<impl Blockstore>) -> anyhow::Result<ChainFinalityStatus> {
1143        let head = ctx.chain_store().heaviest_tipset();
1144        let (ec_finality_threshold_depth, ec_finalized_tip_set) =
1145            Self::get_ec_finality_threshold_depth_and_tipset_with_cache(ctx, head.shallow_clone())?;
1146        let f3_finalized_tip_set = ctx.chain_store().f3_finalized_tipset();
1147        let finalized_tip_set = match (&ec_finalized_tip_set, &f3_finalized_tip_set) {
1148            (Some(ec), Some(f3)) => {
1149                if ec.epoch() >= f3.epoch() {
1150                    Some(ec.shallow_clone())
1151                } else {
1152                    Some(f3.shallow_clone())
1153                }
1154            }
1155            (Some(ec), None) => Some(ec.shallow_clone()),
1156            (None, Some(f3)) => Some(f3.shallow_clone()),
1157            (None, None) => None,
1158        };
1159        Ok(ChainFinalityStatus {
1160            ec_finality_threshold_depth,
1161            ec_finalized_tip_set,
1162            f3_finalized_tip_set,
1163            finalized_tip_set,
1164            head,
1165        })
1166    }
1167
1168    pub fn get_ec_finality_threshold_depth_and_tipset_with_cache(
1169        ctx: &Ctx<impl Blockstore>,
1170        head: Tipset,
1171    ) -> anyhow::Result<(i64, Option<Tipset>)> {
1172        static CACHE: parking_lot::Mutex<Option<(Tipset, i64, Option<Tipset>)>> =
1173            parking_lot::Mutex::new(None);
1174        let mut cache = CACHE.lock();
1175        if let Some((cached_head, cached_threshold, cached_tipset)) = &*cache
1176            && cached_head == &head
1177        {
1178            Ok((*cached_threshold, cached_tipset.shallow_clone()))
1179        } else {
1180            let (threshold, tipset) =
1181                Self::get_ec_finality_threshold_depth_and_tipset(ctx, head.shallow_clone())?;
1182            *cache = Some((head, threshold, tipset.shallow_clone()));
1183            Ok((threshold, tipset))
1184        }
1185    }
1186
1187    fn get_ec_finality_threshold_depth_and_tipset(
1188        ctx: &Ctx<impl Blockstore>,
1189        head: Tipset,
1190    ) -> anyhow::Result<(i64, Option<Tipset>)> {
1191        use crate::chain::ec_finality::calculator::{
1192            DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE,
1193            find_threshold_depth,
1194        };
1195
1196        /// Number of extra epochs to fetch beyond [`chain_finality`] when
1197        /// building the chain sample for [`find_threshold_depth`].
1198        ///
1199        /// The extra 5 epochs act as a tail buffer to prevent out-of-bounds access,
1200        /// particularly when null rounds (epochs with zero blocks) are present, since
1201        /// they consume array slots without advancing the meaningful epoch count.
1202        const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5;
1203
1204        let finality = ctx.chain_config().policy.chain_finality;
1205        let chain_len = finality as usize + FINALITY_CHAIN_EXTRA_EPOCHS;
1206        let mut chain = Vec::with_capacity(chain_len);
1207        let mut ts = head.shallow_clone();
1208        while chain.len() < chain_len {
1209            chain.push(ts.len() as i64);
1210            if let Ok(parent) = ctx.chain_index().load_required_tipset(ts.parents()) {
1211                // insert 0 for null rounds
1212                if let Ok(n_null_tipsets_to_pad) = usize::try_from(ts.epoch() - parent.epoch() - 1)
1213                    && n_null_tipsets_to_pad > 0
1214                {
1215                    let target_len =
1216                        (chain.len().saturating_add(n_null_tipsets_to_pad)).min(chain_len);
1217                    chain.resize(target_len, 0);
1218                }
1219                ts = parent;
1220            } else {
1221                break;
1222            }
1223        }
1224        // Reverse to chronological order (oldest first).
1225        chain.reverse();
1226        let depth = match find_threshold_depth(
1227            &chain,
1228            finality,
1229            DEFAULT_BLOCKS_PER_EPOCH,
1230            DEFAULT_BYZANTINE_FRACTION,
1231            *DEFAULT_GUARANTEE,
1232        ) {
1233            Ok(threshold) => threshold,
1234            Err(e) => {
1235                tracing::error!(
1236                    "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}"
1237                );
1238                -1
1239            }
1240        };
1241        let finalized = if depth >= 0
1242            && let Ok(Some(ts)) = ctx.chain_index().tipset_by_height(
1243                (head.epoch() - depth).max(0),
1244                head.shallow_clone(),
1245                ResolveNullTipset::TakeOlder,
1246            ) {
1247            Some(ts)
1248        } else {
1249            let ec_finality_epoch =
1250                (head.epoch() - ctx.chain_config().policy.chain_finality).max(0);
1251            ctx.chain_index().tipset_by_height(
1252                ec_finality_epoch,
1253                head,
1254                ResolveNullTipset::TakeOlder,
1255            )?
1256        };
1257        Ok((depth, finalized))
1258    }
1259}
1260
1261impl RpcMethod<0> for ChainGetTipSetFinalityStatus {
1262    const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus";
1263    const PARAM_NAMES: [&'static str; 0] = [];
1264    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1265    const PERMISSION: Permission = Permission::Read;
1266    const DESCRIPTION: Option<&'static str> =
1267        Some("Returns a breakdown of how the node is currently determining finality.");
1268
1269    type Params = ();
1270    type Ok = ChainFinalityStatus;
1271
1272    async fn handle(
1273        ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
1274        (): Self::Params,
1275        _: &http::Extensions,
1276    ) -> Result<Self::Ok, ServerError> {
1277        Ok(Self::get_finality_status(&ctx)?)
1278    }
1279}
1280
1281pub enum ChainSetHead {}
1282impl RpcMethod<1> for ChainSetHead {
1283    const NAME: &'static str = "Filecoin.ChainSetHead";
1284    const PARAM_NAMES: [&'static str; 1] = ["tsk"];
1285    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1286    const PERMISSION: Permission = Permission::Admin;
1287
1288    type Params = (TipsetKey,);
1289    type Ok = ();
1290
1291    async fn handle(
1292        ctx: Ctx<impl Blockstore>,
1293        (tsk,): Self::Params,
1294        _: &http::Extensions,
1295    ) -> Result<Self::Ok, ServerError> {
1296        // This is basically a port of the reference implementation at
1297        // https://github.com/filecoin-project/lotus/blob/v1.23.0/node/impl/full/chain.go#L321
1298
1299        let new_head = ctx.chain_index().load_required_tipset(&tsk)?;
1300        let mut current = ctx.chain_store().heaviest_tipset();
1301        while current.epoch() >= new_head.epoch() {
1302            for cid in current.key().to_cids() {
1303                ctx.chain_store().unmark_block_as_validated(&cid);
1304            }
1305            let parents = &current.block_headers().first().parents;
1306            current = ctx.chain_index().load_required_tipset(parents)?;
1307        }
1308        ctx.chain_store()
1309            .set_heaviest_tipset(new_head)
1310            .map_err(Into::into)
1311    }
1312}
1313
1314pub enum ChainGetMinBaseFee {}
1315impl RpcMethod<1> for ChainGetMinBaseFee {
1316    const NAME: &'static str = "Forest.ChainGetMinBaseFee";
1317    const PARAM_NAMES: [&'static str; 1] = ["lookback"];
1318    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1319    const PERMISSION: Permission = Permission::Read;
1320
1321    type Params = (u32,);
1322    type Ok = String;
1323
1324    async fn handle(
1325        ctx: Ctx<impl Blockstore>,
1326        (lookback,): Self::Params,
1327        _: &http::Extensions,
1328    ) -> Result<Self::Ok, ServerError> {
1329        let mut current = ctx.chain_store().heaviest_tipset();
1330        let mut min_base_fee = current.block_headers().first().parent_base_fee.clone();
1331
1332        for _ in 0..lookback {
1333            let parents = &current.block_headers().first().parents;
1334            current = ctx.chain_index().load_required_tipset(parents)?;
1335
1336            min_base_fee =
1337                min_base_fee.min(current.block_headers().first().parent_base_fee.to_owned());
1338        }
1339
1340        Ok(min_base_fee.atto().to_string())
1341    }
1342}
1343
1344pub enum ChainTipSetWeight {}
1345impl RpcMethod<1> for ChainTipSetWeight {
1346    const NAME: &'static str = "Filecoin.ChainTipSetWeight";
1347    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1348    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1349    const PERMISSION: Permission = Permission::Read;
1350    const DESCRIPTION: Option<&'static str> = Some("Returns the weight of the specified tipset.");
1351
1352    type Params = (ApiTipsetKey,);
1353    type Ok = BigInt;
1354
1355    async fn handle(
1356        ctx: Ctx<impl Blockstore>,
1357        (ApiTipsetKey(tipset_key),): Self::Params,
1358        _: &http::Extensions,
1359    ) -> Result<Self::Ok, ServerError> {
1360        let ts = ctx
1361            .chain_store()
1362            .load_required_tipset_or_heaviest(&tipset_key)?;
1363        let weight = crate::fil_cns::weight(ctx.store(), &ts)?;
1364        Ok(weight)
1365    }
1366}
1367
1368pub enum ChainGetTipsetByParentState {}
1369impl RpcMethod<1> for ChainGetTipsetByParentState {
1370    const NAME: &'static str = "Forest.ChainGetTipsetByParentState";
1371    const PARAM_NAMES: [&'static str; 1] = ["parentState"];
1372    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1373    const PERMISSION: Permission = Permission::Read;
1374
1375    type Params = (Cid,);
1376    type Ok = Option<Tipset>;
1377
1378    async fn handle(
1379        ctx: Ctx<impl Blockstore>,
1380        (parent_state,): Self::Params,
1381        _: &http::Extensions,
1382    ) -> Result<Self::Ok, ServerError> {
1383        Ok(ctx
1384            .chain_store()
1385            .heaviest_tipset()
1386            .chain(ctx.store())
1387            .find(|ts| ts.parent_state() == &parent_state)
1388            .shallow_clone())
1389    }
1390}
1391
1392pub const CHAIN_NOTIFY: &str = "Filecoin.ChainNotify";
1393pub(crate) fn chain_notify<DB: Blockstore>(
1394    _params: Params<'_>,
1395    data: &crate::rpc::RPCState<DB>,
1396) -> Subscriber<Vec<ApiHeadChange>> {
1397    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
1398
1399    // As soon as the channel is created, send the current tipset
1400    let current = data.chain_store().heaviest_tipset();
1401    let (change, tipset) = ("current".into(), current);
1402    sender
1403        .send(vec![ApiHeadChange { change, tipset }])
1404        .expect("receiver is not dropped");
1405
1406    let mut head_changes_rx = data.chain_store().subscribe_head_changes();
1407
1408    tokio::spawn(async move {
1409        // Skip first message
1410        let _ = head_changes_rx.recv().await;
1411        while let Ok(changes) = head_changes_rx.recv().await {
1412            let api_changes = changes
1413                .into_change_vec()
1414                .into_iter()
1415                .map(From::from)
1416                .collect();
1417            if sender.send(api_changes).is_err() {
1418                break;
1419            }
1420        }
1421    });
1422    receiver
1423}
1424
1425async fn load_api_messages_from_tipset<DB: Blockstore + Send + Sync + 'static>(
1426    ctx: &crate::rpc::RPCState<DB>,
1427    tipset_keys: &TipsetKey,
1428) -> Result<Vec<ApiMessage>, ServerError> {
1429    static SHOULD_BACKFILL: LazyLock<bool> = LazyLock::new(|| {
1430        let enabled = is_env_truthy("FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK");
1431        if enabled {
1432            tracing::warn!(
1433                "Full tipset backfilling from network is enabled via FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK, excessive disk and bandwidth usage is expected."
1434            );
1435        }
1436        enabled
1437    });
1438    let full_tipset = if *SHOULD_BACKFILL {
1439        get_full_tipset(
1440            &ctx.sync_network_context,
1441            ctx.chain_store(),
1442            None,
1443            tipset_keys,
1444        )
1445        .await?
1446    } else {
1447        load_full_tipset(ctx.chain_store(), tipset_keys)?
1448    };
1449    let blocks = full_tipset.into_blocks();
1450    let mut messages = vec![];
1451    let mut seen = CidHashSet::default();
1452    for Block {
1453        bls_messages,
1454        secp_messages,
1455        ..
1456    } in blocks
1457    {
1458        for message in bls_messages {
1459            let cid = message.cid();
1460            if seen.insert(cid) {
1461                messages.push(ApiMessage { cid, message });
1462            }
1463        }
1464
1465        for msg in secp_messages {
1466            let cid = msg.cid();
1467            if seen.insert(cid) {
1468                messages.push(ApiMessage {
1469                    cid,
1470                    message: msg.message,
1471                });
1472            }
1473        }
1474    }
1475
1476    Ok(messages)
1477}
1478
1479#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1480pub struct BlockMessages {
1481    #[serde(rename = "BlsMessages", with = "crate::lotus_json")]
1482    #[schemars(with = "LotusJson<Vec<Message>>")]
1483    pub bls_msg: Vec<Message>,
1484    #[serde(rename = "SecpkMessages", with = "crate::lotus_json")]
1485    #[schemars(with = "LotusJson<Vec<SignedMessage>>")]
1486    pub secp_msg: Vec<SignedMessage>,
1487    #[serde(rename = "Cids", with = "crate::lotus_json")]
1488    #[schemars(with = "LotusJson<Vec<Cid>>")]
1489    pub cids: Vec<Cid>,
1490}
1491lotus_json_with_self!(BlockMessages);
1492
1493#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
1494#[serde(rename_all = "PascalCase")]
1495pub struct ApiReceipt {
1496    // Exit status of message execution
1497    pub exit_code: ExitCode,
1498    // `Return` value if the exit code is zero
1499    #[serde(rename = "Return", with = "crate::lotus_json")]
1500    #[schemars(with = "LotusJson<RawBytes>")]
1501    pub return_data: RawBytes,
1502    // Non-negative value of GasUsed
1503    pub gas_used: u64,
1504    #[serde(with = "crate::lotus_json")]
1505    #[schemars(with = "LotusJson<Option<Cid>>")]
1506    pub events_root: Option<Cid>,
1507}
1508
1509lotus_json_with_self!(ApiReceipt);
1510
1511#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
1512#[serde(rename_all = "PascalCase")]
1513pub struct ApiMessage {
1514    #[serde(with = "crate::lotus_json")]
1515    #[schemars(with = "LotusJson<Cid>")]
1516    pub cid: Cid,
1517    #[serde(with = "crate::lotus_json")]
1518    #[schemars(with = "LotusJson<Message>")]
1519    pub message: Message,
1520}
1521
1522lotus_json_with_self!(ApiMessage);
1523
1524#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
1525pub struct FlattenedApiMessage {
1526    #[serde(flatten, with = "crate::lotus_json")]
1527    #[schemars(with = "LotusJson<Message>")]
1528    pub message: Message,
1529    #[serde(rename = "CID", with = "crate::lotus_json")]
1530    #[schemars(with = "LotusJson<Cid>")]
1531    pub cid: Cid,
1532}
1533
1534lotus_json_with_self!(FlattenedApiMessage);
1535
1536#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1537pub struct ForestChainExportParams {
1538    pub version: FilecoinSnapshotVersion,
1539    pub epoch: ChainEpoch,
1540    pub recent_roots: i64,
1541    pub output_path: PathBuf,
1542    #[schemars(with = "LotusJson<ApiTipsetKey>")]
1543    #[serde(with = "crate::lotus_json")]
1544    pub tipset_keys: ApiTipsetKey,
1545    #[serde(default)]
1546    pub include_receipts: bool,
1547    #[serde(default)]
1548    pub include_events: bool,
1549    #[serde(default)]
1550    pub include_tipset_keys: bool,
1551    pub skip_checksum: bool,
1552    pub dry_run: bool,
1553}
1554lotus_json_with_self!(ForestChainExportParams);
1555
1556#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1557pub struct ForestChainExportDiffParams {
1558    pub from: ChainEpoch,
1559    pub to: ChainEpoch,
1560    pub depth: i64,
1561    pub output_path: PathBuf,
1562}
1563lotus_json_with_self!(ForestChainExportDiffParams);
1564
1565#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1566pub struct ChainExportParams {
1567    pub epoch: ChainEpoch,
1568    pub recent_roots: i64,
1569    pub output_path: PathBuf,
1570    #[schemars(with = "LotusJson<ApiTipsetKey>")]
1571    #[serde(with = "crate::lotus_json")]
1572    pub tipset_keys: ApiTipsetKey,
1573    pub skip_checksum: bool,
1574    pub dry_run: bool,
1575}
1576lotus_json_with_self!(ChainExportParams);
1577
1578#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, JsonSchema)]
1579#[serde(rename_all = "PascalCase")]
1580pub struct ApiHeadChange {
1581    #[serde(rename = "Type")]
1582    pub change: String,
1583    #[serde(rename = "Val", with = "crate::lotus_json")]
1584    #[schemars(with = "LotusJson<Tipset>")]
1585    pub tipset: Tipset,
1586}
1587lotus_json_with_self!(ApiHeadChange);
1588
1589impl From<HeadChange> for ApiHeadChange {
1590    fn from(change: HeadChange) -> Self {
1591        match change {
1592            HeadChange::Apply(tipset) => Self {
1593                change: "apply".into(),
1594                tipset,
1595            },
1596            HeadChange::Revert(tipset) => Self {
1597                change: "revert".into(),
1598                tipset,
1599            },
1600        }
1601    }
1602}
1603
1604#[derive(PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
1605#[serde(tag = "Type", content = "Val", rename_all = "snake_case")]
1606pub enum PathChange<T = Tipset> {
1607    Revert(T),
1608    Apply(T),
1609}
1610
1611impl<T: Clone> Clone for PathChange<T> {
1612    fn clone(&self) -> Self {
1613        match self {
1614            Self::Revert(i) => Self::Revert(i.clone()),
1615            Self::Apply(i) => Self::Apply(i.clone()),
1616        }
1617    }
1618}
1619
1620impl HasLotusJson for PathChange {
1621    type LotusJson = PathChange<<Tipset as HasLotusJson>::LotusJson>;
1622
1623    #[cfg(test)]
1624    fn snapshots() -> Vec<(serde_json::Value, Self)> {
1625        use serde_json::json;
1626        vec![(
1627            json!({
1628                "Type": "revert",
1629                "Val": {
1630                    "Blocks": [
1631                        {
1632                            "BeaconEntries": null,
1633                            "ForkSignaling": 0,
1634                            "Height": 0,
1635                            "Messages": { "/": "baeaaaaa" },
1636                            "Miner": "f00",
1637                            "ParentBaseFee": "0",
1638                            "ParentMessageReceipts": { "/": "baeaaaaa" },
1639                            "ParentStateRoot": { "/":"baeaaaaa" },
1640                            "ParentWeight": "0",
1641                            "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
1642                            "Timestamp": 0,
1643                            "WinPoStProof": null
1644                        }
1645                    ],
1646                    "Cids": [
1647                        { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
1648                    ],
1649                    "Height": 0
1650                }
1651            }),
1652            Self::Revert(RawBlockHeader::default().into()),
1653        )]
1654    }
1655
1656    fn into_lotus_json(self) -> Self::LotusJson {
1657        match self {
1658            PathChange::Revert(it) => PathChange::Revert(it.into_lotus_json()),
1659            PathChange::Apply(it) => PathChange::Apply(it.into_lotus_json()),
1660        }
1661    }
1662
1663    fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
1664        match lotus_json {
1665            PathChange::Revert(it) => PathChange::Revert(Tipset::from_lotus_json(it)),
1666            PathChange::Apply(it) => PathChange::Apply(Tipset::from_lotus_json(it)),
1667        }
1668    }
1669}
1670
1671#[derive(Debug)]
1672pub struct PathChanges<T = Tipset> {
1673    pub reverts: Vec<T>,
1674    pub applies: Vec<T>,
1675}
1676
1677impl<T: Clone> Clone for PathChanges<T> {
1678    fn clone(&self) -> Self {
1679        let Self { reverts, applies } = self;
1680        Self {
1681            reverts: reverts.clone(),
1682            applies: applies.clone(),
1683        }
1684    }
1685}
1686
1687impl<T> PathChanges<T> {
1688    pub fn into_change_vec(self) -> Vec<PathChange<T>> {
1689        let Self { reverts, applies } = self;
1690        reverts
1691            .into_iter()
1692            .map(PathChange::Revert)
1693            .chain(applies.into_iter().map(PathChange::Apply))
1694            .collect_vec()
1695    }
1696}
1697
1698#[cfg(test)]
1699impl<T> quickcheck::Arbitrary for PathChange<T>
1700where
1701    T: quickcheck::Arbitrary + ShallowClone,
1702{
1703    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
1704        let inner = T::arbitrary(g);
1705        g.choose(&[PathChange::Apply(inner.clone()), PathChange::Revert(inner)])
1706            .unwrap()
1707            .clone()
1708    }
1709}
1710
1711#[test]
1712fn snapshots() {
1713    assert_all_snapshots::<PathChange>()
1714}
1715
1716#[cfg(test)]
1717#[quickcheck_macros::quickcheck]
1718fn quickcheck(val: PathChange) {
1719    assert_unchanged_via_json(val)
1720}
1721
1722#[cfg(test)]
1723mod tests {
1724    use super::*;
1725    use crate::{
1726        blocks::{Chain4U, RawBlockHeader, chain4u},
1727        db::{
1728            MemoryDB,
1729            car::{AnyCar, ManyCar},
1730        },
1731        networks::{self, ChainConfig},
1732    };
1733    use PathChange::{Apply, Revert};
1734    use std::sync::Arc;
1735
1736    #[test]
1737    fn revert_to_ancestor_linear() {
1738        let store = ChainStore::calibnet();
1739        chain4u! {
1740            in store.blockstore();
1741            [_genesis = store.genesis_block_header()]
1742            -> [a] -> [b] -> [c, d] -> [e]
1743        };
1744
1745        // simple
1746        assert_path_change(&store, b, a, [Revert(&[b])]);
1747
1748        // from multi-member tipset
1749        assert_path_change(&store, [c, d], a, [Revert(&[c, d][..]), Revert(&[b])]);
1750
1751        // to multi-member tipset
1752        assert_path_change(&store, e, [c, d], [Revert(e)]);
1753
1754        // over multi-member tipset
1755        assert_path_change(&store, e, b, [Revert(&[e][..]), Revert(&[c, d])]);
1756    }
1757
1758    /// Mirror how lotus handles passing an incomplete `TipsetKey`s.
1759    /// Tested on lotus `1.23.2`
1760    #[test]
1761    fn incomplete_tipsets() {
1762        let store = ChainStore::calibnet();
1763        chain4u! {
1764            in store.blockstore();
1765            [_genesis = store.genesis_block_header()]
1766            -> [a, b] -> [c] -> [d, _e] // this pattern 2 -> 1 -> 2 can be found at calibnet epoch 1369126
1767        };
1768
1769        // apply to descendant with incomplete `from`
1770        assert_path_change(
1771            &store,
1772            a,
1773            c,
1774            [
1775                Revert(&[a][..]), // revert the incomplete tipset
1776                Apply(&[a, b]),   // apply the complete one
1777                Apply(&[c]),      // apply the destination
1778            ],
1779        );
1780
1781        // apply to descendant with incomplete `to`
1782        assert_path_change(&store, c, d, [Apply(d)]);
1783
1784        // revert to ancestor with incomplete `from`
1785        assert_path_change(&store, d, c, [Revert(d)]);
1786
1787        // revert to ancestor with incomplete `to`
1788        assert_path_change(
1789            &store,
1790            c,
1791            a,
1792            [
1793                Revert(&[c][..]),
1794                Revert(&[a, b]), // revert the complete tipset
1795                Apply(&[a]),     // apply the incomplete one
1796            ],
1797        );
1798    }
1799
1800    #[test]
1801    fn apply_to_descendant_linear() {
1802        let store = ChainStore::calibnet();
1803        chain4u! {
1804            in store.blockstore();
1805            [_genesis = store.genesis_block_header()]
1806            -> [a] -> [b] -> [c, d] -> [e]
1807        };
1808
1809        // simple
1810        assert_path_change(&store, a, b, [Apply(&[b])]);
1811
1812        // from multi-member tipset
1813        assert_path_change(&store, [c, d], e, [Apply(e)]);
1814
1815        // to multi-member tipset
1816        assert_path_change(&store, b, [c, d], [Apply([c, d])]);
1817
1818        // over multi-member tipset
1819        assert_path_change(&store, b, e, [Apply(&[c, d][..]), Apply(&[e])]);
1820    }
1821
1822    #[test]
1823    fn cross_fork_simple() {
1824        let store = ChainStore::calibnet();
1825        chain4u! {
1826            in store.blockstore();
1827            [_genesis = store.genesis_block_header()]
1828            -> [a] -> [b1] -> [c1]
1829        };
1830        chain4u! {
1831            from [a] in store.blockstore();
1832            [b2] -> [c2]
1833        };
1834
1835        // same height
1836        assert_path_change(&store, b1, b2, [Revert(b1), Apply(b2)]);
1837
1838        // different height
1839        assert_path_change(&store, b1, c2, [Revert(b1), Apply(b2), Apply(c2)]);
1840
1841        let _ = (a, c1);
1842    }
1843
1844    impl ChainStore<Chain4U<ManyCar>> {
1845        fn _load(genesis_car: &'static [u8], genesis_cid: Cid) -> Self {
1846            let db = Arc::new(Chain4U::with_blockstore(
1847                ManyCar::new(MemoryDB::default())
1848                    .with_read_only(AnyCar::new(genesis_car).unwrap())
1849                    .unwrap(),
1850            ));
1851            let genesis_block_header = db.get_cbor(&genesis_cid).unwrap().unwrap();
1852            ChainStore::new(
1853                db,
1854                Arc::new(MemoryDB::default()),
1855                Arc::new(MemoryDB::default()),
1856                Arc::new(ChainConfig::calibnet()),
1857                genesis_block_header,
1858            )
1859            .unwrap()
1860        }
1861        pub fn calibnet() -> Self {
1862            Self::_load(
1863                networks::calibnet::DEFAULT_GENESIS,
1864                *networks::calibnet::GENESIS_CID,
1865            )
1866        }
1867    }
1868
1869    /// Utility for writing ergonomic tests
1870    trait MakeTipset {
1871        fn make_tipset(self) -> Tipset;
1872    }
1873
1874    impl MakeTipset for &RawBlockHeader {
1875        fn make_tipset(self) -> Tipset {
1876            Tipset::from(CachingBlockHeader::new(self.clone()))
1877        }
1878    }
1879
1880    impl<const N: usize> MakeTipset for [&RawBlockHeader; N] {
1881        fn make_tipset(self) -> Tipset {
1882            self.as_slice().make_tipset()
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 MakeTipset for &[&RawBlockHeader] {
1893        fn make_tipset(self) -> Tipset {
1894            Tipset::new(self.iter().cloned().cloned()).unwrap()
1895        }
1896    }
1897
1898    #[track_caller]
1899    fn assert_path_change<T: MakeTipset>(
1900        store: &ChainStore<impl Blockstore>,
1901        from: impl MakeTipset,
1902        to: impl MakeTipset,
1903        expected: impl IntoIterator<Item = PathChange<T>>,
1904    ) {
1905        fn print(path_change: &PathChange) {
1906            let it = match path_change {
1907                Revert(it) => {
1908                    print!("Revert(");
1909                    it
1910                }
1911                Apply(it) => {
1912                    print!(" Apply(");
1913                    it
1914                }
1915            };
1916            println!(
1917                "epoch = {}, key.cid = {})",
1918                it.epoch(),
1919                it.key().cid().unwrap()
1920            )
1921        }
1922
1923        let actual = chain_get_path(store, from.make_tipset().key(), to.make_tipset().key())
1924            .unwrap()
1925            .into_change_vec();
1926        let expected = expected
1927            .into_iter()
1928            .map(|change| match change {
1929                PathChange::Revert(it) => PathChange::Revert(it.make_tipset()),
1930                PathChange::Apply(it) => PathChange::Apply(it.make_tipset()),
1931            })
1932            .collect_vec();
1933        if expected != actual {
1934            println!("SUMMARY");
1935            println!("=======");
1936            println!("expected:");
1937            for it in &expected {
1938                print(it)
1939            }
1940            println!();
1941            println!("actual:");
1942            for it in &actual {
1943                print(it)
1944            }
1945            println!("=======\n")
1946        }
1947        assert_eq!(
1948            expected, actual,
1949            "expected change (left) does not match actual change (right)"
1950        )
1951    }
1952}