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