1pub 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
60pub 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
124pub 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
611#[serde(rename_all = "camelCase")]
612pub struct IndexBackfillParams {
613 #[serde(default, skip_serializing_if = "Option::is_none")]
616 pub from: Option<ChainEpoch>,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub to: Option<ChainEpoch>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub n_tipsets: Option<u64>,
623 #[serde(default)]
626 pub recompute: bool,
627 #[serde(default)]
630 pub allow_near_head: bool,
631 #[serde(default)]
634 pub resume: bool,
635}
636lotus_json_with_self!(IndexBackfillParams);
637
638#[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 let spec = index_backfill_range_spec(¶ms)?;
706
707 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 let guard = crate::daemon::db_util::BackfillGuard::try_start()?;
721
722 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 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 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
1014pub 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
1120pub 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 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
1181pub 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 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 (None, None) => Ok(ctx.state_manager.heaviest_tipset()),
1344 (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 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 if let ApiTipsetKey(Some(tsk)) = &selector.key {
1390 let ts = ctx.chain_index().load_required_tipset(tsk)?;
1391 return Ok(ts);
1392 }
1393 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 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 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 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 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 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 = ¤t.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 = ¤t.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 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 pub exit_code: ExitCode,
1834 #[serde(rename = "Return", with = "crate::lotus_json")]
1836 #[schemars(with = "LotusJson<RawBytes>")]
1837 pub return_data: RawBytes,
1838 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 #[serde(default)]
1872 pub include_receipts: bool,
1873 #[serde(default)]
1875 pub include_events: bool,
1876 #[serde(default)]
1877 pub include_tipset_keys: bool,
1878 #[serde(default)]
1880 pub augmented_snapshot: bool,
1881 #[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#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
1915#[serde(rename_all = "lowercase")]
1916pub enum HeadChangeType {
1917 Current,
1918 Apply,
1919 Revert,
1920}
1921
1922#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, JsonSchema)]
1923#[serde(rename_all = "PascalCase")]
1924pub struct ApiHeadChange {
1925 #[serde(rename = "Type")]
1926 pub change: HeadChangeType,
1927 #[serde(rename = "Val", with = "crate::lotus_json")]
1928 #[schemars(with = "LotusJson<Tipset>")]
1929 pub tipset: Tipset,
1930}
1931lotus_json_with_self!(ApiHeadChange);
1932
1933impl From<HeadChange> for ApiHeadChange {
1934 fn from(change: HeadChange) -> Self {
1935 match change {
1936 HeadChange::Apply(tipset) => Self {
1937 change: HeadChangeType::Apply,
1938 tipset,
1939 },
1940 HeadChange::Revert(tipset) => Self {
1941 change: HeadChangeType::Revert,
1942 tipset,
1943 },
1944 }
1945 }
1946}
1947
1948#[derive(PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
1949#[serde(tag = "Type", content = "Val", rename_all = "snake_case")]
1950pub enum PathChange<T = Tipset> {
1951 Revert(T),
1952 Apply(T),
1953}
1954
1955impl<T: Clone> Clone for PathChange<T> {
1956 fn clone(&self) -> Self {
1957 match self {
1958 Self::Revert(i) => Self::Revert(i.clone()),
1959 Self::Apply(i) => Self::Apply(i.clone()),
1960 }
1961 }
1962}
1963
1964impl<T> PathChange<T> {
1965 pub fn tipset(&self) -> &T {
1966 match self {
1967 Self::Revert(ts) | Self::Apply(ts) => ts,
1968 }
1969 }
1970}
1971
1972impl HasLotusJson for PathChange {
1973 type LotusJson = PathChange<<Tipset as HasLotusJson>::LotusJson>;
1974
1975 #[cfg(test)]
1976 fn snapshots() -> Vec<(serde_json::Value, Self)> {
1977 use crate::test_utils::dummy_ticket;
1978 use serde_json::json;
1979 let header = CachingBlockHeader::new(RawBlockHeader {
1980 ticket: dummy_ticket(0),
1981 ..Default::default()
1982 });
1983 let header_cid = *header.cid();
1984 vec![(
1985 json!({
1986 "Type": "revert",
1987 "Val": {
1988 "Blocks": [
1989 {
1990 "BeaconEntries": null,
1991 "ForkSignaling": 0,
1992 "Height": 0,
1993 "Messages": { "/": "baeaaaaa" },
1994 "Miner": "f00",
1995 "ParentBaseFee": "0",
1996 "ParentMessageReceipts": { "/": "baeaaaaa" },
1997 "ParentStateRoot": { "/":"baeaaaaa" },
1998 "ParentWeight": "0",
1999 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
2000 "Ticket": { "VRFProof": "AA==" },
2001 "Timestamp": 0,
2002 "WinPoStProof": null
2003 }
2004 ],
2005 "Cids": [
2006 { "/": header_cid.to_string() }
2007 ],
2008 "Height": 0
2009 }
2010 }),
2011 Self::Revert(Tipset::from(header)),
2012 )]
2013 }
2014
2015 fn into_lotus_json(self) -> Self::LotusJson {
2016 match self {
2017 PathChange::Revert(it) => PathChange::Revert(it.into_lotus_json()),
2018 PathChange::Apply(it) => PathChange::Apply(it.into_lotus_json()),
2019 }
2020 }
2021
2022 fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
2023 match lotus_json {
2024 PathChange::Revert(it) => PathChange::Revert(Tipset::from_lotus_json(it)),
2025 PathChange::Apply(it) => PathChange::Apply(Tipset::from_lotus_json(it)),
2026 }
2027 }
2028}
2029
2030#[derive(Debug)]
2031pub struct PathChanges<T = Tipset> {
2032 pub reverts: Vec<T>,
2033 pub applies: Vec<T>,
2034}
2035
2036impl<T: Clone> Clone for PathChanges<T> {
2037 fn clone(&self) -> Self {
2038 let Self { reverts, applies } = self;
2039 Self {
2040 reverts: reverts.clone(),
2041 applies: applies.clone(),
2042 }
2043 }
2044}
2045
2046impl<T> PathChanges<T> {
2047 pub fn is_empty(&self) -> bool {
2048 self.reverts.is_empty() && self.applies.is_empty()
2049 }
2050
2051 pub fn into_change_vec(self) -> Vec<PathChange<T>> {
2052 let Self { reverts, applies } = self;
2053 reverts
2054 .into_iter()
2055 .map(PathChange::Revert)
2056 .chain(applies.into_iter().map(PathChange::Apply))
2057 .collect_vec()
2058 }
2059}
2060
2061#[cfg(test)]
2062impl<T> quickcheck::Arbitrary for PathChange<T>
2063where
2064 T: quickcheck::Arbitrary + ShallowClone,
2065{
2066 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
2067 let inner = T::arbitrary(g);
2068 g.choose(&[PathChange::Apply(inner.clone()), PathChange::Revert(inner)])
2069 .unwrap()
2070 .clone()
2071 }
2072}
2073
2074#[test]
2075fn snapshots() {
2076 assert_all_snapshots::<PathChange>()
2077}
2078
2079#[cfg(test)]
2080#[quickcheck_macros::quickcheck]
2081fn quickcheck(val: PathChange) {
2082 assert_unchanged_via_json(val)
2083}
2084
2085#[cfg(test)]
2086mod tests {
2087 use super::*;
2088 use crate::daemon::db_util::RangeSpec;
2089 use crate::{
2090 blocks::{Chain4U, RawBlockHeader, chain4u},
2091 db::{
2092 MemoryDB,
2093 car::{AnyCar, ManyCar},
2094 },
2095 networks::{self, ChainConfig},
2096 };
2097 use PathChange::{Apply, Revert};
2098 use rstest::rstest;
2099 use std::sync::Arc;
2100
2101 #[rstest]
2102 #[case(Some(0), None, Some(RangeSpec::To(0)))]
2103 #[case(Some(-1), None, None)]
2104 #[case(None, Some(10), Some(RangeSpec::NumTipsets(10)))]
2105 #[case(None, None, None)]
2106 #[case(Some(10), Some(10), None)]
2107 fn index_backfill_range_spec_validates_params(
2108 #[case] to: Option<ChainEpoch>,
2109 #[case] n_tipsets: Option<u64>,
2110 #[case] expected: Option<RangeSpec>,
2111 ) {
2112 let params = IndexBackfillParams {
2113 to,
2114 n_tipsets,
2115 ..Default::default()
2116 };
2117 assert_eq!(index_backfill_range_spec(¶ms).ok(), expected);
2118 }
2119
2120 #[test]
2121 fn revert_to_ancestor_linear() {
2122 let cs = ChainStore::calibnet();
2123 let db = Chain4U::with_blockstore(cs.db_owned());
2124 chain4u! {
2125 in db;
2126 [_genesis = cs.genesis_block_header()]
2127 -> [a] -> [b] -> [c, d] -> [e]
2128 };
2129
2130 assert_path_change(&cs, b, a, [Revert(&[b])]);
2132
2133 assert_path_change(&cs, [c, d], a, [Revert(&[c, d][..]), Revert(&[b])]);
2135
2136 assert_path_change(&cs, e, [c, d], [Revert(e)]);
2138
2139 assert_path_change(&cs, e, b, [Revert(&[e][..]), Revert(&[c, d])]);
2141 }
2142
2143 #[test]
2146 fn incomplete_tipsets() {
2147 let cs = ChainStore::calibnet();
2148 let db = Chain4U::with_blockstore(cs.db_owned());
2149 chain4u! {
2150 in db;
2151 [_genesis = cs.genesis_block_header()]
2152 -> [a, b] -> [c] -> [d, _e] };
2154
2155 assert_path_change(
2157 &cs,
2158 a,
2159 c,
2160 [
2161 Revert(&[a][..]), Apply(&[a, b]), Apply(&[c]), ],
2165 );
2166
2167 assert_path_change(&cs, c, d, [Apply(d)]);
2169
2170 assert_path_change(&cs, d, c, [Revert(d)]);
2172
2173 assert_path_change(
2175 &cs,
2176 c,
2177 a,
2178 [
2179 Revert(&[c][..]),
2180 Revert(&[a, b]), Apply(&[a]), ],
2183 );
2184 }
2185
2186 #[test]
2187 fn apply_to_descendant_linear() {
2188 let cs = ChainStore::calibnet();
2189 let db = Chain4U::with_blockstore(cs.db_owned());
2190 chain4u! {
2191 in db;
2192 [_genesis = cs.genesis_block_header()]
2193 -> [a] -> [b] -> [c, d] -> [e]
2194 };
2195
2196 assert_path_change(&cs, a, b, [Apply(&[b])]);
2198
2199 assert_path_change(&cs, [c, d], e, [Apply(e)]);
2201
2202 assert_path_change(&cs, b, [c, d], [Apply([c, d])]);
2204
2205 assert_path_change(&cs, b, e, [Apply(&[c, d][..]), Apply(&[e])]);
2207 }
2208
2209 #[test]
2210 fn cross_fork_simple() {
2211 let cs = ChainStore::calibnet();
2212 let db = Chain4U::with_blockstore(cs.db_owned());
2213 chain4u! {
2214 in db;
2215 [_genesis = cs.genesis_block_header()]
2216 -> [a] -> [b1] -> [c1]
2217 };
2218 chain4u! {
2219 from [a] in db;
2220 [b2] -> [c2]
2221 };
2222
2223 assert_path_change(&cs, b1, b2, [Revert(b1), Apply(b2)]);
2225
2226 assert_path_change(&cs, b1, c2, [Revert(b1), Apply(b2), Apply(c2)]);
2228
2229 let _ = (a, c1);
2230 }
2231
2232 #[test]
2233 fn head_changes_published_deduped_and_ordered() {
2234 let cs = ChainStore::calibnet();
2235 let db = Chain4U::with_blockstore(cs.db_owned());
2236 chain4u! {
2237 in db;
2238 [_genesis = cs.genesis_block_header()]
2239 -> [a] -> [b] -> [c, d] -> [e]
2240 };
2241
2242 let rx = cs.subscribe_head_changes();
2243
2244 cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2245 cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2247 cs.set_heaviest_tipset(b.make_tipset()).unwrap();
2248 cs.set_heaviest_tipset([c, d].make_tipset()).unwrap();
2249 cs.set_heaviest_tipset(e.make_tipset()).unwrap();
2250
2251 let drained = rx.try_iter().collect_vec();
2252 let applied = drained.iter().map(|c| c.applies.clone()).collect_vec();
2253 assert_eq!(
2254 applied,
2255 vec![
2256 vec![a.make_tipset()],
2257 vec![b.make_tipset()],
2258 vec![[c, d].make_tipset()],
2259 vec![e.make_tipset()],
2260 ]
2261 );
2262 assert!(drained.iter().all(|c| c.reverts.is_empty()));
2263 }
2264
2265 #[test]
2266 fn head_changes_publishes_reverts_on_reorg() {
2267 let cs = ChainStore::calibnet();
2268 let db = Chain4U::with_blockstore(cs.db_owned());
2269 chain4u! {
2270 in db;
2271 [_genesis = cs.genesis_block_header()]
2272 -> [a] -> [b1]
2273 };
2274 chain4u! {
2275 from [a] in db;
2276 [b2]
2277 };
2278
2279 let rx = cs.subscribe_head_changes();
2280 cs.set_heaviest_tipset(a.make_tipset()).unwrap();
2281 cs.set_heaviest_tipset(b1.make_tipset()).unwrap();
2282 cs.set_heaviest_tipset(b2.make_tipset()).unwrap(); let last = rx.try_iter().last().unwrap();
2285 assert_eq!(last.reverts, vec![b1.make_tipset()]);
2286 assert_eq!(last.applies, vec![b2.make_tipset()]);
2287 }
2288
2289 #[tokio::test]
2290 async fn chain_notify_delivers_every_apply_from_subscription() {
2291 let cs = ChainStore::calibnet();
2292 let db = Chain4U::with_blockstore(cs.db_owned());
2293 chain4u! {
2294 in db;
2295 [_genesis = cs.genesis_block_header()]
2296 -> [a] -> [b] -> [c]
2297 };
2298
2299 let mut rx = chain_notify_inner(&cs);
2300
2301 let first = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
2303 .await
2304 .unwrap()
2305 .unwrap();
2306 assert_eq!(first.len(), 1);
2307 assert_eq!(first[0].change, HeadChangeType::Current);
2308
2309 for ts in [&a, &b, &c] {
2310 cs.set_heaviest_tipset(ts.make_tipset()).unwrap();
2311 }
2312
2313 let mut applied = vec![];
2315 for _ in 0..3 {
2316 match tokio::time::timeout(std::time::Duration::from_millis(500), rx.recv()).await {
2317 Ok(Ok(msg)) => applied.extend(
2318 msg.into_iter()
2319 .filter(|c| c.change == HeadChangeType::Apply)
2320 .map(|c| c.tipset),
2321 ),
2322 _ => break,
2323 }
2324 }
2325 assert_eq!(
2326 applied,
2327 vec![a.make_tipset(), b.make_tipset(), c.make_tipset()]
2328 );
2329 }
2330
2331 impl ChainStore {
2332 fn _load(genesis_car: &'static [u8], genesis_cid: Cid) -> Self {
2333 let db = Arc::new(
2334 ManyCar::new(MemoryDB::default())
2335 .with_read_only(AnyCar::new(genesis_car).unwrap())
2336 .unwrap(),
2337 );
2338 let genesis_block_header: CachingBlockHeader =
2339 db.get_cbor(&genesis_cid).unwrap().unwrap();
2340 ChainStore::new(db, Arc::new(ChainConfig::calibnet()), genesis_block_header).unwrap()
2341 }
2342 pub fn calibnet() -> Self {
2343 Self::_load(
2344 networks::calibnet::DEFAULT_GENESIS,
2345 *networks::calibnet::GENESIS_CID,
2346 )
2347 }
2348 }
2349
2350 trait MakeTipset {
2352 fn make_tipset(self) -> Tipset;
2353 }
2354
2355 impl MakeTipset for &RawBlockHeader {
2356 fn make_tipset(self) -> Tipset {
2357 Tipset::from(CachingBlockHeader::new(self.clone()))
2358 }
2359 }
2360
2361 impl<const N: usize> MakeTipset for [&RawBlockHeader; N] {
2362 fn make_tipset(self) -> Tipset {
2363 self.as_slice().make_tipset()
2364 }
2365 }
2366
2367 impl<const N: usize> MakeTipset for &[&RawBlockHeader; N] {
2368 fn make_tipset(self) -> Tipset {
2369 self.as_slice().make_tipset()
2370 }
2371 }
2372
2373 impl MakeTipset for &[&RawBlockHeader] {
2374 fn make_tipset(self) -> Tipset {
2375 Tipset::new(self.iter().cloned().cloned()).unwrap()
2376 }
2377 }
2378
2379 #[track_caller]
2380 fn assert_path_change<T: MakeTipset>(
2381 store: &ChainStore,
2382 from: impl MakeTipset,
2383 to: impl MakeTipset,
2384 expected: impl IntoIterator<Item = PathChange<T>>,
2385 ) {
2386 fn print(path_change: &PathChange) {
2387 let it = match path_change {
2388 Revert(it) => {
2389 print!("Revert(");
2390 it
2391 }
2392 Apply(it) => {
2393 print!(" Apply(");
2394 it
2395 }
2396 };
2397 println!(
2398 "epoch = {}, key.cid = {})",
2399 it.epoch(),
2400 it.key().cid().unwrap()
2401 )
2402 }
2403
2404 let actual = chain_get_path(store, from.make_tipset().key(), to.make_tipset().key())
2405 .unwrap()
2406 .into_change_vec();
2407 let expected = expected
2408 .into_iter()
2409 .map(|change| match change {
2410 PathChange::Revert(it) => PathChange::Revert(it.make_tipset()),
2411 PathChange::Apply(it) => PathChange::Apply(it.make_tipset()),
2412 })
2413 .collect_vec();
2414 if expected != actual {
2415 println!("SUMMARY");
2416 println!("=======");
2417 println!("expected:");
2418 for it in &expected {
2419 print(it)
2420 }
2421 println!();
2422 println!("actual:");
2423 for it in &actual {
2424 print(it)
2425 }
2426 println!("=======\n")
2427 }
2428 assert_eq!(
2429 expected, actual,
2430 "expected change (left) does not match actual change (right)"
2431 )
2432 }
2433}