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::db::EthMappingsStore;
15use crate::ipld::DfsIter;
16use crate::ipld::{CHAIN_EXPORT_STATUS, cancel_export, end_export, start_export};
17use crate::lotus_json::{HasLotusJson, LotusJson, lotus_json_with_self};
18#[cfg(test)]
19use crate::lotus_json::{assert_all_snapshots, assert_unchanged_via_json};
20use crate::message::{ChainMessage, SignedMessage};
21use crate::rpc::eth::Block as EthBlock;
22use crate::rpc::eth::{
23 EthLog, TxInfo, eth_logs_with_filter, types::ApiHeaders, types::EthFilterSpec,
24};
25use crate::rpc::f3::F3ExportLatestSnapshot;
26use crate::rpc::types::*;
27use crate::rpc::{ApiPaths, Ctx, EthEventHandler, Permission, RpcMethod, ServerError};
28use crate::shim::clock::ChainEpoch;
29use crate::shim::error::ExitCode;
30use crate::shim::executor::Receipt;
31use crate::shim::message::Message;
32use crate::utils::ShallowClone;
33use crate::utils::db::CborStoreExt as _;
34use crate::utils::io::VoidAsyncWriter;
35use crate::utils::misc::env::is_env_truthy;
36use anyhow::{Context as _, Result};
37use cid::Cid;
38use enumflags2::{BitFlags, make_bitflags};
39use fvm_ipld_blockstore::Blockstore;
40use fvm_ipld_encoding::{CborStore, RawBytes};
41use hex::ToHex;
42use ipld_core::ipld::Ipld;
43use itertools::Itertools as _;
44use jsonrpsee::types::Params;
45use jsonrpsee::types::error::ErrorObjectOwned;
46use num::BigInt;
47use schemars::JsonSchema;
48use serde::{Deserialize, Serialize};
49use sha2::Sha256;
50use std::fs::File;
51use std::sync::Arc;
52use std::{collections::VecDeque, path::PathBuf, sync::LazyLock};
53use tokio::sync::{
54 Mutex,
55 broadcast::{self, Receiver as Subscriber},
56};
57use tokio::task::JoinHandle;
58use tokio_util::sync::CancellationToken;
59
60const HEAD_CHANNEL_CAPACITY: usize = 10;
61
62pub const SAFE_HEIGHT_DISTANCE: ChainEpoch = 200;
76
77static CHAIN_EXPORT_LOCK: LazyLock<Mutex<Option<CancellationToken>>> =
78 LazyLock::new(|| Mutex::new(None));
79
80pub(crate) fn new_heads<DB: Blockstore + EthMappingsStore + Send + Sync + 'static>(
87 data: Ctx<DB>,
88) -> (Subscriber<ApiHeaders>, JoinHandle<()>) {
89 let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
90
91 let mut head_changes_rx = data.chain_store().subscribe_head_changes();
92
93 let handle = tokio::spawn(async move {
94 while let Ok(changes) = head_changes_rx.recv().await {
95 for ts in changes.applies {
96 match EthBlock::from_filecoin_tipset(data.clone(), ts, TxInfo::Full).await {
99 Ok(block) => {
100 if let Err(e) = sender.send(ApiHeaders(block)) {
101 tracing::error!("Failed to send headers: {}", e);
102 return;
103 }
104 }
105 Err(e) => {
106 tracing::error!("Failed to convert tipset to eth block: {}", e);
107 }
108 }
109 }
110 }
111 });
112
113 (receiver, handle)
114}
115
116pub(crate) fn logs<DB: Blockstore + EthMappingsStore + Sync + Send + 'static>(
123 ctx: &Ctx<DB>,
124 filter: Option<EthFilterSpec>,
125) -> (Subscriber<Vec<EthLog>>, JoinHandle<()>) {
126 let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
127
128 let mut head_changes_rx = ctx.chain_store().subscribe_head_changes();
129
130 let ctx = ctx.clone();
131
132 let handle = tokio::spawn(async move {
133 while let Ok(changes) = head_changes_rx.recv().await {
134 for ts in changes.applies {
135 match eth_logs_with_filter(&ctx, &ts, filter.clone()).await {
136 Ok(logs) => {
137 if !logs.is_empty()
138 && let Err(e) = sender.send(logs)
139 {
140 tracing::error!("Failed to send logs for tipset {}: {}", ts.key(), e);
141 break;
142 }
143 }
144 Err(e) => {
145 tracing::error!("Failed to fetch logs for tipset {}: {}", ts.key(), e);
146 }
147 }
148 }
149 }
150 });
151
152 (receiver, handle)
153}
154
155pub enum ChainGetFinalizedTipset {}
156impl RpcMethod<0> for ChainGetFinalizedTipset {
157 const NAME: &'static str = "Filecoin.ChainGetFinalizedTipSet";
158 const PARAM_NAMES: [&'static str; 0] = [];
159 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::V1);
160 const PERMISSION: Permission = Permission::Read;
161 const DESCRIPTION: Option<&'static str> = Some(
162 "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.",
163 );
164
165 type Params = ();
166 type Ok = Tipset;
167
168 async fn handle(
169 ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
170 (): Self::Params,
171 _: &http::Extensions,
172 ) -> Result<Self::Ok, ServerError> {
173 Ok(ChainGetTipSetV2::get_latest_finalized_tipset(&ctx).await?)
174 }
175}
176
177pub enum ChainGetMessage {}
178impl RpcMethod<1> for ChainGetMessage {
179 const NAME: &'static str = "Filecoin.ChainGetMessage";
180 const PARAM_NAMES: [&'static str; 1] = ["messageCid"];
181 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
182 const PERMISSION: Permission = Permission::Read;
183 const DESCRIPTION: Option<&'static str> = Some("Returns the message with the specified CID.");
184
185 type Params = (Cid,);
186 type Ok = Message;
187
188 async fn handle(
189 ctx: Ctx<impl Blockstore>,
190 (message_cid,): Self::Params,
191 _: &http::Extensions,
192 ) -> Result<Self::Ok, ServerError> {
193 let chain_message: ChainMessage = ctx
194 .store()
195 .get_cbor(&message_cid)?
196 .with_context(|| format!("can't find message with cid {message_cid}"))?;
197 let message = match chain_message {
198 ChainMessage::Signed(m) => Arc::unwrap_or_clone(m).into_message(),
199 ChainMessage::Unsigned(m) => Arc::unwrap_or_clone(m),
200 };
201
202 Ok(message)
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 + EthMappingsStore + 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 + EthMappingsStore + 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 + EthMappingsStore + 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 + EthMappingsStore>,
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 + EthMappingsStore>,
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 + EthMappingsStore + 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 + EthMappingsStore + 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 + EthMappingsStore + 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 + EthMappingsStore + 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 + EthMappingsStore + 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 + EthMappingsStore + 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(
1143 ctx: &Ctx<impl Blockstore + EthMappingsStore>,
1144 ) -> anyhow::Result<ChainFinalityStatus> {
1145 let head = ctx.chain_store().heaviest_tipset();
1146 let (ec_finality_threshold_depth, ec_finalized_tip_set) =
1147 Self::get_ec_finality_threshold_depth_and_tipset_with_cache(ctx, head.shallow_clone())?;
1148 let f3_finalized_tip_set = ctx.chain_store().f3_finalized_tipset();
1149 let finalized_tip_set = match (&ec_finalized_tip_set, &f3_finalized_tip_set) {
1150 (Some(ec), Some(f3)) => {
1151 if ec.epoch() >= f3.epoch() {
1152 Some(ec.shallow_clone())
1153 } else {
1154 Some(f3.shallow_clone())
1155 }
1156 }
1157 (Some(ec), None) => Some(ec.shallow_clone()),
1158 (None, Some(f3)) => Some(f3.shallow_clone()),
1159 (None, None) => None,
1160 };
1161 Ok(ChainFinalityStatus {
1162 ec_finality_threshold_depth,
1163 ec_finalized_tip_set,
1164 f3_finalized_tip_set,
1165 finalized_tip_set,
1166 head,
1167 })
1168 }
1169
1170 pub fn get_ec_finality_threshold_depth_and_tipset_with_cache(
1171 ctx: &Ctx<impl Blockstore + EthMappingsStore>,
1172 head: Tipset,
1173 ) -> anyhow::Result<(i64, Option<Tipset>)> {
1174 static CACHE: parking_lot::Mutex<Option<(Tipset, i64, Option<Tipset>)>> =
1175 parking_lot::Mutex::new(None);
1176 let mut cache = CACHE.lock();
1177 if let Some((cached_head, cached_threshold, cached_tipset)) = &*cache
1178 && cached_head == &head
1179 {
1180 Ok((*cached_threshold, cached_tipset.shallow_clone()))
1181 } else {
1182 let (threshold, tipset) =
1183 Self::get_ec_finality_threshold_depth_and_tipset(ctx, head.shallow_clone())?;
1184 *cache = Some((head, threshold, tipset.shallow_clone()));
1185 Ok((threshold, tipset))
1186 }
1187 }
1188
1189 fn get_ec_finality_threshold_depth_and_tipset(
1190 ctx: &Ctx<impl Blockstore + EthMappingsStore>,
1191 head: Tipset,
1192 ) -> anyhow::Result<(i64, Option<Tipset>)> {
1193 use crate::chain::ec_finality::calculator::{
1194 DEFAULT_BLOCKS_PER_EPOCH, DEFAULT_BYZANTINE_FRACTION, DEFAULT_GUARANTEE,
1195 find_threshold_depth,
1196 };
1197
1198 const FINALITY_CHAIN_EXTRA_EPOCHS: usize = 5;
1205
1206 let finality = ctx.chain_config().policy.chain_finality;
1207 let chain_len = finality as usize + FINALITY_CHAIN_EXTRA_EPOCHS;
1208 let mut chain = Vec::with_capacity(chain_len);
1209 let mut ts = head.shallow_clone();
1210 while chain.len() < chain_len {
1211 chain.push(ts.len() as i64);
1212 if let Ok(parent) = ctx.chain_index().load_required_tipset(ts.parents()) {
1213 if let Ok(n_null_tipsets_to_pad) = usize::try_from(ts.epoch() - parent.epoch() - 1)
1215 && n_null_tipsets_to_pad > 0
1216 {
1217 let target_len =
1218 (chain.len().saturating_add(n_null_tipsets_to_pad)).min(chain_len);
1219 chain.resize(target_len, 0);
1220 }
1221 ts = parent;
1222 } else {
1223 break;
1224 }
1225 }
1226 chain.reverse();
1228 let depth = match find_threshold_depth(
1229 &chain,
1230 finality,
1231 DEFAULT_BLOCKS_PER_EPOCH,
1232 DEFAULT_BYZANTINE_FRACTION,
1233 *DEFAULT_GUARANTEE,
1234 ) {
1235 Ok(threshold) => threshold,
1236 Err(e) => {
1237 tracing::error!(
1238 "Failed to calculate EC finality threshold depth: {e:#}, chain: {chain:?}"
1239 );
1240 -1
1241 }
1242 };
1243 let finalized = if depth >= 0
1244 && let Ok(Some(ts)) = ctx.chain_index().tipset_by_height(
1245 (head.epoch() - depth).max(0),
1246 head.shallow_clone(),
1247 ResolveNullTipset::TakeOlder,
1248 ) {
1249 Some(ts)
1250 } else {
1251 let ec_finality_epoch =
1252 (head.epoch() - ctx.chain_config().policy.chain_finality).max(0);
1253 ctx.chain_index().tipset_by_height(
1254 ec_finality_epoch,
1255 head,
1256 ResolveNullTipset::TakeOlder,
1257 )?
1258 };
1259 Ok((depth, finalized))
1260 }
1261}
1262
1263impl RpcMethod<0> for ChainGetTipSetFinalityStatus {
1264 const NAME: &'static str = "Filecoin.ChainGetTipSetFinalityStatus";
1265 const PARAM_NAMES: [&'static str; 0] = [];
1266 const API_PATHS: BitFlags<ApiPaths> = make_bitflags!(ApiPaths::{ V2 });
1267 const PERMISSION: Permission = Permission::Read;
1268 const DESCRIPTION: Option<&'static str> =
1269 Some("Returns a breakdown of how the node is currently determining finality.");
1270
1271 type Params = ();
1272 type Ok = ChainFinalityStatus;
1273
1274 async fn handle(
1275 ctx: Ctx<impl Blockstore + EthMappingsStore + Send + Sync + 'static>,
1276 (): Self::Params,
1277 _: &http::Extensions,
1278 ) -> Result<Self::Ok, ServerError> {
1279 Ok(Self::get_finality_status(&ctx)?)
1280 }
1281}
1282
1283pub enum ChainSetHead {}
1284impl RpcMethod<1> for ChainSetHead {
1285 const NAME: &'static str = "Filecoin.ChainSetHead";
1286 const PARAM_NAMES: [&'static str; 1] = ["tsk"];
1287 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1288 const PERMISSION: Permission = Permission::Admin;
1289
1290 type Params = (TipsetKey,);
1291 type Ok = ();
1292
1293 async fn handle(
1294 ctx: Ctx<impl Blockstore>,
1295 (tsk,): Self::Params,
1296 _: &http::Extensions,
1297 ) -> Result<Self::Ok, ServerError> {
1298 let new_head = ctx.chain_index().load_required_tipset(&tsk)?;
1302 let mut current = ctx.chain_store().heaviest_tipset();
1303 while current.epoch() >= new_head.epoch() {
1304 for cid in current.key().to_cids() {
1305 ctx.chain_store().unmark_block_as_validated(&cid);
1306 }
1307 let parents = ¤t.block_headers().first().parents;
1308 current = ctx.chain_index().load_required_tipset(parents)?;
1309 }
1310 ctx.chain_store()
1311 .set_heaviest_tipset(new_head)
1312 .map_err(Into::into)
1313 }
1314}
1315
1316pub enum ChainGetMinBaseFee {}
1317impl RpcMethod<1> for ChainGetMinBaseFee {
1318 const NAME: &'static str = "Forest.ChainGetMinBaseFee";
1319 const PARAM_NAMES: [&'static str; 1] = ["lookback"];
1320 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1321 const PERMISSION: Permission = Permission::Read;
1322
1323 type Params = (u32,);
1324 type Ok = String;
1325
1326 async fn handle(
1327 ctx: Ctx<impl Blockstore>,
1328 (lookback,): Self::Params,
1329 _: &http::Extensions,
1330 ) -> Result<Self::Ok, ServerError> {
1331 let mut current = ctx.chain_store().heaviest_tipset();
1332 let mut min_base_fee = current.block_headers().first().parent_base_fee.clone();
1333
1334 for _ in 0..lookback {
1335 let parents = ¤t.block_headers().first().parents;
1336 current = ctx.chain_index().load_required_tipset(parents)?;
1337
1338 min_base_fee =
1339 min_base_fee.min(current.block_headers().first().parent_base_fee.to_owned());
1340 }
1341
1342 Ok(min_base_fee.atto().to_string())
1343 }
1344}
1345
1346pub enum ChainTipSetWeight {}
1347impl RpcMethod<1> for ChainTipSetWeight {
1348 const NAME: &'static str = "Filecoin.ChainTipSetWeight";
1349 const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"];
1350 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1351 const PERMISSION: Permission = Permission::Read;
1352 const DESCRIPTION: Option<&'static str> = Some("Returns the weight of the specified tipset.");
1353
1354 type Params = (ApiTipsetKey,);
1355 type Ok = BigInt;
1356
1357 async fn handle(
1358 ctx: Ctx<impl Blockstore>,
1359 (ApiTipsetKey(tipset_key),): Self::Params,
1360 _: &http::Extensions,
1361 ) -> Result<Self::Ok, ServerError> {
1362 let ts = ctx
1363 .chain_store()
1364 .load_required_tipset_or_heaviest(&tipset_key)?;
1365 let weight = crate::fil_cns::weight(ctx.store(), &ts)?;
1366 Ok(weight)
1367 }
1368}
1369
1370pub enum ChainGetTipsetByParentState {}
1371impl RpcMethod<1> for ChainGetTipsetByParentState {
1372 const NAME: &'static str = "Forest.ChainGetTipsetByParentState";
1373 const PARAM_NAMES: [&'static str; 1] = ["parentState"];
1374 const API_PATHS: BitFlags<ApiPaths> = ApiPaths::all();
1375 const PERMISSION: Permission = Permission::Read;
1376
1377 type Params = (Cid,);
1378 type Ok = Option<Tipset>;
1379
1380 async fn handle(
1381 ctx: Ctx<impl Blockstore>,
1382 (parent_state,): Self::Params,
1383 _: &http::Extensions,
1384 ) -> Result<Self::Ok, ServerError> {
1385 Ok(ctx
1386 .chain_store()
1387 .heaviest_tipset()
1388 .chain(ctx.store())
1389 .find(|ts| ts.parent_state() == &parent_state)
1390 .shallow_clone())
1391 }
1392}
1393
1394pub const CHAIN_NOTIFY: &str = "Filecoin.ChainNotify";
1395pub(crate) fn chain_notify<DB: Blockstore>(
1396 _params: Params<'_>,
1397 data: &crate::rpc::RPCState<DB>,
1398) -> Subscriber<Vec<ApiHeadChange>> {
1399 let (sender, receiver) = broadcast::channel(HEAD_CHANNEL_CAPACITY);
1400
1401 let current = data.chain_store().heaviest_tipset();
1403 let (change, tipset) = ("current".into(), current);
1404 sender
1405 .send(vec![ApiHeadChange { change, tipset }])
1406 .expect("receiver is not dropped");
1407
1408 let mut head_changes_rx = data.chain_store().subscribe_head_changes();
1409
1410 tokio::spawn(async move {
1411 let _ = head_changes_rx.recv().await;
1413 while let Ok(changes) = head_changes_rx.recv().await {
1414 let api_changes = changes
1415 .into_change_vec()
1416 .into_iter()
1417 .map(From::from)
1418 .collect();
1419 if sender.send(api_changes).is_err() {
1420 break;
1421 }
1422 }
1423 });
1424 receiver
1425}
1426
1427async fn load_api_messages_from_tipset<DB: Blockstore + Send + Sync + 'static>(
1428 ctx: &crate::rpc::RPCState<DB>,
1429 tipset_keys: &TipsetKey,
1430) -> Result<Vec<ApiMessage>, ServerError> {
1431 static SHOULD_BACKFILL: LazyLock<bool> = LazyLock::new(|| {
1432 let enabled = is_env_truthy("FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK");
1433 if enabled {
1434 tracing::warn!(
1435 "Full tipset backfilling from network is enabled via FOREST_RPC_BACKFILL_FULL_TIPSET_FROM_NETWORK, excessive disk and bandwidth usage is expected."
1436 );
1437 }
1438 enabled
1439 });
1440 let full_tipset = if *SHOULD_BACKFILL {
1441 get_full_tipset(
1442 &ctx.sync_network_context,
1443 ctx.chain_store(),
1444 None,
1445 tipset_keys,
1446 )
1447 .await?
1448 } else {
1449 load_full_tipset(ctx.chain_store(), tipset_keys)?
1450 };
1451 let blocks = full_tipset.into_blocks();
1452 let mut messages = vec![];
1453 let mut seen = CidHashSet::default();
1454 for Block {
1455 bls_messages,
1456 secp_messages,
1457 ..
1458 } in blocks
1459 {
1460 for message in bls_messages {
1461 let cid = message.cid();
1462 if seen.insert(cid) {
1463 messages.push(ApiMessage { cid, message });
1464 }
1465 }
1466
1467 for msg in secp_messages {
1468 let cid = msg.cid();
1469 if seen.insert(cid) {
1470 messages.push(ApiMessage {
1471 cid,
1472 message: msg.message,
1473 });
1474 }
1475 }
1476 }
1477
1478 Ok(messages)
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1482pub struct BlockMessages {
1483 #[serde(rename = "BlsMessages", with = "crate::lotus_json")]
1484 #[schemars(with = "LotusJson<Vec<Message>>")]
1485 pub bls_msg: Vec<Message>,
1486 #[serde(rename = "SecpkMessages", with = "crate::lotus_json")]
1487 #[schemars(with = "LotusJson<Vec<SignedMessage>>")]
1488 pub secp_msg: Vec<SignedMessage>,
1489 #[serde(rename = "Cids", with = "crate::lotus_json")]
1490 #[schemars(with = "LotusJson<Vec<Cid>>")]
1491 pub cids: Vec<Cid>,
1492}
1493lotus_json_with_self!(BlockMessages);
1494
1495#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
1496#[serde(rename_all = "PascalCase")]
1497pub struct ApiReceipt {
1498 pub exit_code: ExitCode,
1500 #[serde(rename = "Return", with = "crate::lotus_json")]
1502 #[schemars(with = "LotusJson<RawBytes>")]
1503 pub return_data: RawBytes,
1504 pub gas_used: u64,
1506 #[serde(with = "crate::lotus_json")]
1507 #[schemars(with = "LotusJson<Option<Cid>>")]
1508 pub events_root: Option<Cid>,
1509}
1510
1511lotus_json_with_self!(ApiReceipt);
1512
1513#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq)]
1514#[serde(rename_all = "PascalCase")]
1515pub struct ApiMessage {
1516 #[serde(with = "crate::lotus_json")]
1517 #[schemars(with = "LotusJson<Cid>")]
1518 pub cid: Cid,
1519 #[serde(with = "crate::lotus_json")]
1520 #[schemars(with = "LotusJson<Message>")]
1521 pub message: Message,
1522}
1523
1524lotus_json_with_self!(ApiMessage);
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}