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