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, HeaderBuilder, 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    use std::time::Duration;
2101
2102    #[rstest]
2103    #[case(Some(0), None, Some(RangeSpec::To(0)))]
2104    #[case(Some(-1), None, None)]
2105    #[case(None, Some(10), Some(RangeSpec::NumTipsets(10)))]
2106    #[case(None, None, None)]
2107    #[case(Some(10), Some(10), None)]
2108    fn index_backfill_range_spec_validates_params(
2109        #[case] to: Option<ChainEpoch>,
2110        #[case] n_tipsets: Option<u64>,
2111        #[case] expected: Option<RangeSpec>,
2112    ) {
2113        let params = IndexBackfillParams {
2114            to,
2115            n_tipsets,
2116            ..Default::default()
2117        };
2118        assert_eq!(index_backfill_range_spec(&params).ok(), expected);
2119    }
2120
2121    #[test]
2122    fn revert_to_ancestor_linear() {
2123        let cs = ChainStore::calibnet();
2124        let db = Chain4U::with_blockstore(cs.db_owned());
2125        chain4u! {
2126            in db;
2127            [_genesis = cs.genesis_block_header()]
2128            -> [a] -> [b] -> [c, d] -> [e]
2129        };
2130
2131        // simple
2132        assert_path_change(&cs, b, a, [Revert(&[b])]);
2133
2134        // from multi-member tipset
2135        assert_path_change(&cs, [c, d], a, [Revert(&[c, d][..]), Revert(&[b])]);
2136
2137        // to multi-member tipset
2138        assert_path_change(&cs, e, [c, d], [Revert(e)]);
2139
2140        // over multi-member tipset
2141        assert_path_change(&cs, e, b, [Revert(&[e][..]), Revert(&[c, d])]);
2142    }
2143
2144    /// Mirror how lotus handles passing an incomplete `TipsetKey`s.
2145    /// Tested on lotus `1.23.2`
2146    #[test]
2147    fn incomplete_tipsets() {
2148        let cs = ChainStore::calibnet();
2149        let db = Chain4U::with_blockstore(cs.db_owned());
2150        chain4u! {
2151            in db;
2152            [_genesis = cs.genesis_block_header()]
2153            -> [a, b] -> [c] -> [d, _e] // this pattern 2 -> 1 -> 2 can be found at calibnet epoch 1369126
2154        };
2155
2156        // apply to descendant with incomplete `from`
2157        assert_path_change(
2158            &cs,
2159            a,
2160            c,
2161            [
2162                Revert(&[a][..]), // revert the incomplete tipset
2163                Apply(&[a, b]),   // apply the complete one
2164                Apply(&[c]),      // apply the destination
2165            ],
2166        );
2167
2168        // apply to descendant with incomplete `to`
2169        assert_path_change(&cs, c, d, [Apply(d)]);
2170
2171        // revert to ancestor with incomplete `from`
2172        assert_path_change(&cs, d, c, [Revert(d)]);
2173
2174        // revert to ancestor with incomplete `to`
2175        assert_path_change(
2176            &cs,
2177            c,
2178            a,
2179            [
2180                Revert(&[c][..]),
2181                Revert(&[a, b]), // revert the complete tipset
2182                Apply(&[a]),     // apply the incomplete one
2183            ],
2184        );
2185    }
2186
2187    #[test]
2188    fn apply_to_descendant_linear() {
2189        let cs = ChainStore::calibnet();
2190        let db = Chain4U::with_blockstore(cs.db_owned());
2191        chain4u! {
2192            in db;
2193            [_genesis = cs.genesis_block_header()]
2194            -> [a] -> [b] -> [c, d] -> [e]
2195        };
2196
2197        // simple
2198        assert_path_change(&cs, a, b, [Apply(&[b])]);
2199
2200        // from multi-member tipset
2201        assert_path_change(&cs, [c, d], e, [Apply(e)]);
2202
2203        // to multi-member tipset
2204        assert_path_change(&cs, b, [c, d], [Apply([c, d])]);
2205
2206        // over multi-member tipset
2207        assert_path_change(&cs, b, e, [Apply(&[c, d][..]), Apply(&[e])]);
2208    }
2209
2210    #[test]
2211    fn cross_fork_simple() {
2212        let cs = ChainStore::calibnet();
2213        let db = Chain4U::with_blockstore(cs.db_owned());
2214        chain4u! {
2215            in db;
2216            [_genesis = cs.genesis_block_header()]
2217            -> [a] -> [b1] -> [c1]
2218        };
2219        chain4u! {
2220            from [a] in db;
2221            [b2] -> [c2]
2222        };
2223
2224        // same height
2225        assert_path_change(&cs, b1, b2, [Revert(b1), Apply(b2)]);
2226
2227        // different height
2228        assert_path_change(&cs, b1, c2, [Revert(b1), Apply(b2), Apply(c2)]);
2229
2230        let _ = (a, c1);
2231    }
2232
2233    #[test]
2234    fn head_changes_published_deduped_and_ordered() {
2235        let cs = ChainStore::calibnet();
2236        let db = Chain4U::with_blockstore(cs.db_owned());
2237        chain4u! {
2238            in db;
2239            [_genesis = cs.genesis_block_header()]
2240            -> [a] -> [b] -> [c, d] -> [e]
2241        };
2242
2243        let rx = cs.subscribe_head_changes();
2244
2245        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2246        // Re-setting the same head must not publish a change.
2247        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2248        cs.set_heaviest_tipset(b.make_tipset()).unwrap();
2249        cs.set_heaviest_tipset([c, d].make_tipset()).unwrap();
2250        cs.set_heaviest_tipset(e.make_tipset()).unwrap();
2251
2252        let drained = rx.try_iter().collect_vec();
2253        let applied = drained.iter().map(|c| c.applies.clone()).collect_vec();
2254        assert_eq!(
2255            applied,
2256            vec![
2257                vec![a.make_tipset()],
2258                vec![b.make_tipset()],
2259                vec![[c, d].make_tipset()],
2260                vec![e.make_tipset()],
2261            ]
2262        );
2263        assert!(drained.iter().all(|c| c.reverts.is_empty()));
2264    }
2265
2266    #[test]
2267    fn head_changes_publishes_reverts_on_reorg() {
2268        let cs = ChainStore::calibnet();
2269        let db = Chain4U::with_blockstore(cs.db_owned());
2270        chain4u! {
2271            in db;
2272            [_genesis = cs.genesis_block_header()]
2273            -> [a] -> [b1]
2274        };
2275        chain4u! {
2276            from [a] in db;
2277            [b2]
2278        };
2279
2280        let rx = cs.subscribe_head_changes();
2281        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2282        cs.set_heaviest_tipset(b1.make_tipset()).unwrap();
2283        cs.set_heaviest_tipset(b2.make_tipset()).unwrap(); // reorg b1 -> b2
2284
2285        let last = rx.try_iter().last().unwrap();
2286        assert_eq!(last.reverts, vec![b1.make_tipset()]);
2287        assert_eq!(last.applies, vec![b2.make_tipset()]);
2288    }
2289
2290    #[tokio::test]
2291    async fn chain_notify_delivers_every_apply_from_subscription() {
2292        let cs = ChainStore::calibnet();
2293        let db = Chain4U::with_blockstore(cs.db_owned());
2294        chain4u! {
2295            in db;
2296            [_genesis = cs.genesis_block_header()]
2297            -> [a] -> [b] -> [c]
2298        };
2299
2300        let mut rx = chain_notify_inner(&cs);
2301
2302        // First message is the current head.
2303        let first = next_batch(&mut rx).await;
2304        assert_eq!(first.len(), 1);
2305        assert_eq!(first[0].change, HeadChangeType::Current);
2306
2307        for ts in [&a, &b, &c] {
2308            cs.set_heaviest_tipset(ts.make_tipset()).unwrap();
2309        }
2310
2311        // Every applied tipset must be delivered, in order, with none dropped.
2312        let mut applied = vec![];
2313        for _ in 0..3 {
2314            applied.extend(
2315                next_batch(&mut rx)
2316                    .await
2317                    .into_iter()
2318                    .filter(|c| c.change == HeadChangeType::Apply)
2319                    .map(|c| c.tipset),
2320            );
2321        }
2322        assert_eq!(
2323            applied,
2324            vec![a.make_tipset(), b.make_tipset(), c.make_tipset()]
2325        );
2326    }
2327
2328    impl ChainStore {
2329        fn _load(genesis_car: &'static [u8], genesis_cid: Cid) -> Self {
2330            let db = Arc::new(
2331                ManyCar::new(MemoryDB::default())
2332                    .with_read_only(AnyCar::new(genesis_car).unwrap())
2333                    .unwrap(),
2334            );
2335            let genesis_block_header: CachingBlockHeader =
2336                db.get_cbor(&genesis_cid).unwrap().unwrap();
2337            ChainStore::new(db, Arc::new(ChainConfig::calibnet()), genesis_block_header).unwrap()
2338        }
2339        pub fn calibnet() -> Self {
2340            Self::_load(
2341                networks::calibnet::DEFAULT_GENESIS,
2342                *networks::calibnet::GENESIS_CID,
2343            )
2344        }
2345    }
2346
2347    /// Utility for writing ergonomic tests
2348    trait MakeTipset {
2349        fn make_tipset(self) -> Tipset;
2350    }
2351
2352    impl MakeTipset for &RawBlockHeader {
2353        fn make_tipset(self) -> Tipset {
2354            Tipset::from(CachingBlockHeader::new(self.clone()))
2355        }
2356    }
2357
2358    impl<const N: usize> MakeTipset for [&RawBlockHeader; N] {
2359        fn make_tipset(self) -> Tipset {
2360            self.as_slice().make_tipset()
2361        }
2362    }
2363
2364    impl<const N: usize> MakeTipset for &[&RawBlockHeader; N] {
2365        fn make_tipset(self) -> Tipset {
2366            self.as_slice().make_tipset()
2367        }
2368    }
2369
2370    impl MakeTipset for &[&RawBlockHeader] {
2371        fn make_tipset(self) -> Tipset {
2372            Tipset::new(self.iter().cloned().cloned()).unwrap()
2373        }
2374    }
2375
2376    #[track_caller]
2377    fn assert_path_change<T: MakeTipset>(
2378        store: &ChainStore,
2379        from: impl MakeTipset,
2380        to: impl MakeTipset,
2381        expected: impl IntoIterator<Item = PathChange<T>>,
2382    ) {
2383        fn print(path_change: &PathChange) {
2384            let it = match path_change {
2385                Revert(it) => {
2386                    print!("Revert(");
2387                    it
2388                }
2389                Apply(it) => {
2390                    print!(" Apply(");
2391                    it
2392                }
2393            };
2394            println!(
2395                "epoch = {}, key.cid = {})",
2396                it.epoch(),
2397                it.key().cid().unwrap()
2398            )
2399        }
2400
2401        let actual = chain_get_path(store, from.make_tipset().key(), to.make_tipset().key())
2402            .unwrap()
2403            .into_change_vec();
2404        let expected = expected
2405            .into_iter()
2406            .map(|change| match change {
2407                PathChange::Revert(it) => PathChange::Revert(it.make_tipset()),
2408                PathChange::Apply(it) => PathChange::Apply(it.make_tipset()),
2409            })
2410            .collect_vec();
2411        if expected != actual {
2412            println!("SUMMARY");
2413            println!("=======");
2414            println!("expected:");
2415            for it in &expected {
2416                print(it)
2417            }
2418            println!();
2419            println!("actual:");
2420            for it in &actual {
2421                print(it)
2422            }
2423            println!("=======\n")
2424        }
2425        assert_eq!(
2426            expected, actual,
2427            "expected change (left) does not match actual change (right)"
2428        )
2429    }
2430
2431    const BATCH_TIMEOUT: Duration = Duration::from_secs(1);
2432
2433    async fn next_batch(rx: &mut Subscriber<Vec<ApiHeadChange>>) -> Vec<ApiHeadChange> {
2434        tokio::time::timeout(BATCH_TIMEOUT, rx.recv())
2435            .await
2436            .expect("timed out waiting for a head-change batch")
2437            .expect("chain notify channel closed")
2438    }
2439
2440    /// Open a ChainNotify subscription and consume the immediate `current`
2441    /// event, asserting it matches the store head.
2442    async fn open_notify(cs: &ChainStore) -> Subscriber<Vec<ApiHeadChange>> {
2443        let mut rx = chain_notify_inner(cs);
2444        assert_eq!(
2445            next_batch(&mut rx).await,
2446            vec![ApiHeadChange {
2447                change: HeadChangeType::Current,
2448                tipset: cs.heaviest_tipset(),
2449            }]
2450        );
2451        rx
2452    }
2453
2454    fn applied(ts: impl MakeTipset) -> ApiHeadChange {
2455        ApiHeadChange {
2456            change: HeadChangeType::Apply,
2457            tipset: ts.make_tipset(),
2458        }
2459    }
2460
2461    fn reverted(ts: impl MakeTipset) -> ApiHeadChange {
2462        ApiHeadChange {
2463            change: HeadChangeType::Revert,
2464            tipset: ts.make_tipset(),
2465        }
2466    }
2467
2468    #[tokio::test]
2469    async fn current_first_matches_store_head() {
2470        let cs = ChainStore::calibnet();
2471
2472        let mut rx = chain_notify_inner(&cs);
2473
2474        assert_eq!(
2475            next_batch(&mut rx).await,
2476            vec![ApiHeadChange {
2477                change: HeadChangeType::Current,
2478                tipset: cs.heaviest_tipset(),
2479            }]
2480        );
2481    }
2482
2483    #[tokio::test]
2484    async fn linear_applies_chain() {
2485        let cs = ChainStore::calibnet();
2486        let db = Chain4U::with_blockstore(cs.db_owned());
2487        chain4u! {
2488            in db;
2489            [_genesis = cs.genesis_block_header()]
2490            -> [a] -> [b] -> [c] -> [d]
2491        };
2492
2493        let mut rx = open_notify(&cs).await;
2494
2495        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2496        cs.set_heaviest_tipset(b.make_tipset()).unwrap();
2497        cs.set_heaviest_tipset(c.make_tipset()).unwrap();
2498        cs.set_heaviest_tipset(d.make_tipset()).unwrap();
2499
2500        // one single-apply batch per head move, in chain order; equality with
2501        // the constructed tipsets pins increasing heights and parent linkage
2502        assert_eq!(next_batch(&mut rx).await, vec![applied(a)]);
2503        assert_eq!(next_batch(&mut rx).await, vec![applied(b)]);
2504        assert_eq!(next_batch(&mut rx).await, vec![applied(c)]);
2505        assert_eq!(next_batch(&mut rx).await, vec![applied(d)]);
2506    }
2507
2508    #[tokio::test]
2509    async fn fork_switch_one_batch_reverts_then_applies() {
2510        let cs = ChainStore::calibnet();
2511        let db = Chain4U::with_blockstore(cs.db_owned());
2512        chain4u! {
2513            in db;
2514            [_genesis = cs.genesis_block_header()]
2515            -> [a] -> [b] -> [c]
2516        };
2517        chain4u! {
2518            from [a] in db;
2519            [b2] -> [c2] -> [d2]
2520        };
2521
2522        let mut rx = open_notify(&cs).await;
2523
2524        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2525        assert_eq!(next_batch(&mut rx).await, vec![applied(a)]);
2526
2527        // advancing over several epochs delivers one multi-apply batch
2528        cs.set_heaviest_tipset(c.make_tipset()).unwrap();
2529        assert_eq!(next_batch(&mut rx).await, vec![applied(b), applied(c)]);
2530
2531        // switching to the longer fork delivers a single batch: reverts
2532        // newest-first down to the common ancestor, then applies oldest-first
2533        cs.set_heaviest_tipset(d2.make_tipset()).unwrap();
2534        assert_eq!(
2535            next_batch(&mut rx).await,
2536            vec![
2537                reverted(c),
2538                reverted(b),
2539                applied(b2),
2540                applied(c2),
2541                applied(d2)
2542            ]
2543        );
2544    }
2545
2546    #[tokio::test]
2547    async fn rewind_pure_revert_batch() {
2548        let cs = ChainStore::calibnet();
2549        let db = Chain4U::with_blockstore(cs.db_owned());
2550        chain4u! {
2551            in db;
2552            [_genesis = cs.genesis_block_header()]
2553            -> [a] -> [b] -> [c] -> [d]
2554        };
2555
2556        let mut rx = open_notify(&cs).await;
2557
2558        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2559        assert_eq!(next_batch(&mut rx).await, vec![applied(a)]);
2560
2561        cs.set_heaviest_tipset(d.make_tipset()).unwrap();
2562        assert_eq!(
2563            next_batch(&mut rx).await,
2564            vec![applied(b), applied(c), applied(d)]
2565        );
2566
2567        // rewinding head to an ancestor (what the admin `ChainSetHead` does)
2568        // delivers one pure-revert batch, newest-first, with no applies
2569        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2570        assert_eq!(
2571            next_batch(&mut rx).await,
2572            vec![reverted(d), reverted(c), reverted(b)]
2573        );
2574    }
2575
2576    #[tokio::test]
2577    async fn null_round_height_gaps() {
2578        let cs = ChainStore::calibnet();
2579        let db = Chain4U::with_blockstore(cs.db_owned());
2580        chain4u! {
2581            in db;
2582            [_genesis = cs.genesis_block_header()]
2583            -> [a] -> [b]
2584            -> [c = HeaderBuilder::new().with_epoch(5)] // epochs 3 and 4 are null rounds
2585            -> [d]
2586        };
2587        assert_eq!(c.epoch, 5);
2588        assert_eq!(c.parents, b.make_tipset().key().clone());
2589
2590        let mut rx = open_notify(&cs).await;
2591
2592        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2593        cs.set_heaviest_tipset(b.make_tipset()).unwrap();
2594        cs.set_heaviest_tipset(c.make_tipset()).unwrap();
2595        cs.set_heaviest_tipset(d.make_tipset()).unwrap();
2596
2597        // consecutive applies jump from epoch 2 to epoch 5 over the null
2598        // rounds; no filler events are emitted for the missing epochs
2599        assert_eq!(next_batch(&mut rx).await, vec![applied(a)]);
2600        assert_eq!(next_batch(&mut rx).await, vec![applied(b)]);
2601        assert_eq!(next_batch(&mut rx).await, vec![applied(c)]);
2602        assert_eq!(next_batch(&mut rx).await, vec![applied(d)]);
2603    }
2604
2605    #[tokio::test]
2606    async fn over_finality_fallback_single_apply() {
2607        let cs = ChainStore::calibnet();
2608        let finality = cs.chain_config().policy.chain_finality;
2609        let db = Chain4U::with_blockstore(cs.db_owned());
2610        chain4u! {
2611            in db;
2612            [_genesis = cs.genesis_block_header()]
2613            -> [a] -> [b] -> [c]
2614        };
2615        chain4u! {
2616            from [a] in db;
2617            [far = HeaderBuilder::new().with_epoch(finality + 4)]
2618        };
2619
2620        let mut rx = open_notify(&cs).await;
2621
2622        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2623        assert_eq!(next_batch(&mut rx).await, vec![applied(a)]);
2624
2625        cs.set_heaviest_tipset(c.make_tipset()).unwrap();
2626        assert_eq!(next_batch(&mut rx).await, vec![applied(b), applied(c)]);
2627
2628        // `far` forks off `a`, so a real path from `c` would start with
2629        // reverts of `c` and `b`. But the head jump is wider than chain
2630        // finality, so the path computation bails out and the batch degrades
2631        // to a single apply with no reverts.
2632        cs.set_heaviest_tipset(far.make_tipset()).unwrap();
2633        assert_eq!(next_batch(&mut rx).await, vec![applied(far)]);
2634    }
2635
2636    /// End-to-end weld: a real jsonrpsee server serving just the pubsub
2637    /// module (no auth stack), a raw WebSocket client, and a real
2638    /// `ChainStore`. A fork switch must arrive as one correctly framed
2639    /// `xrpc.ch.val` notification carrying the revert+apply batch.
2640    #[tokio::test]
2641    async fn ws_weld_end_to_end() {
2642        use crate::rpc::channel::{NOTIF_METHOD_NAME, RpcModule as FilRpcModule};
2643        use futures::{SinkExt, StreamExt};
2644        use serde_json::Value;
2645        use tokio_tungstenite::tungstenite::Message;
2646
2647        /// Assert `xrpc.ch.val` framing (positional `[channelId, payload]`
2648        /// params) and decode the payload.
2649        fn decode_val_frame(frame: Value) -> (u64, Vec<ApiHeadChange>) {
2650            assert_eq!(
2651                frame.get("method").and_then(Value::as_str),
2652                Some(NOTIF_METHOD_NAME),
2653                "not an xrpc.ch.val frame: {frame}"
2654            );
2655            let params = frame
2656                .get("params")
2657                .and_then(Value::as_array)
2658                .unwrap_or_else(|| panic!("params must be a positional array: {frame}"));
2659            let [channel_id, payload] = params.as_slice() else {
2660                panic!("params must be [channelId, payload]: {frame}");
2661            };
2662            let channel_id = channel_id.as_u64().expect("channel id must be a u64");
2663            let batch = serde_json::from_value(payload.clone())
2664                .expect("payload must decode as Vec<ApiHeadChange>");
2665            (channel_id, batch)
2666        }
2667
2668        let cs = ChainStore::calibnet();
2669        let db = Chain4U::with_blockstore(cs.db_owned());
2670        chain4u! {
2671            in db;
2672            [_genesis = cs.genesis_block_header()]
2673            -> [a] -> [b] -> [c]
2674        };
2675        chain4u! {
2676            from [a] in db;
2677            [b2] -> [c2] -> [d2]
2678        };
2679
2680        // serve only the pubsub module (no auth stack)
2681        let mut pubsub = FilRpcModule::default();
2682        pubsub
2683            .register_channel("Filecoin.ChainNotify", {
2684                let chain_store = cs.shallow_clone();
2685                move |_params| chain_notify_inner(&chain_store)
2686            })
2687            .unwrap();
2688        let server = jsonrpsee::server::Server::builder()
2689            .build("127.0.0.1:0")
2690            .await
2691            .unwrap();
2692        let addr = server.local_addr().unwrap();
2693        let _server_handle = server.start(pubsub);
2694
2695        let (mut ws, _) = tokio_tungstenite::connect_async(format!("ws://{addr}"))
2696            .await
2697            .unwrap();
2698
2699        async fn next_ws_json(
2700            ws: &mut (
2701                     impl StreamExt<Item = Result<Message, tokio_tungstenite::tungstenite::Error>>
2702                     + Unpin
2703                 ),
2704        ) -> Value {
2705            loop {
2706                let message = tokio::time::timeout(BATCH_TIMEOUT, ws.next())
2707                    .await
2708                    .expect("timed out waiting for a websocket frame")
2709                    .expect("websocket closed")
2710                    .unwrap();
2711                if message.is_text() {
2712                    return serde_json::from_str(message.into_text().unwrap().as_str()).unwrap();
2713                }
2714            }
2715        }
2716
2717        // subscribe: the response carries a bare u64 channel id
2718        ws.send(Message::text(
2719            r#"{"jsonrpc":"2.0","id":1,"method":"Filecoin.ChainNotify","params":[]}"#,
2720        ))
2721        .await
2722        .unwrap();
2723        let response = next_ws_json(&mut ws).await;
2724        let channel_id = response
2725            .get("result")
2726            .and_then(Value::as_u64)
2727            .unwrap_or_else(|| panic!("channel id must be a bare u64: {response}"));
2728
2729        // the first frame is the `current` event for the store head
2730        let (frame_channel, batch) = decode_val_frame(next_ws_json(&mut ws).await);
2731        assert_eq!(frame_channel, channel_id);
2732        assert_eq!(
2733            batch,
2734            vec![ApiHeadChange {
2735                change: HeadChangeType::Current,
2736                tipset: cs.heaviest_tipset(),
2737            }]
2738        );
2739
2740        cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2741        let (frame_channel, batch) = decode_val_frame(next_ws_json(&mut ws).await);
2742        assert_eq!(frame_channel, channel_id);
2743        assert_eq!(batch, vec![applied(a)]);
2744
2745        cs.set_heaviest_tipset(c.make_tipset()).unwrap();
2746        let (frame_channel, batch) = decode_val_frame(next_ws_json(&mut ws).await);
2747        assert_eq!(frame_channel, channel_id);
2748        assert_eq!(batch, vec![applied(b), applied(c)]);
2749
2750        // fork switch: one frame carrying reverts-then-applies
2751        cs.set_heaviest_tipset(d2.make_tipset()).unwrap();
2752        let (frame_channel, batch) = decode_val_frame(next_ws_json(&mut ws).await);
2753        assert_eq!(frame_channel, channel_id);
2754        assert_eq!(
2755            batch,
2756            vec![
2757                reverted(c),
2758                reverted(b),
2759                applied(b2),
2760                applied(c2),
2761                applied(d2)
2762            ]
2763        );
2764    }
2765}