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    anyhow::ensure!(
348        recent_roots >= 0,
349        "recentRoots must not be negative, got {recent_roots}."
350    );
351
352    let head = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?;
353    let start_ts = ctx
354        .chain_index()
355        .load_required_tipset_by_height(epoch, head, ResolveNullTipset::TakeOlder)
356        .await?;
357
358    let options = ExportOptions {
359        skip_checksum,
360        include_receipts,
361        include_events,
362        include_tipset_keys,
363        include_tipset_lookup: tipset_lookup,
364        seen: FileBackedCidHashSet::new(ctx.temp_dir.as_path())?,
365    };
366    let tmp_path = tempfile::TempPath::try_from_path(tmp_exporting_forest_car_path(&output_path))?;
367    let writer = if dry_run {
368        tokio_util::either::Either::Left(VoidAsyncWriter)
369    } else {
370        tokio_util::either::Either::Right(tokio::fs::File::create(&tmp_path).await?)
371    };
372    let chain_export = match version {
373        FilecoinSnapshotVersion::V1 => {
374            crate::chain::export::<Sha256, _>(ctx.db(), &start_ts, recent_roots, writer, options)
375                .boxed()
376        }
377        FilecoinSnapshotVersion::V2 => {
378            let f3_snap_tmp_path = {
379                let mut f3_snap_dir = output_path.clone();
380                let mut builder = tempfile::Builder::new();
381                let with_suffix = builder.suffix(".f3snap.bin");
382                if f3_snap_dir.pop() {
383                    with_suffix.tempfile_in(&f3_snap_dir)
384                } else {
385                    with_suffix.tempfile_in(".")
386                }?
387                .into_temp_path()
388            };
389            let f3_snap = {
390                match F3ExportLatestSnapshot::run(f3_snap_tmp_path.display().to_string()).await {
391                    Ok(cid) => Some((cid, File::open(&f3_snap_tmp_path)?)),
392                    Err(e) => {
393                        tracing::error!("Failed to export F3 snapshot: {e:#}");
394                        None
395                    }
396                }
397            };
398            crate::chain::export_v2::<Sha256, _, _>(
399                ctx.db(),
400                f3_snap,
401                &start_ts,
402                recent_roots,
403                writer,
404                options,
405            )
406            .boxed()
407        }
408    };
409    match chain_export_guard.run_cancellable(chain_export).await {
410        Some(result) => {
411            let ExportResult {
412                checksum,
413                tipset_lookup: hamt,
414            } = result?;
415            if !dry_run {
416                let output_path = output_path.clone();
417                spawn_blocking_with_timeout(ASYNC_OPS_TIMEOUT, move || {
418                    tmp_path.persist(&output_path)?;
419                    // The snapshot at `output_path` is usable from this point on;
420                    // a checksum-file failure is not worth failing the export over.
421                    if let Some(checksum) = checksum
422                        && let Err(e) = save_checksum(checksum, &output_path)
423                    {
424                        tracing::warn!(
425                            "failed to save the checksum file for {}: {e:#}",
426                            output_path.display()
427                        );
428                    }
429                    Ok(())
430                })
431                .await
432                .context("failed to persist the exported snapshot")?;
433            }
434            // The auxiliary snapshots must stay cancellable: the guard is still held,
435            // so an unguarded await here would accept a cancel yet ignore it.
436            let auxiliary_exports = async {
437                match (tipset_lookup, hamt) {
438                    (true, Some(hamt)) => {
439                        let mut hamt = hamt.context("failed to generate tipset lookup snapshot")?;
440                        let roots = nunny::vec![hamt.flush()?];
441                        let hamt_output_path =
442                            forest_car_with_filename_suffix(&output_path, "_tipset_lookup")?;
443                        let (mut writer, hamt_output_tmp_path) = if dry_run {
444                            (tokio_util::either::Either::Left(VoidAsyncWriter), None)
445                        } else {
446                            let tmp_path = tempfile::TempPath::try_from_path(
447                                tmp_exporting_forest_car_path(&hamt_output_path),
448                            )?;
449                            (
450                                tokio_util::either::Either::Right(
451                                    tokio::fs::File::create(&tmp_path).await?,
452                                ),
453                                Some(tmp_path),
454                            )
455                        };
456                        hamt.into_store()
457                            .export_forest_car(roots, &mut writer)
458                            .await
459                            .context("failed to write tipset lookup snapshot")?;
460                        if let Some(hamt_output_tmp_path) = hamt_output_tmp_path {
461                            hamt_output_tmp_path.persist(&hamt_output_path)?;
462                            if !skip_checksum {
463                                // No need to generate checksum on the fly for small snapshots
464                                save_checksum(
465                                    Sha256::digest(std::fs::read(&hamt_output_path)?),
466                                    &hamt_output_path,
467                                )?;
468                            }
469                        }
470                    }
471                    (true, None) => {
472                        anyhow::bail!("requested tipset lookup snapshot is missing")
473                    }
474                    (false, Some(_)) => {
475                        anyhow::bail!(
476                            "tipset lookup snapshot should not be generated when it's not requested"
477                        )
478                    }
479                    _ => {}
480                }
481                if augmented_snapshot {
482                    // It takes <10s on mainnet so export it in sequence for simplicity.
483                    // Some stats:
484                    // calibnet 3895470+2000: 5s  5.4MiB
485                    // mainnet  6193120+2000: 5s  12MiB
486                    let augmented_snapshot_output_path =
487                        forest_car_with_filename_suffix(&output_path, "_receipts_events")?;
488                    let (writer, augmented_snapshot_output_tmp_path) = if dry_run {
489                        (tokio_util::either::Either::Left(VoidAsyncWriter), None)
490                    } else {
491                        let tmp_path = tempfile::TempPath::try_from_path(
492                            tmp_exporting_forest_car_path(&augmented_snapshot_output_path),
493                        )?;
494                        (
495                            tokio_util::either::Either::Right(
496                                tokio::fs::File::create(&tmp_path).await?,
497                            ),
498                            Some(tmp_path),
499                        )
500                    };
501                    crate::chain::export_receipts_events_to_forest_car(
502                        ctx.db(),
503                        &start_ts,
504                        recent_roots,
505                        writer,
506                    )
507                    .await
508                    .context("failed to export message receipts and events snapshot")?;
509                    if let Some(augmented_snapshot_output_tmp_path) =
510                        augmented_snapshot_output_tmp_path
511                    {
512                        augmented_snapshot_output_tmp_path
513                            .persist(&augmented_snapshot_output_path)?;
514                        if !skip_checksum {
515                            // No need to generate checksum on the fly for small snapshots
516                            save_checksum(
517                                Sha256::digest(std::fs::read(&augmented_snapshot_output_path)?),
518                                &augmented_snapshot_output_path,
519                            )?;
520                        }
521                    }
522                }
523                anyhow::Ok(())
524            };
525            match chain_export_guard.run_cancellable(auxiliary_exports).await {
526                Some(result) => {
527                    result?;
528                    Ok(ApiExportResult::Done)
529                }
530                None => {
531                    tracing::warn!("Auxiliary snapshot exports were cancelled");
532                    Ok(ApiExportResult::Cancelled)
533                }
534            }
535        }
536        None => {
537            tracing::warn!("Snapshot export was cancelled");
538            Ok(ApiExportResult::Cancelled)
539        }
540    }
541}
542
543pub enum ForestChainExportStatus {}
544impl RpcMethod<0> for ForestChainExportStatus {
545    const NAME: &'static str = "Forest.ChainExportStatus";
546    const PARAM_NAMES: [&'static str; 0] = [];
547    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
548    const PERMISSION: Permission = Permission::Read;
549    const DESCRIPTION: &'static str =
550        "Returns the progress and status of the in-progress chain export.";
551
552    type Params = ();
553    type Ok = ApiExportStatus;
554
555    async fn handle(
556        _ctx: Ctx,
557        (): Self::Params,
558        _: &http::Extensions,
559    ) -> Result<Self::Ok, ServerError> {
560        let snapshot = CHAIN_EXPORT_STATUS.snapshot();
561        let initial_epoch = snapshot.initial_epoch;
562        let epoch = snapshot.epoch;
563        let progress = if initial_epoch == 0 {
564            0.0
565        } else {
566            let p = 1.0 - ((epoch as f64) / (initial_epoch as f64));
567            if p.is_finite() {
568                p.clamp(0.0, 1.0)
569            } else {
570                0.0
571            }
572        };
573        // only two decimal places
574        let progress = (progress * 100.0).round() / 100.0;
575
576        Ok(ApiExportStatus {
577            state: snapshot.state,
578            kind: snapshot.kind,
579            error: snapshot.error,
580            progress,
581            start_time: snapshot.start_time,
582            current_epoch: epoch,
583            start_epoch: initial_epoch,
584        })
585    }
586}
587
588pub enum ForestChainExportCancel {}
589impl RpcMethod<0> for ForestChainExportCancel {
590    const NAME: &'static str = "Forest.ChainExportCancel";
591    const PARAM_NAMES: [&'static str; 0] = [];
592    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
593    const PERMISSION: Permission = Permission::Read;
594    const DESCRIPTION: &'static str =
595        "Cancels the in-progress chain export, returning whether one was running.";
596
597    type Params = ();
598    type Ok = bool;
599
600    async fn handle(
601        _ctx: Ctx,
602        (): Self::Params,
603        _: &http::Extensions,
604    ) -> Result<Self::Ok, ServerError> {
605        Ok(CHAIN_EXPORT_STATUS.cancel_running())
606    }
607}
608
609/// Parameters for [`IndexBackfill`].
610#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
611#[serde(rename_all = "camelCase")]
612pub struct IndexBackfillParams {
613    /// Starting epoch, inclusive. Defaults to the chain head, unless `resume` is set and a
614    /// persisted resume checkpoint exists.
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub from: Option<ChainEpoch>,
617    /// Ending epoch, inclusive. Mutually exclusive with `n_tipsets`.
618    #[serde(default, skip_serializing_if = "Option::is_none")]
619    pub to: Option<ChainEpoch>,
620    /// Number of tipsets to backfill. Mutually exclusive with `to`.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub n_tipsets: Option<u64>,
623    /// Recompute missing tipset state (expensive) instead of skipping it; tipsets that still can't
624    /// be computed are skipped and reported rather than aborting the run.
625    #[serde(default)]
626    pub recompute: bool,
627    /// Also index revert-prone tipsets newer than the EC-finalized epoch (up to the head). By
628    /// default the walk is clamped to the EC-finalized epoch.
629    #[serde(default)]
630    pub allow_near_head: bool,
631    /// Resume from the persisted checkpoint of a previous run instead of starting at the chain
632    /// head. Ignored when `from` is given.
633    #[serde(default)]
634    pub resume: bool,
635}
636lotus_json_with_self!(IndexBackfillParams);
637
638/// Progress and status of the in-daemon index backfill, returned by [`IndexBackfillStatus`].
639#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
640#[serde(rename_all = "camelCase")]
641pub struct ApiIndexBackfillStatus {
642    pub state: crate::ipld::ChainExportState,
643    pub error: Option<String>,
644    pub progress: f64,
645    pub start_epoch: ChainEpoch,
646    pub current_epoch: ChainEpoch,
647    pub target_epoch: ChainEpoch,
648    pub indexed: u64,
649    pub skipped: u64,
650    pub start_time: Option<chrono::DateTime<chrono::Utc>>,
651}
652lotus_json_with_self!(ApiIndexBackfillStatus);
653
654impl std::fmt::Display for ApiIndexBackfillStatus {
655    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656        use crate::ipld::ChainExportState::*;
657        match self.state {
658            Running => write!(
659                f,
660                "Backfilling: {:.1}% (walk at epoch {}, from {} down to {}; indexed {}, skipped {})",
661                self.progress.clamp(0.0, 1.0) * 100.0,
662                self.current_epoch,
663                self.start_epoch,
664                self.target_epoch,
665                self.indexed,
666                self.skipped,
667            ),
668            Idle => write!(f, "No index backfill in progress"),
669            Succeeded => write!(
670                f,
671                "No index backfill in progress (last run succeeded: indexed {}, skipped {})",
672                self.indexed, self.skipped
673            ),
674            Cancelled => write!(
675                f,
676                "No index backfill in progress (last run was cancelled: indexed {}, skipped {})",
677                self.indexed, self.skipped
678            ),
679            Failed => write!(
680                f,
681                "No index backfill in progress (last run failed: {})",
682                self.error.as_deref().unwrap_or("unknown error")
683            ),
684        }
685    }
686}
687
688pub enum IndexBackfill {}
689impl RpcMethod<1> for IndexBackfill {
690    const NAME: &'static str = "Forest.IndexBackfill";
691    const PARAM_NAMES: [&'static str; 1] = ["params"];
692    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
693    const PERMISSION: Permission = Permission::Admin;
694    const DESCRIPTION: &'static str = "Starts a chain index backfill (Ethereum mappings, events, block blooms) over an epoch range using the running node, returning immediately. Poll `Forest.IndexBackfillStatus` for progress. Only one backfill may run at a time, and it never overlaps snapshot export or the snapshot GC.";
695
696    type Params = (IndexBackfillParams,);
697    type Ok = ();
698
699    async fn handle(
700        ctx: Ctx,
701        (params,): Self::Params,
702        _: &http::Extensions,
703    ) -> Result<Self::Ok, ServerError> {
704        // Validate synchronously so bad requests surface immediately to the caller.
705        let spec = index_backfill_range_spec(&params)?;
706
707        // Best-effort early rejection while snapshot GC runs; mutual exclusion is actually enforced
708        // by the shared chain-export slot and `BackfillGuard::try_start` below.
709        if crate::daemon::GLOBAL_SNAPSHOT_GC
710            .get()
711            .is_some_and(|gc| gc.is_running())
712        {
713            return Err(anyhow::anyhow!(
714                "snapshot GC is currently running; retry the backfill once it completes"
715            )
716            .into());
717        }
718
719        // Acquire the single-flight guard now so "already running" is reported immediately.
720        let guard = crate::daemon::db_util::BackfillGuard::try_start()?;
721
722        // Run detached so the backfill is not cancelled when the CLI client disconnects.
723        // So do not wrap this with `AbortOnDropHandle`.
724        tokio::spawn(async move {
725            let result = run_index_backfill_inner(&ctx, params, spec, &guard).await;
726            let _ = guard.finish(result);
727        });
728        Ok(())
729    }
730}
731
732fn index_backfill_range_spec(
733    params: &IndexBackfillParams,
734) -> Result<crate::daemon::db_util::RangeSpec, ServerError> {
735    let n_tipsets = params.n_tipsets.map(|n| n as usize);
736    Ok(crate::daemon::db_util::RangeSpec::new(
737        params.to, n_tipsets,
738    )?)
739}
740
741async fn run_index_backfill_inner(
742    ctx: &Ctx,
743    params: IndexBackfillParams,
744    spec: crate::daemon::db_util::RangeSpec,
745    guard: &crate::daemon::db_util::BackfillGuard,
746) -> anyhow::Result<()> {
747    use crate::daemon::db_util::{BackfillOptions, read_backfill_checkpoint, run_backfill};
748
749    let head_ts = ctx.chain_store().heaviest_tipset();
750    let checkpoint = if params.resume {
751        read_backfill_checkpoint(&ctx.state_manager)?
752    } else {
753        None
754    };
755    let from_ts = if let Some(from) = params.from {
756        let from = from.min(head_ts.epoch());
757        ctx.chain_index()
758            .load_required_tipset_by_height(from, head_ts, ResolveNullTipset::TakeOlder)
759            .await?
760    } else if let Some(checkpoint) = checkpoint {
761        let checkpoint = checkpoint.min(head_ts.epoch());
762        tracing::info!("Resuming index backfill from checkpoint epoch {checkpoint}");
763        ctx.chain_index()
764            .load_required_tipset_by_height(checkpoint, head_ts, ResolveNullTipset::TakeOlder)
765            .await?
766    } else {
767        head_ts
768    };
769
770    let options = BackfillOptions {
771        allow_recompute: params.recompute,
772        allow_near_head: params.allow_near_head,
773        ..BackfillOptions::default()
774    };
775
776    run_backfill(&ctx.state_manager, &from_ts, spec, options, guard).await?;
777    Ok(())
778}
779
780pub enum IndexBackfillStatus {}
781impl RpcMethod<0> for IndexBackfillStatus {
782    const NAME: &'static str = "Forest.IndexBackfillStatus";
783    const PARAM_NAMES: [&'static str; 0] = [];
784    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
785    const PERMISSION: Permission = Permission::Read;
786    const DESCRIPTION: &'static str =
787        "Returns the progress and status of the in-progress (or last) index backfill.";
788
789    type Params = ();
790    type Ok = ApiIndexBackfillStatus;
791
792    async fn handle(
793        _ctx: Ctx,
794        (): Self::Params,
795        _: &http::Extensions,
796    ) -> Result<Self::Ok, ServerError> {
797        let snapshot = crate::daemon::db_util::BACKFILL_STATUS.snapshot();
798        // Progress is the fraction of the epoch span walked (the backfill counts downward).
799        let span = snapshot.start_epoch - snapshot.target_epoch;
800        let progress = if span > 0 {
801            let walked = snapshot.start_epoch - snapshot.current_epoch;
802            ((walked as f64) / (span as f64)).clamp(0.0, 1.0)
803        } else {
804            0.0
805        };
806        let progress = (progress * 100.0).round() / 100.0;
807        Ok(ApiIndexBackfillStatus {
808            state: snapshot.state,
809            error: snapshot.error,
810            progress,
811            start_epoch: snapshot.start_epoch,
812            current_epoch: snapshot.current_epoch,
813            target_epoch: snapshot.target_epoch,
814            indexed: snapshot.indexed,
815            skipped: snapshot.skipped,
816            start_time: snapshot.start_time,
817        })
818    }
819}
820
821pub enum IndexBackfillCancel {}
822impl RpcMethod<0> for IndexBackfillCancel {
823    const NAME: &'static str = "Forest.IndexBackfillCancel";
824    const PARAM_NAMES: [&'static str; 0] = [];
825    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
826    const PERMISSION: Permission = Permission::Admin;
827    const DESCRIPTION: &'static str =
828        "Cancels the in-progress index backfill, returning whether one was running.";
829
830    type Params = ();
831    type Ok = bool;
832
833    async fn handle(
834        _ctx: Ctx,
835        (): Self::Params,
836        _: &http::Extensions,
837    ) -> Result<Self::Ok, ServerError> {
838        Ok(crate::daemon::db_util::BACKFILL_STATUS.cancel_running())
839    }
840}
841
842pub enum ForestChainExportDiff {}
843impl RpcMethod<1> for ForestChainExportDiff {
844    const NAME: &'static str = "Forest.ChainExportDiff";
845    const PARAM_NAMES: [&'static str; 1] = ["params"];
846    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
847    const PERMISSION: Permission = Permission::Read;
848    const DESCRIPTION: &'static str =
849        "Exports a differential snapshot covering the given epoch range to a CAR file.";
850
851    type Params = (ForestChainExportDiffParams,);
852    type Ok = ApiExportResult;
853
854    async fn handle(
855        ctx: Ctx,
856        (params,): Self::Params,
857        _: &http::Extensions,
858    ) -> Result<Self::Ok, ServerError> {
859        // Spawn a task so it's not cancelled when CLI client is disconnected
860        // So do not wrap this with `AbortOnDropHandle`
861        let handle = tokio::spawn(async move {
862            let chain_export_guard =
863                ChainExportGuard::try_start_export(ChainExportKind::DiffSnapshot)?;
864            let result = export_diff_inner(&ctx, params, &chain_export_guard).await;
865            chain_export_guard.finish(result)
866        });
867        Ok(handle.await??)
868    }
869}
870
871async fn export_diff_inner(
872    ctx: &Ctx,
873    params: ForestChainExportDiffParams,
874    chain_export_guard: &ChainExportGuard,
875) -> anyhow::Result<ApiExportResult> {
876    let ForestChainExportDiffParams {
877        from,
878        to,
879        depth,
880        output_path,
881    } = params;
882
883    let chain_finality = ctx.chain_config().policy.chain_finality;
884    anyhow::ensure!(
885        depth >= chain_finality,
886        "depth {depth} must be greater than or equal to chain_finality {chain_finality}"
887    );
888
889    let head = ctx.chain_store().heaviest_tipset();
890    let start_ts = ctx
891        .chain_index()
892        .load_required_tipset_by_height(from, head, ResolveNullTipset::TakeOlder)
893        .await?;
894    let tmp_path = tempfile::TempPath::try_from_path(tmp_exporting_forest_car_path(&output_path))?;
895    let chain_export = crate::tool::subcommands::archive_cmd::do_export(
896        ctx.chain_index().db(),
897        start_ts,
898        Some(ctx.chain_store().genesis_tipset()),
899        tmp_path.to_path_buf(),
900        None,
901        depth,
902        Some(to),
903        Some(chain_finality),
904        true,
905    );
906
907    match chain_export_guard.run_cancellable(chain_export).await {
908        Some(result) => {
909            result?;
910            spawn_blocking_with_timeout(ASYNC_OPS_TIMEOUT, move || {
911                Ok(tmp_path.persist(&output_path)?)
912            })
913            .await
914            .context("failed to persist the exported snapshot")?;
915            Ok(ApiExportResult::Done)
916        }
917        None => {
918            tracing::warn!("Diff snapshot export was cancelled");
919            Ok(ApiExportResult::Cancelled)
920        }
921    }
922}
923
924pub enum ChainExport {}
925impl RpcMethod<1> for ChainExport {
926    const NAME: &'static str = "Filecoin.ChainExport";
927    const PARAM_NAMES: [&'static str; 1] = ["params"];
928    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
929    const PERMISSION: Permission = Permission::Read;
930    const DESCRIPTION: &'static str =
931        "Exports a v1 chain snapshot to a CAR file from the given epoch.";
932
933    type Params = (ChainExportParams,);
934    type Ok = ApiExportResult;
935
936    async fn handle(
937        ctx: Ctx,
938        (ChainExportParams {
939            epoch,
940            recent_roots,
941            output_path,
942            tipset_keys,
943            skip_checksum,
944            dry_run,
945        },): Self::Params,
946        ext: &http::Extensions,
947    ) -> Result<Self::Ok, ServerError> {
948        ForestChainExport::handle(
949            ctx,
950            (ForestChainExportParams {
951                version: FilecoinSnapshotVersion::V1,
952                epoch,
953                recent_roots,
954                output_path,
955                tipset_keys,
956                include_receipts: false,
957                include_events: false,
958                include_tipset_keys: false,
959                augmented_snapshot: false,
960                tipset_lookup: false,
961                skip_checksum,
962                dry_run,
963            },),
964            ext,
965        )
966        .await
967    }
968}
969
970pub enum ChainReadObj {}
971impl RpcMethod<1> for ChainReadObj {
972    const NAME: &'static str = "Filecoin.ChainReadObj";
973    const PARAM_NAMES: [&'static str; 1] = ["cid"];
974    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
975    const PERMISSION: Permission = Permission::Read;
976    const DESCRIPTION: &'static str = "Reads IPLD nodes referenced by the specified CID from the chain blockstore and returns raw bytes.";
977
978    type Params = (Cid,);
979    type Ok = Vec<u8>;
980
981    async fn handle(
982        ctx: Ctx,
983        (cid,): Self::Params,
984        _: &http::Extensions,
985    ) -> Result<Self::Ok, ServerError> {
986        let bytes = ctx
987            .db()
988            .get(&cid)?
989            .with_context(|| format!("can't find object with cid={cid}"))?;
990        Ok(bytes)
991    }
992}
993
994pub enum ChainHasObj {}
995impl RpcMethod<1> for ChainHasObj {
996    const NAME: &'static str = "Filecoin.ChainHasObj";
997    const PARAM_NAMES: [&'static str; 1] = ["cid"];
998    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
999    const PERMISSION: Permission = Permission::Read;
1000    const DESCRIPTION: &'static str = "Checks if a given CID exists in the chain blockstore.";
1001
1002    type Params = (Cid,);
1003    type Ok = bool;
1004
1005    async fn handle(
1006        ctx: Ctx,
1007        (cid,): Self::Params,
1008        _: &http::Extensions,
1009    ) -> Result<Self::Ok, ServerError> {
1010        Ok(ctx.db().get(&cid)?.is_some())
1011    }
1012}
1013
1014/// Returns statistics about the graph referenced by 'obj'.
1015/// If 'base' is also specified, then the returned stat will be a diff between the two objects.
1016pub enum ChainStatObj {}
1017impl RpcMethod<2> for ChainStatObj {
1018    const NAME: &'static str = "Filecoin.ChainStatObj";
1019    const PARAM_NAMES: [&'static str; 2] = ["objCid", "baseCid"];
1020    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1021    const PERMISSION: Permission = Permission::Read;
1022    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.";
1023
1024    type Params = (Cid, Option<Cid>);
1025    type Ok = ObjStat;
1026
1027    async fn handle(
1028        ctx: Ctx,
1029        (obj_cid, base_cid): Self::Params,
1030        _: &http::Extensions,
1031    ) -> Result<Self::Ok, ServerError> {
1032        let mut stats = ObjStat::default();
1033        let mut seen = CidHashSet::default();
1034        let mut walk = |cid, collect| {
1035            let mut queue = VecDeque::new();
1036            queue.push_back(cid);
1037            while let Some(link_cid) = queue.pop_front() {
1038                if !seen.insert(link_cid) {
1039                    continue;
1040                }
1041                let data = ctx.db().get(&link_cid)?;
1042                if let Some(data) = data {
1043                    if collect {
1044                        stats.links += 1;
1045                        stats.size += data.len();
1046                    }
1047                    if matches!(link_cid.codec(), fvm_ipld_encoding::DAG_CBOR)
1048                        && let Ok(ipld) =
1049                            crate::utils::encoding::from_slice_with_fallback::<Ipld>(&data)
1050                    {
1051                        for ipld in DfsIter::new(ipld) {
1052                            if let Ipld::Link(cid) = ipld {
1053                                queue.push_back(cid);
1054                            }
1055                        }
1056                    }
1057                }
1058            }
1059            anyhow::Ok(())
1060        };
1061        if let Some(base_cid) = base_cid {
1062            walk(base_cid, false)?;
1063        }
1064        walk(obj_cid, true)?;
1065        Ok(stats)
1066    }
1067}
1068
1069pub enum ChainGetBlockMessages {}
1070impl RpcMethod<1> for ChainGetBlockMessages {
1071    const NAME: &'static str = "Filecoin.ChainGetBlockMessages";
1072    const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
1073    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1074    const PERMISSION: Permission = Permission::Read;
1075    const DESCRIPTION: &'static str = "Returns all messages from the specified block.";
1076
1077    type Params = (Cid,);
1078    type Ok = BlockMessages;
1079
1080    async fn handle(
1081        ctx: Ctx,
1082        (block_cid,): Self::Params,
1083        _: &http::Extensions,
1084    ) -> Result<Self::Ok, ServerError> {
1085        let blk: CachingBlockHeader = ctx.db().get_cbor_required(&block_cid)?;
1086        let (unsigned_cids, signed_cids) = crate::chain::read_msg_cids(ctx.db(), &blk)?;
1087        let (bls_msg, secp_msg) =
1088            crate::chain::block_messages_from_cids(ctx.db(), &unsigned_cids, &signed_cids)?;
1089        let cids = unsigned_cids.into_iter().chain(signed_cids).collect();
1090
1091        let ret = BlockMessages {
1092            bls_msg,
1093            secp_msg,
1094            cids,
1095        };
1096        Ok(ret)
1097    }
1098}
1099
1100pub enum ChainGetPath {}
1101impl RpcMethod<2> for ChainGetPath {
1102    const NAME: &'static str = "Filecoin.ChainGetPath";
1103    const PARAM_NAMES: [&'static str; 2] = ["from", "to"];
1104    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1105    const PERMISSION: Permission = Permission::Read;
1106    const DESCRIPTION: &'static str = "Returns the path between the two specified tipsets.";
1107
1108    type Params = (TipsetKey, TipsetKey);
1109    type Ok = Vec<PathChange>;
1110
1111    async fn handle(
1112        ctx: Ctx,
1113        (from, to): Self::Params,
1114        _: &http::Extensions,
1115    ) -> Result<Self::Ok, ServerError> {
1116        Ok(chain_get_path(ctx.chain_store(), &from, &to)?.into_change_vec())
1117    }
1118}
1119
1120/// Find the path between two tipsets, as [`PathChanges`].
1121///
1122/// ```text
1123/// 0 - A - B - C - D
1124///     ^~~~~~~~> apply B, C
1125///
1126/// 0 - A - B - C - D
1127///     <~~~~~~~^ revert C, B
1128///
1129///     <~~~~~~~~ revert C, B
1130/// 0 - A - B  - C
1131///     |
1132///      -- B' - C'
1133///      ~~~~~~~~> then apply B', C'
1134/// ```
1135///
1136/// Exposes errors from the [`Blockstore`], and returns an error if there is no common ancestor.
1137pub fn chain_get_path(
1138    chain_store: &ChainStore,
1139    from: &TipsetKey,
1140    to: &TipsetKey,
1141) -> anyhow::Result<PathChanges> {
1142    let finality = chain_store.chain_config().policy.chain_finality;
1143    let mut to_revert = chain_store
1144        .load_required_tipset_or_heaviest(from)
1145        .context("couldn't load `from`")?;
1146    let mut to_apply = chain_store
1147        .load_required_tipset_or_heaviest(to)
1148        .context("couldn't load `to`")?;
1149
1150    anyhow::ensure!(
1151        (to_apply.epoch() - to_revert.epoch()).abs() <= finality,
1152        "the gap between the new head ({}) and the old head ({}) is larger than chain finality ({finality})",
1153        to_apply.epoch(),
1154        to_revert.epoch()
1155    );
1156
1157    let mut reverts = vec![];
1158    let mut applies = vec![];
1159
1160    // This loop is guaranteed to terminate if the blockstore contain no cycles.
1161    // This is currently computationally infeasible.
1162    while to_revert != to_apply {
1163        if to_revert.epoch() > to_apply.epoch() {
1164            let next = chain_store
1165                .load_required_tipset_or_heaviest(to_revert.parents())
1166                .context("couldn't load ancestor of `from`")?;
1167            reverts.push(to_revert);
1168            to_revert = next;
1169        } else {
1170            let next = chain_store
1171                .load_required_tipset_or_heaviest(to_apply.parents())
1172                .context("couldn't load ancestor of `to`")?;
1173            applies.push(to_apply);
1174            to_apply = next;
1175        }
1176    }
1177    applies.reverse();
1178    Ok(PathChanges { reverts, applies })
1179}
1180
1181/// Get tipset at epoch. Pick younger tipset if epoch points to a
1182/// null-tipset. Only tipsets below the given `head` are searched. If `head`
1183/// is null, the node will use the heaviest tipset.
1184pub enum ChainGetTipSetByHeight {}
1185impl RpcMethod<2> for ChainGetTipSetByHeight {
1186    const NAME: &'static str = "Filecoin.ChainGetTipSetByHeight";
1187    const PARAM_NAMES: [&'static str; 2] = ["height", "tipsetKey"];
1188    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1189    const PERMISSION: Permission = Permission::Read;
1190    const DESCRIPTION: &'static str = "Returns the tipset at the specified height.";
1191
1192    type Params = (ChainEpoch, ApiTipsetKey);
1193    type Ok = Tipset;
1194
1195    async fn handle(
1196        ctx: Ctx,
1197        (height, ApiTipsetKey(tipset_key)): Self::Params,
1198        _: &http::Extensions,
1199    ) -> Result<Self::Ok, ServerError> {
1200        let ts = ctx
1201            .chain_store()
1202            .load_required_tipset_or_heaviest(&tipset_key)?;
1203        let tss = ctx
1204            .chain_index()
1205            .load_required_tipset_by_height(height, ts, ResolveNullTipset::TakeOlder)
1206            .await?;
1207        Ok(tss)
1208    }
1209}
1210
1211pub enum ChainGetTipSetAfterHeight {}
1212impl RpcMethod<2> for ChainGetTipSetAfterHeight {
1213    const NAME: &'static str = "Filecoin.ChainGetTipSetAfterHeight";
1214    const PARAM_NAMES: [&'static str; 2] = ["height", "tipsetKey"];
1215    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1216    const PERMISSION: Permission = Permission::Read;
1217    const DESCRIPTION: &'static str = "Looks back and returns the tipset at the specified epoch.
1218    If there are no blocks at the given epoch,
1219    returns the first non-nil tipset at a later epoch.";
1220
1221    type Params = (ChainEpoch, ApiTipsetKey);
1222    type Ok = Tipset;
1223
1224    async fn handle(
1225        ctx: Ctx,
1226        (height, ApiTipsetKey(tipset_key)): Self::Params,
1227        _: &http::Extensions,
1228    ) -> Result<Self::Ok, ServerError> {
1229        let ts = ctx
1230            .chain_store()
1231            .load_required_tipset_or_heaviest(&tipset_key)?;
1232        let tss = ctx
1233            .chain_index()
1234            .load_required_tipset_by_height(height, ts, ResolveNullTipset::TakeNewer)
1235            .await?;
1236        Ok(tss)
1237    }
1238}
1239
1240pub enum ChainGetGenesis {}
1241impl RpcMethod<0> for ChainGetGenesis {
1242    const NAME: &'static str = "Filecoin.ChainGetGenesis";
1243    const PARAM_NAMES: [&'static str; 0] = [];
1244    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1245    const PERMISSION: Permission = Permission::Read;
1246    const DESCRIPTION: &'static str = "Returns the genesis tipset of the chain.";
1247
1248    type Params = ();
1249    type Ok = Option<Tipset>;
1250
1251    async fn handle(
1252        ctx: Ctx,
1253        (): Self::Params,
1254        _: &http::Extensions,
1255    ) -> Result<Self::Ok, ServerError> {
1256        let genesis = ctx.chain_store().genesis_block_header();
1257        Ok(Some(Tipset::from(genesis)))
1258    }
1259}
1260
1261pub enum ChainHead {}
1262impl RpcMethod<0> for ChainHead {
1263    const NAME: &'static str = "Filecoin.ChainHead";
1264    const PARAM_NAMES: [&'static str; 0] = [];
1265    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1266    const PERMISSION: Permission = Permission::Read;
1267    const DESCRIPTION: &'static str = "Returns the chain head (heaviest tipset).";
1268
1269    type Params = ();
1270    type Ok = Tipset;
1271
1272    async fn handle(
1273        ctx: Ctx,
1274        (): Self::Params,
1275        _: &http::Extensions,
1276    ) -> Result<Self::Ok, ServerError> {
1277        let heaviest = ctx.chain_store().heaviest_tipset();
1278        Ok(heaviest)
1279    }
1280}
1281
1282pub enum ChainGetBlock {}
1283impl RpcMethod<1> for ChainGetBlock {
1284    const NAME: &'static str = "Filecoin.ChainGetBlock";
1285    const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
1286    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1287    const PERMISSION: Permission = Permission::Read;
1288    const DESCRIPTION: &'static str = "Returns the block with the specified CID.";
1289
1290    type Params = (Cid,);
1291    type Ok = CachingBlockHeader;
1292
1293    async fn handle(
1294        ctx: Ctx,
1295        (block_cid,): Self::Params,
1296        _: &http::Extensions,
1297    ) -> Result<Self::Ok, ServerError> {
1298        let blk: CachingBlockHeader = ctx.db().get_cbor_required(&block_cid)?;
1299        Ok(blk)
1300    }
1301}
1302
1303pub enum ChainGetTipSet {}
1304
1305impl RpcMethod<1> for ChainGetTipSet {
1306    const NAME: &'static str = "Filecoin.ChainGetTipSet";
1307    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1308    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V0 | V1 });
1309    const PERMISSION: Permission = Permission::Read;
1310    const DESCRIPTION: &'static str = "Returns the tipset with the specified CID.";
1311
1312    type Params = (ApiTipsetKey,);
1313    type Ok = Tipset;
1314
1315    async fn handle(
1316        ctx: Ctx,
1317        (ApiTipsetKey(tsk),): Self::Params,
1318        _: &http::Extensions,
1319    ) -> Result<Self::Ok, ServerError> {
1320        if let Some(tsk) = &tsk {
1321            let ts = ctx.chain_index().load_required_tipset(tsk)?;
1322            Ok(ts)
1323        } else {
1324            // It contains Lotus error message `NewTipSet called with zero length array of blocks` for parity tests
1325            Err(anyhow::anyhow!(
1326                "TipsetKey cannot be empty (NewTipSet called with zero length array of blocks)"
1327            )
1328            .into())
1329        }
1330    }
1331}
1332
1333pub enum ChainGetTipSetV2 {}
1334
1335impl ChainGetTipSetV2 {
1336    pub async fn get_tipset_by_anchor(
1337        ctx: &Ctx,
1338        anchor: Option<&TipsetAnchor>,
1339    ) -> anyhow::Result<Tipset> {
1340        if let Some(anchor) = anchor {
1341            match (&anchor.key.0, &anchor.tag) {
1342                // Anchor is zero-valued. Fall back to heaviest tipset.
1343                (None, None) => Ok(ctx.state_manager.heaviest_tipset()),
1344                // Get tipset at the specified key.
1345                (Some(tsk), None) => Ok(ctx.chain_index().load_required_tipset(tsk)?),
1346                (None, Some(tag)) => Self::get_tipset_by_tag(ctx, *tag).await,
1347                _ => {
1348                    anyhow::bail!("invalid anchor")
1349                }
1350            }
1351        } else {
1352            // No anchor specified. Fall back to finalized tipset.
1353            Self::get_tipset_by_tag(ctx, TipsetTag::Finalized).await
1354        }
1355    }
1356
1357    pub async fn get_tipset_by_tag(ctx: &Ctx, tag: TipsetTag) -> anyhow::Result<Tipset> {
1358        match tag {
1359            TipsetTag::Latest => Ok(ctx.state_manager.heaviest_tipset()),
1360            TipsetTag::Finalized => Self::get_latest_finalized_tipset(ctx).await,
1361            TipsetTag::Safe => Self::get_latest_safe_tipset(ctx).await,
1362        }
1363    }
1364
1365    pub async fn get_latest_safe_tipset(ctx: &Ctx) -> anyhow::Result<Tipset> {
1366        let finalized = Self::get_latest_finalized_tipset(ctx).await?;
1367        let head = ctx.chain_store().heaviest_tipset();
1368        let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0);
1369        if finalized.epoch() >= safe_height {
1370            Ok(finalized)
1371        } else {
1372            Ok(ctx
1373                .chain_index()
1374                .load_required_tipset_by_height(safe_height, head, ResolveNullTipset::TakeOlder)
1375                .await?)
1376        }
1377    }
1378
1379    pub async fn get_latest_finalized_tipset(ctx: &Ctx) -> anyhow::Result<Tipset> {
1380        ChainGetTipSetFinalityStatus::get_finality_status(ctx)
1381            .await?
1382            .finalized_tip_set
1383            .context("failed to resolve finalized tipset")
1384    }
1385
1386    pub async fn get_tipset(ctx: &Ctx, selector: &TipsetSelector) -> anyhow::Result<Tipset> {
1387        selector.validate()?;
1388        // Get tipset by key.
1389        if let ApiTipsetKey(Some(tsk)) = &selector.key {
1390            let ts = ctx.chain_index().load_required_tipset(tsk)?;
1391            return Ok(ts);
1392        }
1393        // Get tipset by height.
1394        if let Some(height) = &selector.height {
1395            let anchor = Self::get_tipset_by_anchor(ctx, height.anchor.as_ref()).await?;
1396            let ts = ctx
1397                .chain_index()
1398                .load_required_tipset_by_height(
1399                    height.at,
1400                    anchor,
1401                    height.resolve_null_tipset_policy(),
1402                )
1403                .await?;
1404            return Ok(ts);
1405        }
1406        // Get tipset by tag, either latest or finalized.
1407        if let Some(tag) = &selector.tag {
1408            let ts = Self::get_tipset_by_tag(ctx, *tag).await?;
1409            return Ok(ts);
1410        }
1411        anyhow::bail!("no tipset found for selector")
1412    }
1413}
1414
1415impl RpcMethod<1> for ChainGetTipSetV2 {
1416    const NAME: &'static str = "Filecoin.ChainGetTipSet";
1417    const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"];
1418    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1419    const PERMISSION: Permission = Permission::Read;
1420    const DESCRIPTION: &'static str = "Returns the tipset with the specified CID.";
1421
1422    type Params = (TipsetSelector,);
1423    type Ok = Tipset;
1424
1425    async fn handle(
1426        ctx: Ctx,
1427        (selector,): Self::Params,
1428        _: &http::Extensions,
1429    ) -> Result<Self::Ok, ServerError> {
1430        Ok(Self::get_tipset(&ctx, &selector).await?)
1431    }
1432}
1433
1434pub enum ChainGetTipSetFinalityStatus {}
1435
1436const EC_CALCULATOR_FINALITY_CACHE_SIZE: usize = 4;
1437impl ChainGetTipSetFinalityStatus {
1438    pub async fn get_finality_status(ctx: &Ctx) -> anyhow::Result<ChainFinalityStatus> {
1439        let head = ctx.chain_store().heaviest_tipset();
1440        let (ec_finality_threshold_depth, ec_finalized_tip_set) =
1441            Self::get_ec_finality_threshold_depth_and_tipset_with_cache(ctx, head.shallow_clone())
1442                .await?;
1443        let f3_finalized_tip_set = ctx.chain_store().f3_finalized_tipset();
1444        let finalized_tip_set = match (&ec_finalized_tip_set, &f3_finalized_tip_set) {
1445            (Some(ec), Some(f3)) => {
1446                if ec.epoch() >= f3.epoch() {
1447                    Some(ec.shallow_clone())
1448                } else {
1449                    Some(f3.shallow_clone())
1450                }
1451            }
1452            (Some(ec), None) => Some(ec.shallow_clone()),
1453            (None, Some(f3)) => Some(f3.shallow_clone()),
1454            (None, None) => None,
1455        };
1456        Ok(ChainFinalityStatus {
1457            ec_finality_threshold_depth,
1458            ec_finalized_tip_set,
1459            f3_finalized_tip_set,
1460            finalized_tip_set,
1461            head,
1462        })
1463    }
1464
1465    pub async fn get_ec_finality_threshold_depth_and_tipset_with_cache(
1466        ctx: &Ctx,
1467        head: Tipset,
1468    ) -> anyhow::Result<(i64, Option<Tipset>)> {
1469        static CACHE: LazyLock<quick_cache::sync::Cache<TipsetKey, (i64, Option<Tipset>)>> =
1470            LazyLock::new(|| quick_cache::sync::Cache::new(EC_CALCULATOR_FINALITY_CACHE_SIZE));
1471        CACHE
1472            .get_or_insert_async(
1473                head.shallow_clone().key(),
1474                Self::get_ec_finality_threshold_depth_and_tipset(ctx, head),
1475            )
1476            .await
1477    }
1478
1479    pub fn get_ec_finality_epoch(
1480        chain_index: &ChainIndex,
1481        chain_config: &ChainConfig,
1482        head: &Tipset,
1483    ) -> i64 {
1484        let depth =
1485            Self::get_ec_finality_threshold_depth_with_cache(chain_index, chain_config, head);
1486        Self::get_ec_finality_epoch_by_depth(chain_config, head, depth)
1487    }
1488
1489    fn get_ec_finality_epoch_by_depth(
1490        chain_config: &ChainConfig,
1491        head: &Tipset,
1492        depth: i64,
1493    ) -> i64 {
1494        let depth = if depth >= 0 {
1495            depth
1496        } else {
1497            chain_config.policy.chain_finality
1498        };
1499        (head.epoch() - depth).max(0)
1500    }
1501
1502    fn get_ec_finality_threshold_depth_with_cache(
1503        chain_index: &ChainIndex,
1504        chain_config: &ChainConfig,
1505        head: &Tipset,
1506    ) -> i64 {
1507        static CACHE: LazyLock<quick_cache::sync::Cache<TipsetKey, i64>> =
1508            LazyLock::new(|| quick_cache::sync::Cache::new(EC_CALCULATOR_FINALITY_CACHE_SIZE));
1509        CACHE
1510            .get_or_insert_with(head.key(), move || -> Result<i64, Infallible> {
1511                Ok(Self::get_ec_finality_threshold_depth(
1512                    chain_index,
1513                    chain_config,
1514                    head,
1515                ))
1516            })
1517            .expect("infallible")
1518    }
1519
1520    fn get_ec_finality_threshold_depth(
1521        chain_index: &ChainIndex,
1522        chain_config: &ChainConfig,
1523        head: &Tipset,
1524    ) -> i64 {
1525        use crate::chain::ec_finality::calculator::{
1526            DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE,
1527            find_threshold_depth,
1528        };
1529
1530        /// Number of extra epochs to fetch beyond [`chain_finality`] when
1531        /// building the chain sample for [`find_threshold_depth`].
1532        ///
1533        /// The extra 5 epochs act as a tail buffer to prevent out-of-bounds access,
1534        /// particularly when null rounds (epochs with zero blocks) are present, since
1535        /// they consume array slots without advancing the meaningful epoch count.
1536        const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5;
1537
1538        let finality = chain_config.policy.chain_finality;
1539        let chain_len = finality as usize + FINALITY_CHAIN_EXTRA_EPOCHS;
1540        let mut chain = Vec::with_capacity(chain_len);
1541        let mut ts = head.shallow_clone();
1542        while chain.len() < chain_len {
1543            chain.push(ts.len() as i64);
1544            if let Ok(parent) = chain_index.load_required_tipset(ts.parents()) {
1545                // insert 0 for null rounds
1546                let pad = usize::try_from(ts.epoch() - parent.epoch() - 1).unwrap_or_default();
1547                chain.resize(chain.len().saturating_add(pad).min(chain_len), 0);
1548                ts = parent;
1549            } else {
1550                break;
1551            }
1552        }
1553        // Reverse to chronological order (oldest first).
1554        chain.reverse();
1555        match find_threshold_depth(
1556            &chain,
1557            finality,
1558            DEFAULT_BLOCKS_PER_EPOCH,
1559            DEFAULT_BYZANTINE_FRACTION,
1560            *DEFAULT_GUARANTEE,
1561        ) {
1562            Ok(threshold) => threshold,
1563            Err(e) => {
1564                tracing::error!(
1565                    "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}"
1566                );
1567                -1
1568            }
1569        }
1570    }
1571
1572    async fn get_ec_finality_threshold_depth_and_tipset(
1573        ctx: &Ctx,
1574        head: Tipset,
1575    ) -> anyhow::Result<(i64, Option<Tipset>)> {
1576        let depth = Self::get_ec_finality_threshold_depth_with_cache(
1577            ctx.chain_index(),
1578            ctx.chain_config(),
1579            &head,
1580        );
1581        let ec_finality_epoch =
1582            Self::get_ec_finality_epoch_by_depth(ctx.chain_config(), &head, depth);
1583        let finalized = ctx
1584            .chain_index()
1585            .tipset_by_height(ec_finality_epoch, head, ResolveNullTipset::TakeOlder)
1586            .await?;
1587        Ok((depth, finalized))
1588    }
1589}
1590
1591impl RpcMethod<0> for ChainGetTipSetFinalityStatus {
1592    const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus";
1593    const PARAM_NAMES: [&'static str; 0] = [];
1594    const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1595    const PERMISSION: Permission = Permission::Read;
1596    const DESCRIPTION: &'static str =
1597        "Returns a breakdown of how the node is currently determining finality.";
1598
1599    type Params = ();
1600    type Ok = ChainFinalityStatus;
1601
1602    async fn handle(
1603        ctx: Ctx,
1604        (): Self::Params,
1605        _: &http::Extensions,
1606    ) -> Result<Self::Ok, ServerError> {
1607        Ok(Self::get_finality_status(&ctx).await?)
1608    }
1609}
1610
1611pub enum ChainSetHead {}
1612impl RpcMethod<1> for ChainSetHead {
1613    const NAME: &'static str = "Filecoin.ChainSetHead";
1614    const PARAM_NAMES: [&'static str; 1] = ["tsk"];
1615    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1616    const PERMISSION: Permission = Permission::Admin;
1617    const DESCRIPTION: &'static str =
1618        "Forcibly sets the chain head to the tipset with the given key.";
1619
1620    type Params = (TipsetKey,);
1621    type Ok = ();
1622
1623    async fn handle(
1624        ctx: Ctx,
1625        (tsk,): Self::Params,
1626        _: &http::Extensions,
1627    ) -> Result<Self::Ok, ServerError> {
1628        // This is basically a port of the reference implementation at
1629        // https://github.com/filecoin-project/lotus/blob/v1.23.0/node/impl/full/chain.go#L321
1630
1631        let new_head = ctx.chain_index().load_required_tipset(&tsk)?;
1632        let mut current = ctx.chain_store().heaviest_tipset();
1633        while current.epoch() >= new_head.epoch() {
1634            for cid in current.key().to_cids() {
1635                ctx.chain_store().unmark_block_as_validated(&cid);
1636            }
1637            let parents = &current.block_headers().first().parents;
1638            current = ctx.chain_index().load_required_tipset(parents)?;
1639        }
1640        ctx.chain_store()
1641            .set_heaviest_tipset(new_head)
1642            .map_err(Into::into)
1643    }
1644}
1645
1646pub enum ChainGetMinBaseFee {}
1647impl RpcMethod<1> for ChainGetMinBaseFee {
1648    const NAME: &'static str = "Forest.ChainGetMinBaseFee";
1649    const PARAM_NAMES: [&'static str; 1] = ["lookback"];
1650    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1651    const PERMISSION: Permission = Permission::Read;
1652    const DESCRIPTION: &'static str =
1653        "Returns the minimum base fee across the given number of lookback tipsets, in attoFIL.";
1654
1655    type Params = (u32,);
1656    type Ok = String;
1657
1658    async fn handle(
1659        ctx: Ctx,
1660        (lookback,): Self::Params,
1661        _: &http::Extensions,
1662    ) -> Result<Self::Ok, ServerError> {
1663        let mut current = ctx.chain_store().heaviest_tipset();
1664        let mut min_base_fee = current.block_headers().first().parent_base_fee.clone();
1665
1666        for _ in 0..lookback {
1667            let parents = &current.block_headers().first().parents;
1668            current = ctx.chain_index().load_required_tipset(parents)?;
1669
1670            min_base_fee =
1671                min_base_fee.min(current.block_headers().first().parent_base_fee.to_owned());
1672        }
1673
1674        Ok(min_base_fee.atto().to_string())
1675    }
1676}
1677
1678pub enum ChainTipSetWeight {}
1679impl RpcMethod<1> for ChainTipSetWeight {
1680    const NAME: &'static str = "Filecoin.ChainTipSetWeight";
1681    const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1682    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1683    const PERMISSION: Permission = Permission::Read;
1684    const DESCRIPTION: &'static str = "Returns the weight of the specified tipset.";
1685
1686    type Params = (ApiTipsetKey,);
1687    type Ok = BigInt;
1688
1689    async fn handle(
1690        ctx: Ctx,
1691        (ApiTipsetKey(tipset_key),): Self::Params,
1692        _: &http::Extensions,
1693    ) -> Result<Self::Ok, ServerError> {
1694        let ts = ctx
1695            .chain_store()
1696            .load_required_tipset_or_heaviest(&tipset_key)?;
1697        let weight = crate::fil_cns::weight(ctx.db(), &ts)?;
1698        Ok(weight)
1699    }
1700}
1701
1702pub enum ChainGetTipsetByParentState {}
1703impl RpcMethod<1> for ChainGetTipsetByParentState {
1704    const NAME: &'static str = "Forest.ChainGetTipsetByParentState";
1705    const PARAM_NAMES: [&'static str; 1] = ["parentState"];
1706    const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1707    const PERMISSION: Permission = Permission::Read;
1708    const DESCRIPTION: &'static str = "Returns the tipset whose parent state root matches the given CID, or null if none is found.";
1709
1710    type Params = (Cid,);
1711    type Ok = Option<Tipset>;
1712
1713    async fn handle(
1714        ctx: Ctx,
1715        (parent_state,): Self::Params,
1716        _: &http::Extensions,
1717    ) -> Result<Self::Ok, ServerError> {
1718        Ok(ctx
1719            .chain_store()
1720            .heaviest_tipset()
1721            .chain(ctx.db())
1722            .find(|ts| ts.parent_state() == &parent_state)
1723            .shallow_clone())
1724    }
1725}
1726
1727pub const CHAIN_NOTIFY: &str = "Filecoin.ChainNotify";
1728pub(crate) fn chain_notify(
1729    _params: Params<'_>,
1730    data: &crate::rpc::RPCState,
1731) -> Subscriber<Vec<ApiHeadChange>> {
1732    chain_notify_inner(data.chain_store())
1733}
1734
1735fn chain_notify_inner(chain_store: &ChainStore) -> Subscriber<Vec<ApiHeadChange>> {
1736    let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
1737
1738    // Subscribe before sampling the head, else a change landing in between is lost.
1739    let head_changes_rx = chain_store.subscribe_head_changes();
1740    let current = chain_store.heaviest_tipset();
1741    sender
1742        .send(vec![ApiHeadChange {
1743            change: HeadChangeType::Current,
1744            tipset: current,
1745        }])
1746        .expect("receiver is not dropped");
1747
1748    tokio::spawn(async move {
1749        while let Ok(changes) = head_changes_rx.recv_async().await {
1750            let api_changes = changes.into_change_vec().into_iter().map_into().collect();
1751            if sender.send(api_changes).is_err() {
1752                tracing::info!("chain notify subscribers are all closed");
1753                break;
1754            }
1755        }
1756        tracing::info!("head changes channel closed");
1757    });
1758    receiver
1759}
1760
1761async fn load_api_messages_from_tipset(
1762    ctx: &crate::rpc::RPCState,
1763    tipset_keys: &TipsetKey,
1764) -> Result<Vec<ApiMessage>, ServerError> {
1765    static SHOULD_BACKFILL: LazyLock<bool> = LazyLock::new(|| {
1766        let enabled = is_env_truthy("FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK");
1767        if enabled {
1768            tracing::warn!(
1769                "Full tipset backfilling from network is enabled via FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK, excessive disk and bandwidth usage is expected."
1770            );
1771        }
1772        enabled
1773    });
1774    let full_tipset = if *SHOULD_BACKFILL {
1775        get_full_tipset(
1776            &ctx.sync_network_context,
1777            ctx.chain_store(),
1778            None,
1779            tipset_keys,
1780        )
1781        .await?
1782    } else {
1783        load_full_tipset(ctx.chain_store(), tipset_keys)?
1784    };
1785    let blocks = full_tipset.into_blocks();
1786    let mut messages = vec![];
1787    let mut seen = CidHashSet::default();
1788    for Block {
1789        bls_messages,
1790        secp_messages,
1791        ..
1792    } in blocks
1793    {
1794        for message in bls_messages {
1795            let cid = message.cid();
1796            if seen.insert(cid) {
1797                messages.push(ApiMessage { cid, message });
1798            }
1799        }
1800
1801        for msg in secp_messages {
1802            let cid = msg.cid();
1803            if seen.insert(cid) {
1804                messages.push(ApiMessage {
1805                    cid,
1806                    message: msg.message,
1807                });
1808            }
1809        }
1810    }
1811
1812    Ok(messages)
1813}
1814
1815#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1816pub struct BlockMessages {
1817    #[serde(rename = "BlsMessages", with = "crate::lotus_json")]
1818    #[schemars(with = "LotusJson<Vec<Message>>")]
1819    pub bls_msg: Vec<Message>,
1820    #[serde(rename = "SecpkMessages", with = "crate::lotus_json")]
1821    #[schemars(with = "LotusJson<Vec<SignedMessage>>")]
1822    pub secp_msg: Vec<SignedMessage>,
1823    #[serde(rename = "Cids", with = "crate::lotus_json")]
1824    #[schemars(with = "LotusJson<Vec<Cid>>")]
1825    pub cids: Vec<Cid>,
1826}
1827lotus_json_with_self!(BlockMessages);
1828
1829#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
1830#[serde(rename_all = "PascalCase")]
1831pub struct ApiReceipt {
1832    // Exit status of message execution
1833    pub exit_code: ExitCode,
1834    // `Return` value if the exit code is zero
1835    #[serde(rename = "Return", with = "crate::lotus_json")]
1836    #[schemars(with = "LotusJson<RawBytes>")]
1837    pub return_data: RawBytes,
1838    // Non-negative value of GasUsed
1839    pub gas_used: u64,
1840    #[serde(with = "crate::lotus_json")]
1841    #[schemars(with = "LotusJson<Option<Cid>>")]
1842    pub events_root: Option<Cid>,
1843}
1844
1845lotus_json_with_self!(ApiReceipt);
1846
1847#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
1848#[serde(rename_all = "PascalCase")]
1849pub struct ApiMessage {
1850    #[serde(with = "crate::lotus_json")]
1851    #[schemars(with = "LotusJson<Cid>")]
1852    pub cid: Cid,
1853    #[serde(with = "crate::lotus_json")]
1854    #[schemars(with = "LotusJson<Message>")]
1855    pub message: Message,
1856}
1857
1858lotus_json_with_self!(ApiMessage);
1859
1860#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1861#[serde(rename_all = "camelCase")]
1862pub struct ForestChainExportParams {
1863    pub version: FilecoinSnapshotVersion,
1864    pub epoch: ChainEpoch,
1865    pub recent_roots: i64,
1866    pub output_path: PathBuf,
1867    #[schemars(with = "LotusJson<ApiTipsetKey>")]
1868    #[serde(with = "crate::lotus_json", default)]
1869    pub tipset_keys: ApiTipsetKey,
1870    /// Include message receipts in the output snapshot
1871    #[serde(default)]
1872    pub include_receipts: bool,
1873    /// Include events in the output snapshot
1874    #[serde(default)]
1875    pub include_events: bool,
1876    #[serde(default)]
1877    pub include_tipset_keys: bool,
1878    /// Generate a separate snapshot that contains augmented data (message receipts and events)
1879    #[serde(default)]
1880    pub augmented_snapshot: bool,
1881    /// Generate a separate snapshot that contains tipset lookup
1882    #[serde(default)]
1883    pub tipset_lookup: bool,
1884    pub skip_checksum: bool,
1885    pub dry_run: bool,
1886}
1887lotus_json_with_self!(ForestChainExportParams);
1888
1889#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1890#[serde(rename_all = "camelCase")]
1891pub struct ForestChainExportDiffParams {
1892    pub from: ChainEpoch,
1893    pub to: ChainEpoch,
1894    pub depth: i64,
1895    pub output_path: PathBuf,
1896}
1897lotus_json_with_self!(ForestChainExportDiffParams);
1898
1899#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1900#[serde(rename_all = "camelCase")]
1901pub struct ChainExportParams {
1902    pub epoch: ChainEpoch,
1903    pub recent_roots: i64,
1904    pub output_path: PathBuf,
1905    #[schemars(with = "LotusJson<ApiTipsetKey>")]
1906    #[serde(with = "crate::lotus_json")]
1907    pub tipset_keys: ApiTipsetKey,
1908    pub skip_checksum: bool,
1909    pub dry_run: bool,
1910}
1911lotus_json_with_self!(ChainExportParams);
1912
1913/// The kind of head change delivered by `Filecoin.ChainNotify`.
1914#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
1915#[serde(rename_all = "lowercase")]
1916pub enum HeadChangeType {
1917    Current,
1918    Apply,
1919    Revert,
1920}
1921
1922#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, JsonSchema)]
1923#[serde(rename_all = "PascalCase")]
1924pub struct ApiHeadChange {
1925    #[serde(rename = "Type")]
1926    pub change: HeadChangeType,
1927    #[serde(rename = "Val", with = "crate::lotus_json")]
1928    #[schemars(with = "LotusJson<Tipset>")]
1929    pub tipset: Tipset,
1930}
1931lotus_json_with_self!(ApiHeadChange);
1932
1933impl From<HeadChange> for ApiHeadChange {
1934    fn from(change: HeadChange) -> Self {
1935        match change {
1936            HeadChange::Apply(tipset) => Self {
1937                change: HeadChangeType::Apply,
1938                tipset,
1939            },
1940            HeadChange::Revert(tipset) => Self {
1941                change: HeadChangeType::Revert,
1942                tipset,
1943            },
1944        }
1945    }
1946}
1947
1948#[derive(PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
1949#[serde(tag = "Type", content = "Val", rename_all = "snake_case")]
1950pub enum PathChange<T = Tipset> {
1951    Revert(T),
1952    Apply(T),
1953}
1954
1955impl<T: Clone> Clone for PathChange<T> {
1956    fn clone(&self) -> Self {
1957        match self {
1958            Self::Revert(i) => Self::Revert(i.clone()),
1959            Self::Apply(i) => Self::Apply(i.clone()),
1960        }
1961    }
1962}
1963
1964impl<T> PathChange<T> {
1965    pub fn tipset(&self) -> &T {
1966        match self {
1967            Self::Revert(ts) | Self::Apply(ts) => ts,
1968        }
1969    }
1970}
1971
1972impl HasLotusJson for PathChange {
1973    type LotusJson = PathChange<<Tipset as HasLotusJson>::LotusJson>;
1974
1975    #[cfg(test)]
1976    fn snapshots() -> Vec<(serde_json::Value, Self)> {
1977        use crate::test_utils::dummy_ticket;
1978        use serde_json::json;
1979        let header = CachingBlockHeader::new(RawBlockHeader {
1980            ticket: dummy_ticket(0),
1981            ..Default::default()
1982        });
1983        let header_cid = *header.cid();
1984        vec![(
1985            json!({
1986                "Type": "revert",
1987                "Val": {
1988                    "Blocks": [
1989                        {
1990                            "BeaconEntries": null,
1991                            "ForkSignaling": 0,
1992                            "Height": 0,
1993                            "Messages": { "/": "baeaaaaa" },
1994                            "Miner": "f00",
1995                            "ParentBaseFee": "0",
1996                            "ParentMessageReceipts": { "/": "baeaaaaa" },
1997                            "ParentStateRoot": { "/":"baeaaaaa" },
1998                            "ParentWeight": "0",
1999                            "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
2000                            "Ticket": { "VRFProof": "AA==" },
2001                            "Timestamp": 0,
2002                            "WinPoStProof": null
2003                        }
2004                    ],
2005                    "Cids": [
2006                        { "/": header_cid.to_string() }
2007                    ],
2008                    "Height": 0
2009                }
2010            }),
2011            Self::Revert(Tipset::from(header)),
2012        )]
2013    }
2014
2015    fn into_lotus_json(self) -> Self::LotusJson {
2016        match self {
2017            PathChange::Revert(it) => PathChange::Revert(it.into_lotus_json()),
2018            PathChange::Apply(it) => PathChange::Apply(it.into_lotus_json()),
2019        }
2020    }
2021
2022    fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
2023        match lotus_json {
2024            PathChange::Revert(it) => PathChange::Revert(Tipset::from_lotus_json(it)),
2025            PathChange::Apply(it) => PathChange::Apply(Tipset::from_lotus_json(it)),
2026        }
2027    }
2028}
2029
2030#[derive(Debug)]
2031pub struct PathChanges<T = Tipset> {
2032    pub reverts: Vec<T>,
2033    pub applies: Vec<T>,
2034}
2035
2036impl<T: Clone> Clone for PathChanges<T> {
2037    fn clone(&self) -> Self {
2038        let Self { reverts, applies } = self;
2039        Self {
2040            reverts: reverts.clone(),
2041            applies: applies.clone(),
2042        }
2043    }
2044}
2045
2046impl<T> PathChanges<T> {
2047    pub fn is_empty(&self) -> bool {
2048        self.reverts.is_empty() && self.applies.is_empty()
2049    }
2050
2051    pub fn into_change_vec(self) -> Vec<PathChange<T>> {
2052        let Self { reverts, applies } = self;
2053        reverts
2054            .into_iter()
2055            .map(PathChange::Revert)
2056            .chain(applies.into_iter().map(PathChange::Apply))
2057            .collect_vec()
2058    }
2059}
2060
2061#[cfg(test)]
2062impl<T> quickcheck::Arbitrary for PathChange<T>
2063where
2064    T: quickcheck::Arbitrary + ShallowClone,
2065{
2066    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
2067        let inner = T::arbitrary(g);
2068        g.choose(&[PathChange::Apply(inner.clone()), PathChange::Revert(inner)])
2069            .unwrap()
2070            .clone()
2071    }
2072}
2073
2074#[test]
2075fn snapshots() {
2076    assert_all_snapshots::<PathChange>()
2077}
2078
2079#[cfg(test)]
2080#[quickcheck_macros::quickcheck]
2081fn quickcheck(val: PathChange) {
2082    assert_unchanged_via_json(val)
2083}
2084
2085#[cfg(test)]
2086mod tests {
2087    use super::*;
2088    use crate::daemon::db_util::RangeSpec;
2089    use crate::{
2090        blocks::{Chain4U, RawBlockHeader, chain4u},
2091        db::{
2092            MemoryDB,
2093            car::{AnyCar, ManyCar},
2094        },
2095        networks::{self, ChainConfig},
2096    };
2097    use PathChange::{Apply, Revert};
2098    use rstest::rstest;
2099    use std::sync::Arc;
2100
2101    #[rstest]
2102    #[case(Some(0), None, Some(RangeSpec::To(0)))]
2103    #[case(Some(-1), None, None)]
2104    #[case(None, Some(10), Some(RangeSpec::NumTipsets(10)))]
2105    #[case(None, None, None)]
2106    #[case(Some(10), Some(10), None)]
2107    fn index_backfill_range_spec_validates_params(
2108        #[case] to: Option<ChainEpoch>,
2109        #[case] n_tipsets: Option<u64>,
2110        #[case] expected: Option<RangeSpec>,
2111    ) {
2112        let params = IndexBackfillParams {
2113            to,
2114            n_tipsets,
2115            ..Default::default()
2116        };
2117        assert_eq!(index_backfill_range_spec(&params).ok(), expected);
2118    }
2119
2120    #[test]
2121    fn revert_to_ancestor_linear() {
2122        let cs = ChainStore::calibnet();
2123        let db = Chain4U::with_blockstore(cs.db_owned());
2124        chain4u! {
2125            in db;
2126            [_genesis = cs.genesis_block_header()]
2127            -> [a] -> [b] -> [c, d] -> [e]
2128        };
2129
2130        // simple
2131        assert_path_change(&cs, b, a, [Revert(&[b])]);
2132
2133        // from multi-member tipset
2134        assert_path_change(&cs, [c, d], a, [Revert(&[c, d][..]), Revert(&[b])]);
2135
2136        // to multi-member tipset
2137        assert_path_change(&cs, e, [c, d], [Revert(e)]);
2138
2139        // over multi-member tipset
2140        assert_path_change(&cs, e, b, [Revert(&[e][..]), Revert(&[c, d])]);
2141    }
2142
2143    /// Mirror how lotus handles passing an incomplete `TipsetKey`s.
2144    /// Tested on lotus `1.23.2`
2145    #[test]
2146    fn incomplete_tipsets() {
2147        let cs = ChainStore::calibnet();
2148        let db = Chain4U::with_blockstore(cs.db_owned());
2149        chain4u! {
2150            in db;
2151            [_genesis = cs.genesis_block_header()]
2152            -> [a, b] -> [c] -> [d, _e] // this pattern 2 -> 1 -> 2 can be found at calibnet epoch 1369126
2153        };
2154
2155        // apply to descendant with incomplete `from`
2156        assert_path_change(
2157            &cs,
2158            a,
2159            c,
2160            [
2161                Revert(&[a][..]), // revert the incomplete tipset
2162                Apply(&[a, b]),   // apply the complete one
2163                Apply(&[c]),      // apply the destination
2164            ],
2165        );
2166
2167        // apply to descendant with incomplete `to`
2168        assert_path_change(&cs, c, d, [Apply(d)]);
2169
2170        // revert to ancestor with incomplete `from`
2171        assert_path_change(&cs, d, c, [Revert(d)]);
2172
2173        // revert to ancestor with incomplete `to`
2174        assert_path_change(
2175            &cs,
2176            c,
2177            a,
2178            [
2179                Revert(&[c][..]),
2180                Revert(&[a, b]), // revert the complete tipset
2181                Apply(&[a]),     // apply the incomplete one
2182            ],
2183        );
2184    }
2185
2186    #[test]
2187    fn apply_to_descendant_linear() {
2188        let cs = ChainStore::calibnet();
2189        let db = Chain4U::with_blockstore(cs.db_owned());
2190        chain4u! {
2191            in db;
2192            [_genesis = cs.genesis_block_header()]
2193            -> [a] -> [b] -> [c, d] -> [e]
2194        };
2195
2196        // simple
2197        assert_path_change(&cs, a, b, [Apply(&[b])]);
2198
2199        // from multi-member tipset
2200        assert_path_change(&cs, [c, d], e, [Apply(e)]);
2201
2202        // to multi-member tipset
2203        assert_path_change(&cs, b, [c, d], [Apply([c, d])]);
2204
2205        // over multi-member tipset
2206        assert_path_change(&cs, b, e, [Apply(&[c, d][..]), Apply(&[e])]);
2207    }
2208
2209    #[test]
2210    fn cross_fork_simple() {
2211        let cs = ChainStore::calibnet();
2212        let db = Chain4U::with_blockstore(cs.db_owned());
2213        chain4u! {
2214            in db;
2215            [_genesis = cs.genesis_block_header()]
2216            -> [a] -> [b1] -> [c1]
2217        };
2218        chain4u! {
2219            from [a] in db;
2220            [b2] -> [c2]
2221        };
2222
2223        // same height
2224        assert_path_change(&cs, b1, b2, [Revert(b1), Apply(b2)]);
2225
2226        // different height
2227        assert_path_change(&cs, b1, c2, [Revert(b1), Apply(b2), Apply(c2)]);
2228
2229        let _ = (a, c1);
2230    }
2231
2232    #[test]
2233    fn head_changes_published_deduped_and_ordered() {
2234        let cs = ChainStore::calibnet();
2235        let db = Chain4U::with_blockstore(cs.db_owned());
2236        chain4u! {
2237            in db;
2238            [_genesis = cs.genesis_block_header()]
2239            -> [a] -> [b] -> [c, d] -> [e]
2240        };
2241
2242        let rx = cs.subscribe_head_changes();
2243
2244        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2245        // Re-setting the same head must not publish a change.
2246        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2247        cs.set_heaviest_tipset(b.make_tipset()).unwrap();
2248        cs.set_heaviest_tipset([c, d].make_tipset()).unwrap();
2249        cs.set_heaviest_tipset(e.make_tipset()).unwrap();
2250
2251        let drained = rx.try_iter().collect_vec();
2252        let applied = drained.iter().map(|c| c.applies.clone()).collect_vec();
2253        assert_eq!(
2254            applied,
2255            vec![
2256                vec![a.make_tipset()],
2257                vec![b.make_tipset()],
2258                vec![[c, d].make_tipset()],
2259                vec![e.make_tipset()],
2260            ]
2261        );
2262        assert!(drained.iter().all(|c| c.reverts.is_empty()));
2263    }
2264
2265    #[test]
2266    fn head_changes_publishes_reverts_on_reorg() {
2267        let cs = ChainStore::calibnet();
2268        let db = Chain4U::with_blockstore(cs.db_owned());
2269        chain4u! {
2270            in db;
2271            [_genesis = cs.genesis_block_header()]
2272            -> [a] -> [b1]
2273        };
2274        chain4u! {
2275            from [a] in db;
2276            [b2]
2277        };
2278
2279        let rx = cs.subscribe_head_changes();
2280        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2281        cs.set_heaviest_tipset(b1.make_tipset()).unwrap();
2282        cs.set_heaviest_tipset(b2.make_tipset()).unwrap(); // reorg b1 -> b2
2283
2284        let last = rx.try_iter().last().unwrap();
2285        assert_eq!(last.reverts, vec![b1.make_tipset()]);
2286        assert_eq!(last.applies, vec![b2.make_tipset()]);
2287    }
2288
2289    #[tokio::test]
2290    async fn chain_notify_delivers_every_apply_from_subscription() {
2291        let cs = ChainStore::calibnet();
2292        let db = Chain4U::with_blockstore(cs.db_owned());
2293        chain4u! {
2294            in db;
2295            [_genesis = cs.genesis_block_header()]
2296            -> [a] -> [b] -> [c]
2297        };
2298
2299        let mut rx = chain_notify_inner(&cs);
2300
2301        // First message is the current head.
2302        let first = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
2303            .await
2304            .unwrap()
2305            .unwrap();
2306        assert_eq!(first.len(), 1);
2307        assert_eq!(first[0].change, HeadChangeType::Current);
2308
2309        for ts in [&a, &b, &c] {
2310            cs.set_heaviest_tipset(ts.make_tipset()).unwrap();
2311        }
2312
2313        // Every applied tipset must be delivered, in order, with none dropped.
2314        let mut applied = vec![];
2315        for _ in 0..3 {
2316            match tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv()).await {
2317                Ok(Ok(msg)) => applied.extend(
2318                    msg.into_iter()
2319                        .filter(|c| c.change == HeadChangeType::Apply)
2320                        .map(|c| c.tipset),
2321                ),
2322                _ => break,
2323            }
2324        }
2325        assert_eq!(
2326            applied,
2327            vec![a.make_tipset(), b.make_tipset(), c.make_tipset()]
2328        );
2329    }
2330
2331    impl ChainStore {
2332        fn _load(genesis_car: &'static [u8], genesis_cid: Cid) -> Self {
2333            let db = Arc::new(
2334                ManyCar::new(MemoryDB::default())
2335                    .with_read_only(AnyCar::new(genesis_car).unwrap())
2336                    .unwrap(),
2337            );
2338            let genesis_block_header: CachingBlockHeader =
2339                db.get_cbor(&genesis_cid).unwrap().unwrap();
2340            ChainStore::new(db, Arc::new(ChainConfig::calibnet()), genesis_block_header).unwrap()
2341        }
2342        pub fn calibnet() -> Self {
2343            Self::_load(
2344                networks::calibnet::DEFAULT_GENESIS,
2345                *networks::calibnet::GENESIS_CID,
2346            )
2347        }
2348    }
2349
2350    /// Utility for writing ergonomic tests
2351    trait MakeTipset {
2352        fn make_tipset(self) -> Tipset;
2353    }
2354
2355    impl MakeTipset for &RawBlockHeader {
2356        fn make_tipset(self) -> Tipset {
2357            Tipset::from(CachingBlockHeader::new(self.clone()))
2358        }
2359    }
2360
2361    impl<const N: usize> MakeTipset for [&RawBlockHeader; N] {
2362        fn make_tipset(self) -> Tipset {
2363            self.as_slice().make_tipset()
2364        }
2365    }
2366
2367    impl<const N: usize> MakeTipset for &[&RawBlockHeader; N] {
2368        fn make_tipset(self) -> Tipset {
2369            self.as_slice().make_tipset()
2370        }
2371    }
2372
2373    impl MakeTipset for &[&RawBlockHeader] {
2374        fn make_tipset(self) -> Tipset {
2375            Tipset::new(self.iter().cloned().cloned()).unwrap()
2376        }
2377    }
2378
2379    #[track_caller]
2380    fn assert_path_change<T: MakeTipset>(
2381        store: &ChainStore,
2382        from: impl MakeTipset,
2383        to: impl MakeTipset,
2384        expected: impl IntoIterator<Item = PathChange<T>>,
2385    ) {
2386        fn print(path_change: &PathChange) {
2387            let it = match path_change {
2388                Revert(it) => {
2389                    print!("Revert(");
2390                    it
2391                }
2392                Apply(it) => {
2393                    print!(" Apply(");
2394                    it
2395                }
2396            };
2397            println!(
2398                "epoch = {}, key.cid = {})",
2399                it.epoch(),
2400                it.key().cid().unwrap()
2401            )
2402        }
2403
2404        let actual = chain_get_path(store, from.make_tipset().key(), to.make_tipset().key())
2405            .unwrap()
2406            .into_change_vec();
2407        let expected = expected
2408            .into_iter()
2409            .map(|change| match change {
2410                PathChange::Revert(it) => PathChange::Revert(it.make_tipset()),
2411                PathChange::Apply(it) => PathChange::Apply(it.make_tipset()),
2412            })
2413            .collect_vec();
2414        if expected != actual {
2415            println!("SUMMARY");
2416            println!("=======");
2417            println!("expected:");
2418            for it in &expected {
2419                print(it)
2420            }
2421            println!();
2422            println!("actual:");
2423            for it in &actual {
2424                print(it)
2425            }
2426            println!("=======\n")
2427        }
2428        assert_eq!(
2429            expected, actual,
2430            "expected change (left) does not match actual change (right)"
2431        )
2432    }
2433}