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