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 let head = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?;
348 let start_ts = ctx
349 .chain_index()
350 .load_required_tipset_by_height(epoch, head, ResolveNullTipset::TakeOlder)
351 .await?;
352
353 let options = ExportOptions {
354 skip_checksum,
355 include_receipts,
356 include_events,
357 include_tipset_keys,
358 include_tipset_lookup: tipset_lookup,
359 seen: FileBackedCidHashSet::new(ctx.temp_dir.as_path())?,
360 };
361 let tmp_path = tempfile::TempPath::try_from_path(tmp_exporting_forest_car_path(&output_path))?;
362 let writer = if dry_run {
363 tokio_util::either::Either::Left(VoidAsyncWriter)
364 } else {
365 tokio_util::either::Either::Right(tokio::fs::File::create(&tmp_path).await?)
366 };
367 let chain_export = match version {
368 FilecoinSnapshotVersion::V1 => {
369 crate::chain::export::<Sha256, _>(ctx.db(), &start_ts, recent_roots, writer, options)
370 .boxed()
371 }
372 FilecoinSnapshotVersion::V2 => {
373 let f3_snap_tmp_path = {
374 let mut f3_snap_dir = output_path.clone();
375 let mut builder = tempfile::Builder::new();
376 let with_suffix = builder.suffix(".f3snap.bin");
377 if f3_snap_dir.pop() {
378 with_suffix.tempfile_in(&f3_snap_dir)
379 } else {
380 with_suffix.tempfile_in(".")
381 }?
382 .into_temp_path()
383 };
384 let f3_snap = {
385 match F3ExportLatestSnapshot::run(f3_snap_tmp_path.display().to_string()).await {
386 Ok(cid) => Some((cid, File::open(&f3_snap_tmp_path)?)),
387 Err(e) => {
388 tracing::error!("Failed to export F3 snapshot: {e:#}");
389 None
390 }
391 }
392 };
393 crate::chain::export_v2::<Sha256, _, _>(
394 ctx.db(),
395 f3_snap,
396 &start_ts,
397 recent_roots,
398 writer,
399 options,
400 )
401 .boxed()
402 }
403 };
404 match chain_export_guard.run_cancellable(chain_export).await {
405 Some(result) => {
406 let ExportResult {
407 checksum,
408 tipset_lookup: hamt,
409 } = result?;
410 if !dry_run {
411 let output_path = output_path.clone();
412 spawn_blocking_with_timeout(ASYNC_OPS_TIMEOUT, move || {
413 tmp_path.persist(&output_path)?;
414 if let Some(checksum) = checksum
417 && let Err(e) = save_checksum(checksum, &output_path)
418 {
419 tracing::warn!(
420 "failed to save the checksum file for {}: {e:#}",
421 output_path.display()
422 );
423 }
424 Ok(())
425 })
426 .await
427 .context("failed to persist the exported snapshot")?;
428 }
429 let auxiliary_exports = async {
432 match (tipset_lookup, hamt) {
433 (true, Some(hamt)) => {
434 let mut hamt = hamt.context("failed to generate tipset lookup snapshot")?;
435 let roots = nunny::vec![hamt.flush()?];
436 let hamt_output_path =
437 forest_car_with_filename_suffix(&output_path, "_tipset_lookup")?;
438 let (mut writer, hamt_output_tmp_path) = if dry_run {
439 (tokio_util::either::Either::Left(VoidAsyncWriter), None)
440 } else {
441 let tmp_path = tempfile::TempPath::try_from_path(
442 tmp_exporting_forest_car_path(&hamt_output_path),
443 )?;
444 (
445 tokio_util::either::Either::Right(
446 tokio::fs::File::create(&tmp_path).await?,
447 ),
448 Some(tmp_path),
449 )
450 };
451 hamt.into_store()
452 .export_forest_car(roots, &mut writer)
453 .await
454 .context("failed to write tipset lookup snapshot")?;
455 if let Some(hamt_output_tmp_path) = hamt_output_tmp_path {
456 hamt_output_tmp_path.persist(&hamt_output_path)?;
457 if !skip_checksum {
458 save_checksum(
460 Sha256::digest(std::fs::read(&hamt_output_path)?),
461 &hamt_output_path,
462 )?;
463 }
464 }
465 }
466 (true, None) => {
467 anyhow::bail!("requested tipset lookup snapshot is missing")
468 }
469 (false, Some(_)) => {
470 anyhow::bail!(
471 "tipset lookup snapshot should not be generated when it's not requested"
472 )
473 }
474 _ => {}
475 }
476 if augmented_snapshot {
477 let augmented_snapshot_output_path =
482 forest_car_with_filename_suffix(&output_path, "_receipts_events")?;
483 let (writer, augmented_snapshot_output_tmp_path) = if dry_run {
484 (tokio_util::either::Either::Left(VoidAsyncWriter), None)
485 } else {
486 let tmp_path = tempfile::TempPath::try_from_path(
487 tmp_exporting_forest_car_path(&augmented_snapshot_output_path),
488 )?;
489 (
490 tokio_util::either::Either::Right(
491 tokio::fs::File::create(&tmp_path).await?,
492 ),
493 Some(tmp_path),
494 )
495 };
496 crate::chain::export_receipts_events_to_forest_car(
497 ctx.db(),
498 &start_ts,
499 recent_roots,
500 writer,
501 )
502 .await
503 .context("failed to export message receipts and events snapshot")?;
504 if let Some(augmented_snapshot_output_tmp_path) =
505 augmented_snapshot_output_tmp_path
506 {
507 augmented_snapshot_output_tmp_path
508 .persist(&augmented_snapshot_output_path)?;
509 if !skip_checksum {
510 save_checksum(
512 Sha256::digest(std::fs::read(&augmented_snapshot_output_path)?),
513 &augmented_snapshot_output_path,
514 )?;
515 }
516 }
517 }
518 anyhow::Ok(())
519 };
520 match chain_export_guard.run_cancellable(auxiliary_exports).await {
521 Some(result) => {
522 result?;
523 Ok(ApiExportResult::Done)
524 }
525 None => {
526 tracing::warn!("Auxiliary snapshot exports were cancelled");
527 Ok(ApiExportResult::Cancelled)
528 }
529 }
530 }
531 None => {
532 tracing::warn!("Snapshot export was cancelled");
533 Ok(ApiExportResult::Cancelled)
534 }
535 }
536}
537
538pub enum ForestChainExportStatus {}
539impl RpcMethod<0> for ForestChainExportStatus {
540 const NAME: &'static str = "Forest.ChainExportStatus";
541 const PARAM_NAMES: [&'static str; 0] = [];
542 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
543 const PERMISSION: Permission = Permission::Read;
544 const DESCRIPTION: &'static str =
545 "Returns the progress and status of the in-progress chain export.";
546
547 type Params = ();
548 type Ok = ApiExportStatus;
549
550 async fn handle(
551 _ctx: Ctx,
552 (): Self::Params,
553 _: &http::Extensions,
554 ) -> Result<Self::Ok, ServerError> {
555 let snapshot = CHAIN_EXPORT_STATUS.snapshot();
556 let initial_epoch = snapshot.initial_epoch;
557 let epoch = snapshot.epoch;
558 let progress = if initial_epoch == 0 {
559 0.0
560 } else {
561 let p = 1.0 - ((epoch as f64) / (initial_epoch as f64));
562 if p.is_finite() {
563 p.clamp(0.0, 1.0)
564 } else {
565 0.0
566 }
567 };
568 let progress = (progress * 100.0).round() / 100.0;
570
571 Ok(ApiExportStatus {
572 state: snapshot.state,
573 kind: snapshot.kind,
574 error: snapshot.error,
575 progress,
576 start_time: snapshot.start_time,
577 current_epoch: epoch,
578 start_epoch: initial_epoch,
579 })
580 }
581}
582
583pub enum ForestChainExportCancel {}
584impl RpcMethod<0> for ForestChainExportCancel {
585 const NAME: &'static str = "Forest.ChainExportCancel";
586 const PARAM_NAMES: [&'static str; 0] = [];
587 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
588 const PERMISSION: Permission = Permission::Read;
589 const DESCRIPTION: &'static str =
590 "Cancels the in-progress chain export, returning whether one was running.";
591
592 type Params = ();
593 type Ok = bool;
594
595 async fn handle(
596 _ctx: Ctx,
597 (): Self::Params,
598 _: &http::Extensions,
599 ) -> Result<Self::Ok, ServerError> {
600 Ok(CHAIN_EXPORT_STATUS.cancel_running())
601 }
602}
603
604pub enum ForestChainExportDiff {}
605impl RpcMethod<1> for ForestChainExportDiff {
606 const NAME: &'static str = "Forest.ChainExportDiff";
607 const PARAM_NAMES: [&'static str; 1] = ["params"];
608 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all_with_v2();
609 const PERMISSION: Permission = Permission::Read;
610 const DESCRIPTION: &'static str =
611 "Exports a differential snapshot covering the given epoch range to a CAR file.";
612
613 type Params = (ForestChainExportDiffParams,);
614 type Ok = ApiExportResult;
615
616 async fn handle(
617 ctx: Ctx,
618 (params,): Self::Params,
619 _: &http::Extensions,
620 ) -> Result<Self::Ok, ServerError> {
621 let handle = tokio::spawn(async move {
624 let chain_export_guard =
625 ChainExportGuard::try_start_export(ChainExportKind::DiffSnapshot)?;
626 let result = export_diff_inner(&ctx, params, &chain_export_guard).await;
627 chain_export_guard.finish(result)
628 });
629 Ok(handle.await??)
630 }
631}
632
633async fn export_diff_inner(
634 ctx: &Ctx,
635 params: ForestChainExportDiffParams,
636 chain_export_guard: &ChainExportGuard,
637) -> anyhow::Result<ApiExportResult> {
638 let ForestChainExportDiffParams {
639 from,
640 to,
641 depth,
642 output_path,
643 } = params;
644
645 let chain_finality = ctx.chain_config().policy.chain_finality;
646 anyhow::ensure!(
647 depth >= chain_finality,
648 "depth {depth} must be greater than or equal to chain_finality {chain_finality}"
649 );
650
651 let head = ctx.chain_store().heaviest_tipset();
652 let start_ts = ctx
653 .chain_index()
654 .load_required_tipset_by_height(from, head, ResolveNullTipset::TakeOlder)
655 .await?;
656 let tmp_path = tempfile::TempPath::try_from_path(tmp_exporting_forest_car_path(&output_path))?;
657 let chain_export = crate::tool::subcommands::archive_cmd::do_export(
658 ctx.chain_index().db(),
659 start_ts,
660 Some(ctx.chain_store().genesis_tipset()),
661 tmp_path.to_path_buf(),
662 None,
663 depth,
664 Some(to),
665 Some(chain_finality),
666 true,
667 );
668
669 match chain_export_guard.run_cancellable(chain_export).await {
670 Some(result) => {
671 result?;
672 spawn_blocking_with_timeout(ASYNC_OPS_TIMEOUT, move || {
673 Ok(tmp_path.persist(&output_path)?)
674 })
675 .await
676 .context("failed to persist the exported snapshot")?;
677 Ok(ApiExportResult::Done)
678 }
679 None => {
680 tracing::warn!("Diff snapshot export was cancelled");
681 Ok(ApiExportResult::Cancelled)
682 }
683 }
684}
685
686pub enum ChainExport {}
687impl RpcMethod<1> for ChainExport {
688 const NAME: &'static str = "Filecoin.ChainExport";
689 const PARAM_NAMES: [&'static str; 1] = ["params"];
690 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
691 const PERMISSION: Permission = Permission::Read;
692 const DESCRIPTION: &'static str =
693 "Exports a v1 chain snapshot to a CAR file from the given epoch.";
694
695 type Params = (ChainExportParams,);
696 type Ok = ApiExportResult;
697
698 async fn handle(
699 ctx: Ctx,
700 (ChainExportParams {
701 epoch,
702 recent_roots,
703 output_path,
704 tipset_keys,
705 skip_checksum,
706 dry_run,
707 },): Self::Params,
708 ext: &http::Extensions,
709 ) -> Result<Self::Ok, ServerError> {
710 ForestChainExport::handle(
711 ctx,
712 (ForestChainExportParams {
713 version: FilecoinSnapshotVersion::V1,
714 epoch,
715 recent_roots,
716 output_path,
717 tipset_keys,
718 include_receipts: false,
719 include_events: false,
720 include_tipset_keys: false,
721 augmented_snapshot: false,
722 tipset_lookup: false,
723 skip_checksum,
724 dry_run,
725 },),
726 ext,
727 )
728 .await
729 }
730}
731
732pub enum ChainReadObj {}
733impl RpcMethod<1> for ChainReadObj {
734 const NAME: &'static str = "Filecoin.ChainReadObj";
735 const PARAM_NAMES: [&'static str; 1] = ["cid"];
736 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
737 const PERMISSION: Permission = Permission::Read;
738 const DESCRIPTION: &'static str = "Reads IPLD nodes referenced by the specified CID from the chain blockstore and returns raw bytes.";
739
740 type Params = (Cid,);
741 type Ok = Vec<u8>;
742
743 async fn handle(
744 ctx: Ctx,
745 (cid,): Self::Params,
746 _: &http::Extensions,
747 ) -> Result<Self::Ok, ServerError> {
748 let bytes = ctx
749 .db()
750 .get(&cid)?
751 .with_context(|| format!("can't find object with cid={cid}"))?;
752 Ok(bytes)
753 }
754}
755
756pub enum ChainHasObj {}
757impl RpcMethod<1> for ChainHasObj {
758 const NAME: &'static str = "Filecoin.ChainHasObj";
759 const PARAM_NAMES: [&'static str; 1] = ["cid"];
760 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
761 const PERMISSION: Permission = Permission::Read;
762 const DESCRIPTION: &'static str = "Checks if a given CID exists in the chain blockstore.";
763
764 type Params = (Cid,);
765 type Ok = bool;
766
767 async fn handle(
768 ctx: Ctx,
769 (cid,): Self::Params,
770 _: &http::Extensions,
771 ) -> Result<Self::Ok, ServerError> {
772 Ok(ctx.db().get(&cid)?.is_some())
773 }
774}
775
776pub enum ChainStatObj {}
779impl RpcMethod<2> for ChainStatObj {
780 const NAME: &'static str = "Filecoin.ChainStatObj";
781 const PARAM_NAMES: [&'static str; 2] = ["objCid", "baseCid"];
782 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
783 const PERMISSION: Permission = Permission::Read;
784 const DESCRIPTION: &'static str = "Returns the size and link count of the IPLD graph under the given CID, or the difference relative to an optional base CID.";
785
786 type Params = (Cid, Option<Cid>);
787 type Ok = ObjStat;
788
789 async fn handle(
790 ctx: Ctx,
791 (obj_cid, base_cid): Self::Params,
792 _: &http::Extensions,
793 ) -> Result<Self::Ok, ServerError> {
794 let mut stats = ObjStat::default();
795 let mut seen = CidHashSet::default();
796 let mut walk = |cid, collect| {
797 let mut queue = VecDeque::new();
798 queue.push_back(cid);
799 while let Some(link_cid) = queue.pop_front() {
800 if !seen.insert(link_cid) {
801 continue;
802 }
803 let data = ctx.db().get(&link_cid)?;
804 if let Some(data) = data {
805 if collect {
806 stats.links += 1;
807 stats.size += data.len();
808 }
809 if matches!(link_cid.codec(), fvm_ipld_encoding::DAG_CBOR)
810 && let Ok(ipld) =
811 crate::utils::encoding::from_slice_with_fallback::<Ipld>(&data)
812 {
813 for ipld in DfsIter::new(ipld) {
814 if let Ipld::Link(cid) = ipld {
815 queue.push_back(cid);
816 }
817 }
818 }
819 }
820 }
821 anyhow::Ok(())
822 };
823 if let Some(base_cid) = base_cid {
824 walk(base_cid, false)?;
825 }
826 walk(obj_cid, true)?;
827 Ok(stats)
828 }
829}
830
831pub enum ChainGetBlockMessages {}
832impl RpcMethod<1> for ChainGetBlockMessages {
833 const NAME: &'static str = "Filecoin.ChainGetBlockMessages";
834 const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
835 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
836 const PERMISSION: Permission = Permission::Read;
837 const DESCRIPTION: &'static str = "Returns all messages from the specified block.";
838
839 type Params = (Cid,);
840 type Ok = BlockMessages;
841
842 async fn handle(
843 ctx: Ctx,
844 (block_cid,): Self::Params,
845 _: &http::Extensions,
846 ) -> Result<Self::Ok, ServerError> {
847 let blk: CachingBlockHeader = ctx.db().get_cbor_required(&block_cid)?;
848 let (unsigned_cids, signed_cids) = crate::chain::read_msg_cids(ctx.db(), &blk)?;
849 let (bls_msg, secp_msg) =
850 crate::chain::block_messages_from_cids(ctx.db(), &unsigned_cids, &signed_cids)?;
851 let cids = unsigned_cids.into_iter().chain(signed_cids).collect();
852
853 let ret = BlockMessages {
854 bls_msg,
855 secp_msg,
856 cids,
857 };
858 Ok(ret)
859 }
860}
861
862pub enum ChainGetPath {}
863impl RpcMethod<2> for ChainGetPath {
864 const NAME: &'static str = "Filecoin.ChainGetPath";
865 const PARAM_NAMES: [&'static str; 2] = ["from", "to"];
866 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
867 const PERMISSION: Permission = Permission::Read;
868 const DESCRIPTION: &'static str = "Returns the path between the two specified tipsets.";
869
870 type Params = (TipsetKey, TipsetKey);
871 type Ok = Vec<PathChange>;
872
873 async fn handle(
874 ctx: Ctx,
875 (from, to): Self::Params,
876 _: &http::Extensions,
877 ) -> Result<Self::Ok, ServerError> {
878 Ok(chain_get_path(ctx.chain_store(), &from, &to)?.into_change_vec())
879 }
880}
881
882pub fn chain_get_path(
900 chain_store: &ChainStore,
901 from: &TipsetKey,
902 to: &TipsetKey,
903) -> anyhow::Result<PathChanges> {
904 let finality = chain_store.chain_config().policy.chain_finality;
905 let mut to_revert = chain_store
906 .load_required_tipset_or_heaviest(from)
907 .context("couldn't load `from`")?;
908 let mut to_apply = chain_store
909 .load_required_tipset_or_heaviest(to)
910 .context("couldn't load `to`")?;
911
912 anyhow::ensure!(
913 (to_apply.epoch() - to_revert.epoch()).abs() <= finality,
914 "the gap between the new head ({}) and the old head ({}) is larger than chain finality ({finality})",
915 to_apply.epoch(),
916 to_revert.epoch()
917 );
918
919 let mut reverts = vec![];
920 let mut applies = vec![];
921
922 while to_revert != to_apply {
925 if to_revert.epoch() > to_apply.epoch() {
926 let next = chain_store
927 .load_required_tipset_or_heaviest(to_revert.parents())
928 .context("couldn't load ancestor of `from`")?;
929 reverts.push(to_revert);
930 to_revert = next;
931 } else {
932 let next = chain_store
933 .load_required_tipset_or_heaviest(to_apply.parents())
934 .context("couldn't load ancestor of `to`")?;
935 applies.push(to_apply);
936 to_apply = next;
937 }
938 }
939 applies.reverse();
940 Ok(PathChanges { reverts, applies })
941}
942
943pub enum ChainGetTipSetByHeight {}
947impl RpcMethod<2> for ChainGetTipSetByHeight {
948 const NAME: &'static str = "Filecoin.ChainGetTipSetByHeight";
949 const PARAM_NAMES: [&'static str; 2] = ["height", "tipsetKey"];
950 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
951 const PERMISSION: Permission = Permission::Read;
952 const DESCRIPTION: &'static str = "Returns the tipset at the specified height.";
953
954 type Params = (ChainEpoch, ApiTipsetKey);
955 type Ok = Tipset;
956
957 async fn handle(
958 ctx: Ctx,
959 (height, ApiTipsetKey(tipset_key)): Self::Params,
960 _: &http::Extensions,
961 ) -> Result<Self::Ok, ServerError> {
962 let ts = ctx
963 .chain_store()
964 .load_required_tipset_or_heaviest(&tipset_key)?;
965 let tss = ctx
966 .chain_index()
967 .load_required_tipset_by_height(height, ts, ResolveNullTipset::TakeOlder)
968 .await?;
969 Ok(tss)
970 }
971}
972
973pub enum ChainGetTipSetAfterHeight {}
974impl RpcMethod<2> for ChainGetTipSetAfterHeight {
975 const NAME: &'static str = "Filecoin.ChainGetTipSetAfterHeight";
976 const PARAM_NAMES: [&'static str; 2] = ["height", "tipsetKey"];
977 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
978 const PERMISSION: Permission = Permission::Read;
979 const DESCRIPTION: &'static str = "Looks back and returns the tipset at the specified epoch.
980 If there are no blocks at the given epoch,
981 returns the first non-nil tipset at a later epoch.";
982
983 type Params = (ChainEpoch, ApiTipsetKey);
984 type Ok = Tipset;
985
986 async fn handle(
987 ctx: Ctx,
988 (height, ApiTipsetKey(tipset_key)): Self::Params,
989 _: &http::Extensions,
990 ) -> Result<Self::Ok, ServerError> {
991 let ts = ctx
992 .chain_store()
993 .load_required_tipset_or_heaviest(&tipset_key)?;
994 let tss = ctx
995 .chain_index()
996 .load_required_tipset_by_height(height, ts, ResolveNullTipset::TakeNewer)
997 .await?;
998 Ok(tss)
999 }
1000}
1001
1002pub enum ChainGetGenesis {}
1003impl RpcMethod<0> for ChainGetGenesis {
1004 const NAME: &'static str = "Filecoin.ChainGetGenesis";
1005 const PARAM_NAMES: [&'static str; 0] = [];
1006 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1007 const PERMISSION: Permission = Permission::Read;
1008 const DESCRIPTION: &'static str = "Returns the genesis tipset of the chain.";
1009
1010 type Params = ();
1011 type Ok = Option<Tipset>;
1012
1013 async fn handle(
1014 ctx: Ctx,
1015 (): Self::Params,
1016 _: &http::Extensions,
1017 ) -> Result<Self::Ok, ServerError> {
1018 let genesis = ctx.chain_store().genesis_block_header();
1019 Ok(Some(Tipset::from(genesis)))
1020 }
1021}
1022
1023pub enum ChainHead {}
1024impl RpcMethod<0> for ChainHead {
1025 const NAME: &'static str = "Filecoin.ChainHead";
1026 const PARAM_NAMES: [&'static str; 0] = [];
1027 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1028 const PERMISSION: Permission = Permission::Read;
1029 const DESCRIPTION: &'static str = "Returns the chain head (heaviest tipset).";
1030
1031 type Params = ();
1032 type Ok = Tipset;
1033
1034 async fn handle(
1035 ctx: Ctx,
1036 (): Self::Params,
1037 _: &http::Extensions,
1038 ) -> Result<Self::Ok, ServerError> {
1039 let heaviest = ctx.chain_store().heaviest_tipset();
1040 Ok(heaviest)
1041 }
1042}
1043
1044pub enum ChainGetBlock {}
1045impl RpcMethod<1> for ChainGetBlock {
1046 const NAME: &'static str = "Filecoin.ChainGetBlock";
1047 const PARAM_NAMES: [&'static str; 1] = ["blockCid"];
1048 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1049 const PERMISSION: Permission = Permission::Read;
1050 const DESCRIPTION: &'static str = "Returns the block with the specified CID.";
1051
1052 type Params = (Cid,);
1053 type Ok = CachingBlockHeader;
1054
1055 async fn handle(
1056 ctx: Ctx,
1057 (block_cid,): Self::Params,
1058 _: &http::Extensions,
1059 ) -> Result<Self::Ok, ServerError> {
1060 let blk: CachingBlockHeader = ctx.db().get_cbor_required(&block_cid)?;
1061 Ok(blk)
1062 }
1063}
1064
1065pub enum ChainGetTipSet {}
1066
1067impl RpcMethod<1> for ChainGetTipSet {
1068 const NAME: &'static str = "Filecoin.ChainGetTipSet";
1069 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1070 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V0 | V1 });
1071 const PERMISSION: Permission = Permission::Read;
1072 const DESCRIPTION: &'static str = "Returns the tipset with the specified CID.";
1073
1074 type Params = (ApiTipsetKey,);
1075 type Ok = Tipset;
1076
1077 async fn handle(
1078 ctx: Ctx,
1079 (ApiTipsetKey(tsk),): Self::Params,
1080 _: &http::Extensions,
1081 ) -> Result<Self::Ok, ServerError> {
1082 if let Some(tsk) = &tsk {
1083 let ts = ctx.chain_index().load_required_tipset(tsk)?;
1084 Ok(ts)
1085 } else {
1086 Err(anyhow::anyhow!(
1088 "TipsetKey cannot be empty (NewTipSet called with zero length array of blocks)"
1089 )
1090 .into())
1091 }
1092 }
1093}
1094
1095pub enum ChainGetTipSetV2 {}
1096
1097impl ChainGetTipSetV2 {
1098 pub async fn get_tipset_by_anchor(
1099 ctx: &Ctx,
1100 anchor: Option<&TipsetAnchor>,
1101 ) -> anyhow::Result<Tipset> {
1102 if let Some(anchor) = anchor {
1103 match (&anchor.key.0, &anchor.tag) {
1104 (None, None) => Ok(ctx.state_manager.heaviest_tipset()),
1106 (Some(tsk), None) => Ok(ctx.chain_index().load_required_tipset(tsk)?),
1108 (None, Some(tag)) => Self::get_tipset_by_tag(ctx, *tag).await,
1109 _ => {
1110 anyhow::bail!("invalid anchor")
1111 }
1112 }
1113 } else {
1114 Self::get_tipset_by_tag(ctx, TipsetTag::Finalized).await
1116 }
1117 }
1118
1119 pub async fn get_tipset_by_tag(ctx: &Ctx, tag: TipsetTag) -> anyhow::Result<Tipset> {
1120 match tag {
1121 TipsetTag::Latest => Ok(ctx.state_manager.heaviest_tipset()),
1122 TipsetTag::Finalized => Self::get_latest_finalized_tipset(ctx).await,
1123 TipsetTag::Safe => Self::get_latest_safe_tipset(ctx).await,
1124 }
1125 }
1126
1127 pub async fn get_latest_safe_tipset(ctx: &Ctx) -> anyhow::Result<Tipset> {
1128 let finalized = Self::get_latest_finalized_tipset(ctx).await?;
1129 let head = ctx.chain_store().heaviest_tipset();
1130 let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0);
1131 if finalized.epoch() >= safe_height {
1132 Ok(finalized)
1133 } else {
1134 Ok(ctx
1135 .chain_index()
1136 .load_required_tipset_by_height(safe_height, head, ResolveNullTipset::TakeOlder)
1137 .await?)
1138 }
1139 }
1140
1141 pub async fn get_latest_finalized_tipset(ctx: &Ctx) -> anyhow::Result<Tipset> {
1142 ChainGetTipSetFinalityStatus::get_finality_status(ctx)
1143 .await?
1144 .finalized_tip_set
1145 .context("failed to resolve finalized tipset")
1146 }
1147
1148 pub async fn get_tipset(ctx: &Ctx, selector: &TipsetSelector) -> anyhow::Result<Tipset> {
1149 selector.validate()?;
1150 if let ApiTipsetKey(Some(tsk)) = &selector.key {
1152 let ts = ctx.chain_index().load_required_tipset(tsk)?;
1153 return Ok(ts);
1154 }
1155 if let Some(height) = &selector.height {
1157 let anchor = Self::get_tipset_by_anchor(ctx, height.anchor.as_ref()).await?;
1158 let ts = ctx
1159 .chain_index()
1160 .load_required_tipset_by_height(
1161 height.at,
1162 anchor,
1163 height.resolve_null_tipset_policy(),
1164 )
1165 .await?;
1166 return Ok(ts);
1167 }
1168 if let Some(tag) = &selector.tag {
1170 let ts = Self::get_tipset_by_tag(ctx, *tag).await?;
1171 return Ok(ts);
1172 }
1173 anyhow::bail!("no tipset found for selector")
1174 }
1175}
1176
1177impl RpcMethod<1> for ChainGetTipSetV2 {
1178 const NAME: &'static str = "Filecoin.ChainGetTipSet";
1179 const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"];
1180 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1181 const PERMISSION: Permission = Permission::Read;
1182 const DESCRIPTION: &'static str = "Returns the tipset with the specified CID.";
1183
1184 type Params = (TipsetSelector,);
1185 type Ok = Tipset;
1186
1187 async fn handle(
1188 ctx: Ctx,
1189 (selector,): Self::Params,
1190 _: &http::Extensions,
1191 ) -> Result<Self::Ok, ServerError> {
1192 Ok(Self::get_tipset(&ctx, &selector).await?)
1193 }
1194}
1195
1196pub enum ChainGetTipSetFinalityStatus {}
1197
1198const EC_CALCULATOR_FINALITY_CACHE_SIZE: usize = 4;
1199impl ChainGetTipSetFinalityStatus {
1200 pub async fn get_finality_status(ctx: &Ctx) -> anyhow::Result<ChainFinalityStatus> {
1201 let head = ctx.chain_store().heaviest_tipset();
1202 let (ec_finality_threshold_depth, ec_finalized_tip_set) =
1203 Self::get_ec_finality_threshold_depth_and_tipset_with_cache(ctx, head.shallow_clone())
1204 .await?;
1205 let f3_finalized_tip_set = ctx.chain_store().f3_finalized_tipset();
1206 let finalized_tip_set = match (&ec_finalized_tip_set, &f3_finalized_tip_set) {
1207 (Some(ec), Some(f3)) => {
1208 if ec.epoch() >= f3.epoch() {
1209 Some(ec.shallow_clone())
1210 } else {
1211 Some(f3.shallow_clone())
1212 }
1213 }
1214 (Some(ec), None) => Some(ec.shallow_clone()),
1215 (None, Some(f3)) => Some(f3.shallow_clone()),
1216 (None, None) => None,
1217 };
1218 Ok(ChainFinalityStatus {
1219 ec_finality_threshold_depth,
1220 ec_finalized_tip_set,
1221 f3_finalized_tip_set,
1222 finalized_tip_set,
1223 head,
1224 })
1225 }
1226
1227 pub async fn get_ec_finality_threshold_depth_and_tipset_with_cache(
1228 ctx: &Ctx,
1229 head: Tipset,
1230 ) -> anyhow::Result<(i64, Option<Tipset>)> {
1231 static CACHE: LazyLock<quick_cache::sync::Cache<TipsetKey, (i64, Option<Tipset>)>> =
1232 LazyLock::new(|| quick_cache::sync::Cache::new(EC_CALCULATOR_FINALITY_CACHE_SIZE));
1233 CACHE
1234 .get_or_insert_async(
1235 head.shallow_clone().key(),
1236 Self::get_ec_finality_threshold_depth_and_tipset(ctx, head),
1237 )
1238 .await
1239 }
1240
1241 pub fn get_ec_finality_epoch(
1242 chain_index: &ChainIndex,
1243 chain_config: &ChainConfig,
1244 head: &Tipset,
1245 ) -> i64 {
1246 let depth =
1247 Self::get_ec_finality_threshold_depth_with_cache(chain_index, chain_config, head);
1248 Self::get_ec_finality_epoch_by_depth(chain_config, head, depth)
1249 }
1250
1251 fn get_ec_finality_epoch_by_depth(
1252 chain_config: &ChainConfig,
1253 head: &Tipset,
1254 depth: i64,
1255 ) -> i64 {
1256 if depth >= 0 {
1257 (head.epoch() - depth).max(0)
1258 } else {
1259 (head.epoch() - chain_config.policy.chain_finality).max(0)
1260 }
1261 }
1262
1263 fn get_ec_finality_threshold_depth_with_cache(
1264 chain_index: &ChainIndex,
1265 chain_config: &ChainConfig,
1266 head: &Tipset,
1267 ) -> i64 {
1268 static CACHE: LazyLock<quick_cache::sync::Cache<TipsetKey, i64>> =
1269 LazyLock::new(|| quick_cache::sync::Cache::new(EC_CALCULATOR_FINALITY_CACHE_SIZE));
1270 CACHE
1271 .get_or_insert_with(head.key(), move || -> Result<i64, Infallible> {
1272 Ok(Self::get_ec_finality_threshold_depth(
1273 chain_index,
1274 chain_config,
1275 head,
1276 ))
1277 })
1278 .expect("infallible")
1279 }
1280
1281 fn get_ec_finality_threshold_depth(
1282 chain_index: &ChainIndex,
1283 chain_config: &ChainConfig,
1284 head: &Tipset,
1285 ) -> i64 {
1286 use crate::chain::ec_finality::calculator::{
1287 DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE,
1288 find_threshold_depth,
1289 };
1290
1291 const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5;
1298
1299 let finality = chain_config.policy.chain_finality;
1300 let chain_len = finality as usize + FINALITY_CHAIN_EXTRA_EPOCHS;
1301 let mut chain = Vec::with_capacity(chain_len);
1302 let mut ts = head.shallow_clone();
1303 while chain.len() < chain_len {
1304 chain.push(ts.len() as i64);
1305 if let Ok(parent) = chain_index.load_required_tipset(ts.parents()) {
1306 if let Ok(n_null_tipsets_to_pad) = usize::try_from(ts.epoch() - parent.epoch() - 1)
1308 && n_null_tipsets_to_pad > 0
1309 {
1310 let target_len =
1311 (chain.len().saturating_add(n_null_tipsets_to_pad)).min(chain_len);
1312 chain.resize(target_len, 0);
1313 }
1314 ts = parent;
1315 } else {
1316 break;
1317 }
1318 }
1319 chain.reverse();
1321 match find_threshold_depth(
1322 &chain,
1323 finality,
1324 DEFAULT_BLOCKS_PER_EPOCH,
1325 DEFAULT_BYZANTINE_FRACTION,
1326 *DEFAULT_GUARANTEE,
1327 ) {
1328 Ok(threshold) => threshold,
1329 Err(e) => {
1330 tracing::error!(
1331 "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}"
1332 );
1333 -1
1334 }
1335 }
1336 }
1337
1338 async fn get_ec_finality_threshold_depth_and_tipset(
1339 ctx: &Ctx,
1340 head: Tipset,
1341 ) -> anyhow::Result<(i64, Option<Tipset>)> {
1342 let depth = Self::get_ec_finality_threshold_depth_with_cache(
1343 ctx.chain_index(),
1344 ctx.chain_config(),
1345 &head,
1346 );
1347 let ec_finality_epoch =
1348 Self::get_ec_finality_epoch_by_depth(ctx.chain_config(), &head, depth);
1349 let finalized = ctx
1350 .chain_index()
1351 .tipset_by_height(ec_finality_epoch, head, ResolveNullTipset::TakeOlder)
1352 .await?;
1353 Ok((depth, finalized))
1354 }
1355}
1356
1357impl RpcMethod<0> for ChainGetTipSetFinalityStatus {
1358 const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus";
1359 const PARAM_NAMES: [&'static str; 0] = [];
1360 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1361 const PERMISSION: Permission = Permission::Read;
1362 const DESCRIPTION: &'static str =
1363 "Returns a breakdown of how the node is currently determining finality.";
1364
1365 type Params = ();
1366 type Ok = ChainFinalityStatus;
1367
1368 async fn handle(
1369 ctx: Ctx,
1370 (): Self::Params,
1371 _: &http::Extensions,
1372 ) -> Result<Self::Ok, ServerError> {
1373 Ok(Self::get_finality_status(&ctx).await?)
1374 }
1375}
1376
1377pub enum ChainSetHead {}
1378impl RpcMethod<1> for ChainSetHead {
1379 const NAME: &'static str = "Filecoin.ChainSetHead";
1380 const PARAM_NAMES: [&'static str; 1] = ["tsk"];
1381 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1382 const PERMISSION: Permission = Permission::Admin;
1383 const DESCRIPTION: &'static str =
1384 "Forcibly sets the chain head to the tipset with the given key.";
1385
1386 type Params = (TipsetKey,);
1387 type Ok = ();
1388
1389 async fn handle(
1390 ctx: Ctx,
1391 (tsk,): Self::Params,
1392 _: &http::Extensions,
1393 ) -> Result<Self::Ok, ServerError> {
1394 let new_head = ctx.chain_index().load_required_tipset(&tsk)?;
1398 let mut current = ctx.chain_store().heaviest_tipset();
1399 while current.epoch() >= new_head.epoch() {
1400 for cid in current.key().to_cids() {
1401 ctx.chain_store().unmark_block_as_validated(&cid);
1402 }
1403 let parents = ¤t.block_headers().first().parents;
1404 current = ctx.chain_index().load_required_tipset(parents)?;
1405 }
1406 ctx.chain_store()
1407 .set_heaviest_tipset(new_head)
1408 .map_err(Into::into)
1409 }
1410}
1411
1412pub enum ChainGetMinBaseFee {}
1413impl RpcMethod<1> for ChainGetMinBaseFee {
1414 const NAME: &'static str = "Forest.ChainGetMinBaseFee";
1415 const PARAM_NAMES: [&'static str; 1] = ["lookback"];
1416 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1417 const PERMISSION: Permission = Permission::Read;
1418 const DESCRIPTION: &'static str =
1419 "Returns the minimum base fee across the given number of lookback tipsets, in attoFIL.";
1420
1421 type Params = (u32,);
1422 type Ok = String;
1423
1424 async fn handle(
1425 ctx: Ctx,
1426 (lookback,): Self::Params,
1427 _: &http::Extensions,
1428 ) -> Result<Self::Ok, ServerError> {
1429 let mut current = ctx.chain_store().heaviest_tipset();
1430 let mut min_base_fee = current.block_headers().first().parent_base_fee.clone();
1431
1432 for _ in 0..lookback {
1433 let parents = ¤t.block_headers().first().parents;
1434 current = ctx.chain_index().load_required_tipset(parents)?;
1435
1436 min_base_fee =
1437 min_base_fee.min(current.block_headers().first().parent_base_fee.to_owned());
1438 }
1439
1440 Ok(min_base_fee.atto().to_string())
1441 }
1442}
1443
1444pub enum ChainTipSetWeight {}
1445impl RpcMethod<1> for ChainTipSetWeight {
1446 const NAME: &'static str = "Filecoin.ChainTipSetWeight";
1447 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1448 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1449 const PERMISSION: Permission = Permission::Read;
1450 const DESCRIPTION: &'static str = "Returns the weight of the specified tipset.";
1451
1452 type Params = (ApiTipsetKey,);
1453 type Ok = BigInt;
1454
1455 async fn handle(
1456 ctx: Ctx,
1457 (ApiTipsetKey(tipset_key),): Self::Params,
1458 _: &http::Extensions,
1459 ) -> Result<Self::Ok, ServerError> {
1460 let ts = ctx
1461 .chain_store()
1462 .load_required_tipset_or_heaviest(&tipset_key)?;
1463 let weight = crate::fil_cns::weight(ctx.db(), &ts)?;
1464 Ok(weight)
1465 }
1466}
1467
1468pub enum ChainGetTipsetByParentState {}
1469impl RpcMethod<1> for ChainGetTipsetByParentState {
1470 const NAME: &'static str = "Forest.ChainGetTipsetByParentState";
1471 const PARAM_NAMES: [&'static str; 1] = ["parentState"];
1472 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1473 const PERMISSION: Permission = Permission::Read;
1474 const DESCRIPTION: &'static str = "Returns the tipset whose parent state root matches the given CID, or null if none is found.";
1475
1476 type Params = (Cid,);
1477 type Ok = Option<Tipset>;
1478
1479 async fn handle(
1480 ctx: Ctx,
1481 (parent_state,): Self::Params,
1482 _: &http::Extensions,
1483 ) -> Result<Self::Ok, ServerError> {
1484 Ok(ctx
1485 .chain_store()
1486 .heaviest_tipset()
1487 .chain(ctx.db())
1488 .find(|ts| ts.parent_state() == &parent_state)
1489 .shallow_clone())
1490 }
1491}
1492
1493pub const CHAIN_NOTIFY: &str = "Filecoin.ChainNotify";
1494pub(crate) fn chain_notify(
1495 _params: Params<'_>,
1496 data: &crate::rpc::RPCState,
1497) -> Subscriber<Vec<ApiHeadChange>> {
1498 let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
1499
1500 let current = data.chain_store().heaviest_tipset();
1502 let (change, tipset) = ("current".into(), current);
1503 sender
1504 .send(vec![ApiHeadChange { change, tipset }])
1505 .expect("receiver is not dropped");
1506
1507 let mut head_changes_rx = data.chain_store().subscribe_head_changes();
1508
1509 tokio::spawn(async move {
1510 let _ = head_changes_rx.recv().await;
1512 loop {
1513 match head_changes_rx.recv().await {
1514 Ok(changes) => {
1515 let api_changes = changes
1516 .into_change_vec()
1517 .into_iter()
1518 .map(From::from)
1519 .collect();
1520 if sender.send(api_changes).is_err() {
1521 tracing::info!("chain notify subscribers are all closed");
1522 break;
1523 }
1524 }
1525 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
1526 tracing::info!("head changes channel closed");
1527 break;
1528 }
1529 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
1530 tracing::warn!("head changes channel lagged by {n} messages");
1531 }
1532 }
1533 }
1534 });
1535 receiver
1536}
1537
1538async fn load_api_messages_from_tipset(
1539 ctx: &crate::rpc::RPCState,
1540 tipset_keys: &TipsetKey,
1541) -> Result<Vec<ApiMessage>, ServerError> {
1542 static SHOULD_BACKFILL: LazyLock<bool> = LazyLock::new(|| {
1543 let enabled = is_env_truthy("FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK");
1544 if enabled {
1545 tracing::warn!(
1546 "Full tipset backfilling from network is enabled via FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK, excessive disk and bandwidth usage is expected."
1547 );
1548 }
1549 enabled
1550 });
1551 let full_tipset = if *SHOULD_BACKFILL {
1552 get_full_tipset(
1553 &ctx.sync_network_context,
1554 ctx.chain_store(),
1555 None,
1556 tipset_keys,
1557 )
1558 .await?
1559 } else {
1560 load_full_tipset(ctx.chain_store(), tipset_keys)?
1561 };
1562 let blocks = full_tipset.into_blocks();
1563 let mut messages = vec![];
1564 let mut seen = CidHashSet::default();
1565 for Block {
1566 bls_messages,
1567 secp_messages,
1568 ..
1569 } in blocks
1570 {
1571 for message in bls_messages {
1572 let cid = message.cid();
1573 if seen.insert(cid) {
1574 messages.push(ApiMessage { cid, message });
1575 }
1576 }
1577
1578 for msg in secp_messages {
1579 let cid = msg.cid();
1580 if seen.insert(cid) {
1581 messages.push(ApiMessage {
1582 cid,
1583 message: msg.message,
1584 });
1585 }
1586 }
1587 }
1588
1589 Ok(messages)
1590}
1591
1592#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1593pub struct BlockMessages {
1594 #[serde(rename = "BlsMessages", with = "crate::lotus_json")]
1595 #[schemars(with = "LotusJson<Vec<Message>>")]
1596 pub bls_msg: Vec<Message>,
1597 #[serde(rename = "SecpkMessages", with = "crate::lotus_json")]
1598 #[schemars(with = "LotusJson<Vec<SignedMessage>>")]
1599 pub secp_msg: Vec<SignedMessage>,
1600 #[serde(rename = "Cids", with = "crate::lotus_json")]
1601 #[schemars(with = "LotusJson<Vec<Cid>>")]
1602 pub cids: Vec<Cid>,
1603}
1604lotus_json_with_self!(BlockMessages);
1605
1606#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
1607#[serde(rename_all = "PascalCase")]
1608pub struct ApiReceipt {
1609 pub exit_code: ExitCode,
1611 #[serde(rename = "Return", with = "crate::lotus_json")]
1613 #[schemars(with = "LotusJson<RawBytes>")]
1614 pub return_data: RawBytes,
1615 pub gas_used: u64,
1617 #[serde(with = "crate::lotus_json")]
1618 #[schemars(with = "LotusJson<Option<Cid>>")]
1619 pub events_root: Option<Cid>,
1620}
1621
1622lotus_json_with_self!(ApiReceipt);
1623
1624#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
1625#[serde(rename_all = "PascalCase")]
1626pub struct ApiMessage {
1627 #[serde(with = "crate::lotus_json")]
1628 #[schemars(with = "LotusJson<Cid>")]
1629 pub cid: Cid,
1630 #[serde(with = "crate::lotus_json")]
1631 #[schemars(with = "LotusJson<Message>")]
1632 pub message: Message,
1633}
1634
1635lotus_json_with_self!(ApiMessage);
1636
1637#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1638pub struct ForestChainExportParams {
1639 pub version: FilecoinSnapshotVersion,
1640 pub epoch: ChainEpoch,
1641 pub recent_roots: i64,
1642 pub output_path: PathBuf,
1643 #[schemars(with = "LotusJson<ApiTipsetKey>")]
1644 #[serde(with = "crate::lotus_json", default)]
1645 pub tipset_keys: ApiTipsetKey,
1646 #[serde(default)]
1648 pub include_receipts: bool,
1649 #[serde(default)]
1651 pub include_events: bool,
1652 #[serde(default)]
1653 pub include_tipset_keys: bool,
1654 #[serde(default)]
1656 pub augmented_snapshot: bool,
1657 #[serde(default)]
1659 pub tipset_lookup: bool,
1660 pub skip_checksum: bool,
1661 pub dry_run: bool,
1662}
1663lotus_json_with_self!(ForestChainExportParams);
1664
1665#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1666pub struct ForestChainExportDiffParams {
1667 pub from: ChainEpoch,
1668 pub to: ChainEpoch,
1669 pub depth: i64,
1670 pub output_path: PathBuf,
1671}
1672lotus_json_with_self!(ForestChainExportDiffParams);
1673
1674#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1675pub struct ChainExportParams {
1676 pub epoch: ChainEpoch,
1677 pub recent_roots: i64,
1678 pub output_path: PathBuf,
1679 #[schemars(with = "LotusJson<ApiTipsetKey>")]
1680 #[serde(with = "crate::lotus_json")]
1681 pub tipset_keys: ApiTipsetKey,
1682 pub skip_checksum: bool,
1683 pub dry_run: bool,
1684}
1685lotus_json_with_self!(ChainExportParams);
1686
1687#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, JsonSchema)]
1688#[serde(rename_all = "PascalCase")]
1689pub struct ApiHeadChange {
1690 #[serde(rename = "Type")]
1691 pub change: String,
1692 #[serde(rename = "Val", with = "crate::lotus_json")]
1693 #[schemars(with = "LotusJson<Tipset>")]
1694 pub tipset: Tipset,
1695}
1696lotus_json_with_self!(ApiHeadChange);
1697
1698impl From<HeadChange> for ApiHeadChange {
1699 fn from(change: HeadChange) -> Self {
1700 match change {
1701 HeadChange::Apply(tipset) => Self {
1702 change: "apply".into(),
1703 tipset,
1704 },
1705 HeadChange::Revert(tipset) => Self {
1706 change: "revert".into(),
1707 tipset,
1708 },
1709 }
1710 }
1711}
1712
1713#[derive(PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
1714#[serde(tag = "Type", content = "Val", rename_all = "snake_case")]
1715pub enum PathChange<T = Tipset> {
1716 Revert(T),
1717 Apply(T),
1718}
1719
1720impl<T: Clone> Clone for PathChange<T> {
1721 fn clone(&self) -> Self {
1722 match self {
1723 Self::Revert(i) => Self::Revert(i.clone()),
1724 Self::Apply(i) => Self::Apply(i.clone()),
1725 }
1726 }
1727}
1728
1729impl<T> PathChange<T> {
1730 pub fn tipset(&self) -> &T {
1731 match self {
1732 Self::Revert(ts) | Self::Apply(ts) => ts,
1733 }
1734 }
1735}
1736
1737impl HasLotusJson for PathChange {
1738 type LotusJson = PathChange<<Tipset as HasLotusJson>::LotusJson>;
1739
1740 #[cfg(test)]
1741 fn snapshots() -> Vec<(serde_json::Value, Self)> {
1742 use crate::test_utils::dummy_ticket;
1743 use serde_json::json;
1744 let header = CachingBlockHeader::new(RawBlockHeader {
1745 ticket: dummy_ticket(0),
1746 ..Default::default()
1747 });
1748 let header_cid = *header.cid();
1749 vec![(
1750 json!({
1751 "Type": "revert",
1752 "Val": {
1753 "Blocks": [
1754 {
1755 "BeaconEntries": null,
1756 "ForkSignaling": 0,
1757 "Height": 0,
1758 "Messages": { "/": "baeaaaaa" },
1759 "Miner": "f00",
1760 "ParentBaseFee": "0",
1761 "ParentMessageReceipts": { "/": "baeaaaaa" },
1762 "ParentStateRoot": { "/":"baeaaaaa" },
1763 "ParentWeight": "0",
1764 "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
1765 "Ticket": { "VRFProof": "AA==" },
1766 "Timestamp": 0,
1767 "WinPoStProof": null
1768 }
1769 ],
1770 "Cids": [
1771 { "/": header_cid.to_string() }
1772 ],
1773 "Height": 0
1774 }
1775 }),
1776 Self::Revert(Tipset::from(header)),
1777 )]
1778 }
1779
1780 fn into_lotus_json(self) -> Self::LotusJson {
1781 match self {
1782 PathChange::Revert(it) => PathChange::Revert(it.into_lotus_json()),
1783 PathChange::Apply(it) => PathChange::Apply(it.into_lotus_json()),
1784 }
1785 }
1786
1787 fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
1788 match lotus_json {
1789 PathChange::Revert(it) => PathChange::Revert(Tipset::from_lotus_json(it)),
1790 PathChange::Apply(it) => PathChange::Apply(Tipset::from_lotus_json(it)),
1791 }
1792 }
1793}
1794
1795#[derive(Debug)]
1796pub struct PathChanges<T = Tipset> {
1797 pub reverts: Vec<T>,
1798 pub applies: Vec<T>,
1799}
1800
1801impl<T: Clone> Clone for PathChanges<T> {
1802 fn clone(&self) -> Self {
1803 let Self { reverts, applies } = self;
1804 Self {
1805 reverts: reverts.clone(),
1806 applies: applies.clone(),
1807 }
1808 }
1809}
1810
1811impl<T> PathChanges<T> {
1812 pub fn into_change_vec(self) -> Vec<PathChange<T>> {
1813 let Self { reverts, applies } = self;
1814 reverts
1815 .into_iter()
1816 .map(PathChange::Revert)
1817 .chain(applies.into_iter().map(PathChange::Apply))
1818 .collect_vec()
1819 }
1820}
1821
1822#[cfg(test)]
1823impl<T> quickcheck::Arbitrary for PathChange<T>
1824where
1825 T: quickcheck::Arbitrary + ShallowClone,
1826{
1827 fn arbitrary(g: &mut quickcheck::Gen) -> Self {
1828 let inner = T::arbitrary(g);
1829 g.choose(&[PathChange::Apply(inner.clone()), PathChange::Revert(inner)])
1830 .unwrap()
1831 .clone()
1832 }
1833}
1834
1835#[test]
1836fn snapshots() {
1837 assert_all_snapshots::<PathChange>()
1838}
1839
1840#[cfg(test)]
1841#[quickcheck_macros::quickcheck]
1842fn quickcheck(val: PathChange) {
1843 assert_unchanged_via_json(val)
1844}
1845
1846#[cfg(test)]
1847mod tests {
1848 use super::*;
1849 use crate::{
1850 blocks::{Chain4U, RawBlockHeader, chain4u},
1851 db::{
1852 MemoryDB,
1853 car::{AnyCar, ManyCar},
1854 },
1855 networks::{self, ChainConfig},
1856 };
1857 use PathChange::{Apply, Revert};
1858 use std::sync::Arc;
1859
1860 #[test]
1861 fn revert_to_ancestor_linear() {
1862 let cs = ChainStore::calibnet();
1863 let db = Chain4U::with_blockstore(cs.db_owned());
1864 chain4u! {
1865 in db;
1866 [_genesis = cs.genesis_block_header()]
1867 -> [a] -> [b] -> [c, d] -> [e]
1868 };
1869
1870 assert_path_change(&cs, b, a, [Revert(&[b])]);
1872
1873 assert_path_change(&cs, [c, d], a, [Revert(&[c, d][..]), Revert(&[b])]);
1875
1876 assert_path_change(&cs, e, [c, d], [Revert(e)]);
1878
1879 assert_path_change(&cs, e, b, [Revert(&[e][..]), Revert(&[c, d])]);
1881 }
1882
1883 #[test]
1886 fn incomplete_tipsets() {
1887 let cs = ChainStore::calibnet();
1888 let db = Chain4U::with_blockstore(cs.db_owned());
1889 chain4u! {
1890 in db;
1891 [_genesis = cs.genesis_block_header()]
1892 -> [a, b] -> [c] -> [d, _e] };
1894
1895 assert_path_change(
1897 &cs,
1898 a,
1899 c,
1900 [
1901 Revert(&[a][..]), Apply(&[a, b]), Apply(&[c]), ],
1905 );
1906
1907 assert_path_change(&cs, c, d, [Apply(d)]);
1909
1910 assert_path_change(&cs, d, c, [Revert(d)]);
1912
1913 assert_path_change(
1915 &cs,
1916 c,
1917 a,
1918 [
1919 Revert(&[c][..]),
1920 Revert(&[a, b]), Apply(&[a]), ],
1923 );
1924 }
1925
1926 #[test]
1927 fn apply_to_descendant_linear() {
1928 let cs = ChainStore::calibnet();
1929 let db = Chain4U::with_blockstore(cs.db_owned());
1930 chain4u! {
1931 in db;
1932 [_genesis = cs.genesis_block_header()]
1933 -> [a] -> [b] -> [c, d] -> [e]
1934 };
1935
1936 assert_path_change(&cs, a, b, [Apply(&[b])]);
1938
1939 assert_path_change(&cs, [c, d], e, [Apply(e)]);
1941
1942 assert_path_change(&cs, b, [c, d], [Apply([c, d])]);
1944
1945 assert_path_change(&cs, b, e, [Apply(&[c, d][..]), Apply(&[e])]);
1947 }
1948
1949 #[test]
1950 fn cross_fork_simple() {
1951 let cs = ChainStore::calibnet();
1952 let db = Chain4U::with_blockstore(cs.db_owned());
1953 chain4u! {
1954 in db;
1955 [_genesis = cs.genesis_block_header()]
1956 -> [a] -> [b1] -> [c1]
1957 };
1958 chain4u! {
1959 from [a] in db;
1960 [b2] -> [c2]
1961 };
1962
1963 assert_path_change(&cs, b1, b2, [Revert(b1), Apply(b2)]);
1965
1966 assert_path_change(&cs, b1, c2, [Revert(b1), Apply(b2), Apply(c2)]);
1968
1969 let _ = (a, c1);
1970 }
1971
1972 impl ChainStore {
1973 fn _load(genesis_car: &'static [u8], genesis_cid: Cid) -> Self {
1974 let db = Arc::new(
1975 ManyCar::new(MemoryDB::default())
1976 .with_read_only(AnyCar::new(genesis_car).unwrap())
1977 .unwrap(),
1978 );
1979 let genesis_block_header: CachingBlockHeader =
1980 db.get_cbor(&genesis_cid).unwrap().unwrap();
1981 ChainStore::new(db, Arc::new(ChainConfig::calibnet()), genesis_block_header).unwrap()
1982 }
1983 pub fn calibnet() -> Self {
1984 Self::_load(
1985 networks::calibnet::DEFAULT_GENESIS,
1986 *networks::calibnet::GENESIS_CID,
1987 )
1988 }
1989 }
1990
1991 trait MakeTipset {
1993 fn make_tipset(self) -> Tipset;
1994 }
1995
1996 impl MakeTipset for &RawBlockHeader {
1997 fn make_tipset(self) -> Tipset {
1998 Tipset::from(CachingBlockHeader::new(self.clone()))
1999 }
2000 }
2001
2002 impl<const N: usize> MakeTipset for [&RawBlockHeader; N] {
2003 fn make_tipset(self) -> Tipset {
2004 self.as_slice().make_tipset()
2005 }
2006 }
2007
2008 impl<const N: usize> MakeTipset for &[&RawBlockHeader; N] {
2009 fn make_tipset(self) -> Tipset {
2010 self.as_slice().make_tipset()
2011 }
2012 }
2013
2014 impl MakeTipset for &[&RawBlockHeader] {
2015 fn make_tipset(self) -> Tipset {
2016 Tipset::new(self.iter().cloned().cloned()).unwrap()
2017 }
2018 }
2019
2020 #[track_caller]
2021 fn assert_path_change<T: MakeTipset>(
2022 store: &ChainStore,
2023 from: impl MakeTipset,
2024 to: impl MakeTipset,
2025 expected: impl IntoIterator<Item = PathChange<T>>,
2026 ) {
2027 fn print(path_change: &PathChange) {
2028 let it = match path_change {
2029 Revert(it) => {
2030 print!("Revert(");
2031 it
2032 }
2033 Apply(it) => {
2034 print!(" Apply(");
2035 it
2036 }
2037 };
2038 println!(
2039 "epoch = {}, key.cid = {})",
2040 it.epoch(),
2041 it.key().cid().unwrap()
2042 )
2043 }
2044
2045 let actual = chain_get_path(store, from.make_tipset().key(), to.make_tipset().key())
2046 .unwrap()
2047 .into_change_vec();
2048 let expected = expected
2049 .into_iter()
2050 .map(|change| match change {
2051 PathChange::Revert(it) => PathChange::Revert(it.make_tipset()),
2052 PathChange::Apply(it) => PathChange::Apply(it.make_tipset()),
2053 })
2054 .collect_vec();
2055 if expected != actual {
2056 println!("SUMMARY");
2057 println!("=======");
2058 println!("expected:");
2059 for it in &expected {
2060 print(it)
2061 }
2062 println!();
2063 println!("actual:");
2064 for it in &actual {
2065 print(it)
2066 }
2067 println!("=======\n")
2068 }
2069 assert_eq!(
2070 expected, actual,
2071 "expected change (left) does not match actual change (right)"
2072 )
2073 }
2074}