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