1pub mod bundle;
5mod context;
6pub mod db_util;
7pub mod main;
8
9use crate::blocks::TipsetKey;
10use crate::chain::ChainStore;
11use crate::chain_sync::ChainFollower;
12use crate::chain_sync::network_context::SyncNetworkContext;
13use crate::cli_shared::snapshot;
14use crate::cli_shared::{
15 chain_path,
16 cli::{CliOpts, Config},
17 delete_chain_data,
18};
19use crate::daemon::{context::AppContext, db_util::import_chain_as_forest_car};
20use crate::db::gc::SnapshotGarbageCollector;
21use crate::db::ttl::EthMappingCollector;
22use crate::libp2p::{Libp2pService, PeerManager};
23use crate::message_pool::{MessagePool, MpoolConfig, MpoolLocker, NonceTracker};
24use crate::networks::{self, ChainConfig};
25use crate::prelude::*;
26use crate::rpc::RPCState;
27use crate::rpc::eth::filter::EthEventHandler;
28use crate::rpc::eth::types::CallSource;
29use crate::rpc::start_rpc;
30use crate::shim::address::Address;
31use crate::shim::clock::ChainEpoch;
32use crate::shim::state_tree::StateTree;
33use crate::shim::version::NetworkVersion;
34use crate::state_manager::StateManager;
35use crate::utils::misc::env::is_env_truthy;
36use crate::utils::{self};
37use crate::utils::{proofs_api::ensure_proof_params_downloaded, version::FOREST_VERSION_STRING};
38use anyhow::{Context as _, bail};
39use backon::{ExponentialBuilder, Retryable};
40use dialoguer::theme::ColorfulTheme;
41use futures::{Future, FutureExt};
42use std::path::Path;
43use std::sync::Arc;
44use std::sync::OnceLock;
45use std::time::{Duration, Instant};
46use tokio::sync::broadcast::error::RecvError;
47use tokio::{
48 signal::{
49 ctrl_c,
50 unix::{SignalKind, signal},
51 },
52 sync::mpsc,
53 task::JoinSet,
54};
55use tokio_util::sync::CancellationToken;
56use tracing::{debug, info, warn};
57
58pub static GLOBAL_SNAPSHOT_GC: OnceLock<Arc<SnapshotGarbageCollector>> = OnceLock::new();
59
60fn maybe_increase_fd_limit() -> anyhow::Result<()> {
64 static DESIRED_SOFT_LIMIT: u64 = 8192;
65 let (soft_before, _) = rlimit::Resource::NOFILE.get()?;
66
67 let soft_after = rlimit::increase_nofile_limit(DESIRED_SOFT_LIMIT)?;
68 if soft_before < soft_after {
69 debug!("Increased file descriptor limit from {soft_before} to {soft_after}");
70 }
71 if soft_after < DESIRED_SOFT_LIMIT {
72 warn!(
73 "File descriptor limit is too low: {soft_after} < {DESIRED_SOFT_LIMIT}. \
74 You may encounter 'too many open files' errors.",
75 );
76 }
77
78 Ok(())
79}
80
81pub async fn start_interruptable(opts: CliOpts, config: Config) -> anyhow::Result<()> {
83 let start_time = chrono::Utc::now();
84 let mut terminate = signal(SignalKind::terminate())?;
85 let (shutdown_send, mut shutdown_recv) = mpsc::channel(1);
86 let (rpc_stop_handle, rpc_server_handle) = jsonrpsee::server::stop_channel();
87 let result = tokio::select! {
88 ret = start(start_time, opts, config, shutdown_send, rpc_stop_handle) => ret,
89 _ = ctrl_c() => {
90 info!("Keyboard interrupt.");
91 Ok(())
92 },
93 _ = terminate.recv() => {
94 info!("Received SIGTERM.");
95 Ok(())
96 },
97 _ = shutdown_recv.recv() => {
98 info!("Client requested a shutdown.");
99 Ok(())
100 },
101 };
102 _ = rpc_server_handle.stop();
103 crate::utils::io::terminal_cleanup();
104 result
105}
106
107fn startup_init(config: &Config) -> anyhow::Result<()> {
112 maybe_increase_fd_limit()?;
113 crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env(&config.client.data_dir);
116 info!(
117 "Starting Forest daemon, version {}",
118 FOREST_VERSION_STRING.as_str()
119 );
120 info!("Using data directory: {}", config.client.data_dir.display());
121 Ok(())
122}
123
124async fn maybe_import_snapshot(
125 opts: &CliOpts,
126 config: &mut Config,
127 ctx: &AppContext,
128) -> anyhow::Result<()> {
129 let chain_config = ctx.state_manager.chain_config();
130 if config.client.snapshot_path.is_none() && !opts.stateless {
132 maybe_set_snapshot_path(
133 config,
134 chain_config,
135 ctx.state_manager.chain_store().heaviest_tipset().epoch(),
136 opts.auto_download_snapshot,
137 &ctx.db_meta_data.get_root_dir(),
138 )
139 .await?;
140 }
141
142 let snapshot_tracker = ctx.snapshot_progress_tracker.clone();
143 if !opts.skip_load.unwrap_or_default()
145 && let Some(path) = &config.client.snapshot_path
146 {
147 let (car_db_path, ts) = import_chain_as_forest_car(
148 path,
149 &ctx.db_meta_data.get_forest_car_db_dir(),
150 config.client.import_mode,
151 config.client.rpc_v1_endpoint()?,
152 &crate::f3::get_f3_root(config),
153 ctx.chain_config(),
154 &snapshot_tracker,
155 )
156 .await?;
157 ctx.db
158 .read_only_files(std::iter::once(car_db_path.clone()))?;
159 let ts_epoch = ts.epoch();
160 ctx.state_manager.chain_store().set_heaviest_tipset(ts)?;
163 debug!(
164 "Loaded car DB at {} and set current head to epoch {ts_epoch}",
165 car_db_path.display(),
166 );
167 }
168
169 if !snapshot_tracker.is_completed() {
172 snapshot_tracker.not_required();
173 }
174
175 if let Some(validate_from) = config.client.snapshot_height {
176 ensure_proof_params_downloaded().await?;
178 let current_height = config
180 .client
181 .snapshot_head
182 .unwrap_or_else(|| ctx.state_manager.chain_store().heaviest_tipset().epoch());
183
184 let validation_range = validation_range(current_height, validate_from)?;
185 let state_manager = ctx.state_manager.shallow_clone();
188 tokio::task::spawn_blocking(move || {
189 state_manager.validate_range_blocking(validation_range)
190 })
191 .await??;
192 }
193
194 Ok(())
195}
196
197fn validation_range(
200 current: ChainEpoch,
201 from: ChainEpoch,
202) -> anyhow::Result<std::ops::RangeInclusive<ChainEpoch>> {
203 anyhow::ensure!(
204 current.is_positive(),
205 "current head epoch {current} is invalid"
206 );
207
208 let start = if from.is_negative() {
212 current.saturating_add(from).max(0)
213 } else {
214 from
215 };
216
217 anyhow::ensure!(
220 start <= current,
221 "requested validation start epoch {start} is beyond the current head at epoch {current}",
222 );
223
224 Ok(start..=current)
225}
226
227async fn maybe_start_metrics_service(
228 services: &mut JoinSet<anyhow::Result<()>>,
229 config: &Config,
230 ctx: &AppContext,
231) -> anyhow::Result<()> {
232 if config.client.enable_metrics_endpoint {
233 let prometheus_listener =
234 crate::utils::net::bind_tcp_listener(config.client.metrics_address, 0).await?;
235 info!(
236 "Prometheus server started at {}",
237 config.client.metrics_address
238 );
239 let db_directory = crate::db::db_engine::db_root(&chain_path(config))?;
240 let db = ctx.db.writer().clone();
241
242 let get_chain_head_height = Arc::new({
243 let cs = ctx.state_manager.chain_store().shallow_clone();
244 move || cs.heaviest_tipset().epoch()
245 });
246 let get_chain_head_actor_version = Arc::new({
247 let cs = ctx.state_manager.chain_store().shallow_clone();
248 move || {
249 if let Ok(state) =
250 StateTree::new_from_root(cs.db(), cs.heaviest_tipset().parent_state())
251 && let Ok(bundle_meta) = state.get_actor_bundle_metadata()
252 && let Ok(actor_version) = bundle_meta.actor_major_version()
253 {
254 actor_version
255 } else {
256 0
257 }
258 }
259 });
260 services.spawn({
261 let chain_config = ctx.chain_config().clone();
262 let get_chain_head_height = get_chain_head_height.clone();
263 async {
264 crate::metrics::init_prometheus(
265 prometheus_listener,
266 db_directory,
267 db,
268 chain_config,
269 get_chain_head_height,
270 get_chain_head_actor_version,
271 )
272 .await
273 .context("Failed to initiate prometheus server")
274 }
275 });
276
277 crate::metrics::register_collector(Box::new(
278 networks::metrics::NetworkHeightCollector::new(
279 ctx.state_manager.chain_config().block_delay_secs,
280 ctx.state_manager
281 .chain_store()
282 .genesis_block_header()
283 .timestamp,
284 get_chain_head_height,
285 ),
286 ));
287 }
288 Ok(())
289}
290
291async fn create_p2p_service(
292 services: &mut JoinSet<anyhow::Result<()>>,
293 config: &mut Config,
294 ctx: &AppContext,
295) -> anyhow::Result<Libp2pService> {
296 if config.network.bootstrap_peers.is_empty() {
298 config.network.bootstrap_peers = ctx.state_manager.chain_config().bootstrap_peers.clone();
299 }
300
301 let peer_manager = Arc::new(PeerManager::default());
302 services.spawn(peer_manager.clone().peer_operation_event_loop_task());
303 let p2p_service = Libp2pService::new(
305 config.network.clone(),
306 ctx.state_manager.chain_store().shallow_clone(),
307 peer_manager.clone(),
308 ctx.net_keypair.clone(),
309 config.chain.genesis_name(),
310 *ctx.state_manager.chain_store().genesis_block_header().cid(),
311 )
312 .await?;
313 Ok(p2p_service)
314}
315
316fn create_mpool(
317 services: &mut JoinSet<anyhow::Result<()>>,
318 p2p_service: &Libp2pService,
319 ctx: &AppContext,
320) -> anyhow::Result<MessagePool<ChainStore>> {
321 Ok(MessagePool::new(
322 ctx.state_manager.chain_store().shallow_clone(),
323 p2p_service.network_sender(),
324 MpoolConfig::load_config(ctx.db.writer().as_ref())?,
325 ctx.state_manager.chain_config().clone(),
326 services,
327 )?)
328}
329
330fn create_chain_follower(
331 opts: &CliOpts,
332 p2p_service: &Libp2pService,
333 mpool: MessagePool<ChainStore>,
334 ctx: &AppContext,
335) -> anyhow::Result<ChainFollower> {
336 let network_send = p2p_service.network_sender();
337 let peer_manager = p2p_service.peer_manager().clone();
338 let network = SyncNetworkContext::new(network_send, peer_manager, ctx.db.clone().into());
339 Ok(ChainFollower::new(
340 ctx.state_manager.shallow_clone(),
341 network,
342 ctx.state_manager.chain_store().genesis_tipset(),
343 p2p_service.network_receiver(),
344 opts.stateless,
345 mpool,
346 ))
347}
348
349fn start_chain_follower_service(
350 services: &mut JoinSet<anyhow::Result<()>>,
351 opts: &CliOpts,
352 config: &Config,
353 chain_follower: ChainFollower,
354) {
355 services.spawn({
356 let chain_follower = chain_follower.shallow_clone();
357 async move { chain_follower.run().await }
358 });
359 maybe_prefill_rpc_caches(services, opts, config, chain_follower);
360}
361
362fn maybe_prefill_rpc_caches(
363 services: &mut JoinSet<anyhow::Result<()>>,
364 opts: &CliOpts,
365 config: &Config,
366 chain_follower: ChainFollower,
367) {
368 if config.client.enable_rpc && !opts.stateless {
370 let sync_status = chain_follower.sync_status.shallow_clone();
371 let state_manager = chain_follower.state_manager.shallow_clone();
372 let mut validated_tipset_rx = chain_follower.subscribe_validated_tipset();
373 services.spawn(async move {
374 let cancellation_token = CancellationToken::new();
375 let _cancellation_token_drop_guard = cancellation_token.drop_guard_ref();
376 loop {
377 match validated_tipset_rx.recv().await {
378 Ok(_) if !sync_status.load().is_synced() => {
379 continue;
381 }
382 Ok(tsk) if state_manager.chain_store().heaviest_tipset().key() != &tsk => {
383 continue;
385 }
386 Ok(tsk) => {
387 let state_manager = state_manager.shallow_clone();
388 let cancellation_token = cancellation_token.clone();
389 tokio::spawn(async move {
390 cancellation_token
391 .run_until_cancelled(prefill_rpc_caches_for_tipset(
392 state_manager,
393 tsk,
394 cancellation_token.clone(),
395 ))
396 .await
397 });
398 }
399 Err(RecvError::Lagged(n)) => {
400 warn!("validated tipset broadcast lagged: skipped {n} tipsets")
401 }
402 Err(RecvError::Closed) => break Ok(()),
403 }
404 }
405 });
406 }
407}
408
409async fn prefill_rpc_caches_for_tipset(
410 state_manager: StateManager,
411 tsk: TipsetKey,
412 cancellation_token: CancellationToken,
413) {
414 match state_manager.chain_index().load_required_tipset(&tsk) {
415 Ok(ts) => {
416 {
417 if let Err(e) = state_manager.load_executed_tipset(&ts).await {
419 warn!("failed to load executed tipset for cache warmup: {e:#}");
420 return; }
422 }
423 {
424 if state_manager.chain_store().heaviest_tipset().key() == ts.key() {
429 tokio::spawn({
430 let state_manager = state_manager.shallow_clone();
431 let ts = ts.shallow_clone();
432 async move {
433 if let Some(Err(e)) = cancellation_token
434 .run_until_cancelled(crate::rpc::eth::eth_trace_block(
435 &state_manager,
436 &ts,
437 CallSource::Internal,
438 ))
439 .await
440 {
441 warn!("failed to call `eth_trace_block` for cache warmup: {e:#}");
442 }
443 }
444 });
445 }
446 }
447 for tx_info in [crate::rpc::eth::TxInfo::Full, crate::rpc::eth::TxInfo::Hash] {
448 if let Err(e) = crate::rpc::eth::Block::from_filecoin_tipset(
449 &state_manager,
450 ts.shallow_clone(),
451 tx_info,
452 )
453 .await
454 {
455 warn!("failed to call `Block::from_filecoin_tipset` for cache warmup: {e:#}");
456 }
457 }
458 {
459 use crate::rpc::eth::filter::{Matcher, SkipEvent};
460 struct CollectEventsCachePrefillingMatcher;
461 impl Matcher for CollectEventsCachePrefillingMatcher {
462 fn msg_cid_filter(&self) -> Option<&Cid> {
463 None
464 }
465 fn matches(
466 &self,
467 _: &Address,
468 _: &[crate::shim::executor::Entry],
469 ) -> anyhow::Result<bool> {
470 Ok(false)
471 }
472 }
473 let mut collected_events = vec![];
474 if let Err(e) = EthEventHandler::collect_events(
475 &state_manager,
476 &ts,
477 Some(&CollectEventsCachePrefillingMatcher),
478 SkipEvent::OnUnresolvedAddress,
479 &mut collected_events,
480 )
481 .await
482 {
483 warn!("failed to collect events for cache warmup: {e:#}");
484 }
485 }
486 }
487 Err(e) => {
488 warn!("failed to load tipset for cache warmup: {e:#}");
489 }
490 }
491}
492
493async fn maybe_start_health_check_service(
494 services: &mut JoinSet<anyhow::Result<()>>,
495 config: &Config,
496 p2p_service: &Libp2pService,
497 chain_follower: &ChainFollower,
498 ctx: &AppContext,
499) -> anyhow::Result<()> {
500 if config.client.enable_health_check {
501 let forest_state = crate::health::ForestState {
502 config: config.clone(),
503 chain_config: ctx.state_manager.chain_config().clone(),
504 genesis_timestamp: ctx
505 .state_manager
506 .chain_store()
507 .genesis_block_header()
508 .timestamp,
509 sync_status: chain_follower.sync_status.clone(),
510 peer_manager: p2p_service.peer_manager().clone(),
511 };
512 let healthcheck_address = forest_state.config.client.healthcheck_address;
513 info!("Healthcheck endpoint will listen at {healthcheck_address}");
514 let listener = crate::utils::net::bind_tcp_listener(healthcheck_address, 0).await?;
515 services.spawn(async move {
516 crate::health::init_healthcheck_server(forest_state, listener)
517 .await
518 .context("Failed to initiate healthcheck server")
519 });
520 } else {
521 info!("Healthcheck service is disabled");
522 }
523 Ok(())
524}
525
526fn maybe_start_gc_service(
527 services: &mut JoinSet<anyhow::Result<()>>,
528 opts: &CliOpts,
529 config: &Config,
530 chain_follower: ChainFollower,
531) -> anyhow::Result<()> {
532 if opts.stateless {
534 return Ok(());
535 }
536
537 let snap_gc = Arc::new(SnapshotGarbageCollector::new(chain_follower, config)?);
538
539 GLOBAL_SNAPSHOT_GC
540 .set(snap_gc.clone())
541 .ok()
542 .context("failed to set GLOBAL_SNAPSHOT_GC")?;
543
544 services.spawn({
545 let snap_gc = snap_gc.clone();
546 async move {
547 snap_gc.event_loop().await;
548 Ok(())
549 }
550 });
551
552 if !opts.no_gc {
554 services.spawn(async move { snap_gc.scheduler_loop().await });
555 }
556
557 Ok(())
558}
559
560#[allow(clippy::too_many_arguments)]
561fn maybe_start_rpc_service(
562 services: &mut JoinSet<anyhow::Result<()>>,
563 config: &Config,
564 mpool: MessagePool<ChainStore>,
565 chain_follower: &ChainFollower,
566 start_time: chrono::DateTime<chrono::Utc>,
567 shutdown: mpsc::Sender<()>,
568 rpc_stop_handle: jsonrpsee::server::StopHandle,
569 ctx: &AppContext,
570) -> anyhow::Result<()> {
571 if config.client.enable_rpc {
572 let rpc_address = config.client.rpc_address;
573 let metrics_mode = crate::rpc::MetricsMode::from(config.client.enable_metrics_endpoint);
574 let filter_list = config
575 .client
576 .rpc_filter_list
577 .as_ref()
578 .map(|path| crate::rpc::FilterList::new_from_file(path).map(Arc::new))
579 .transpose()?;
580 info!("JSON-RPC endpoint will listen at {rpc_address}");
581 let eth_event_handler = Arc::new(EthEventHandler::from_config(
582 &config.events,
583 ctx.chain_config().eth_chain_id,
584 mpool.subscriber(),
585 ));
586 if let Some(ttl) = eth_event_handler.filter_ttl() {
587 services.spawn({
588 let eth_event_handler = eth_event_handler.clone();
589 async move { eth_event_handler.run_filter_gc(ttl).await }
590 });
591 }
592 if is_env_truthy("FOREST_JWT_DISABLE_EXP_VALIDATION") {
593 warn!(
594 "JWT expiration validation is disabled; this significantly weakens security and should only be used in tightly controlled environments"
595 );
596 }
597 services.spawn({
598 let state_manager = ctx.state_manager.shallow_clone();
599 let bad_blocks = chain_follower.bad_blocks.shallow_clone();
600 let sync_status = chain_follower.sync_status.shallow_clone();
601 let sync_network_context = chain_follower.network.shallow_clone();
602 let tipset_send = chain_follower.tipset_sender.clone();
603 let keystore = ctx.keystore.shallow_clone();
604 let snapshot_progress_tracker = ctx.snapshot_progress_tracker.clone();
605 let nonce_tracker = NonceTracker::new();
606 let mpool_locker = MpoolLocker::new();
607 let temp_dir = Arc::new(ctx.temp_dir.clone());
608 async move {
609 let rpc_listener = crate::utils::net::bind_tcp_listener(
610 rpc_address,
611 crate::rpc::default_max_connections(),
612 )
613 .await?;
614 start_rpc(
615 RPCState {
616 state_manager,
617 keystore,
618 mpool,
619 bad_blocks,
620 sync_status,
621 eth_event_handler,
622 eth_logs_feed: Default::default(),
623 sync_network_context,
624 start_time,
625 shutdown,
626 tipset_send,
627 snapshot_progress_tracker,
628 mpool_locker,
629 nonce_tracker,
630 temp_dir,
631 },
632 rpc_listener,
633 rpc_stop_handle,
634 filter_list,
635 metrics_mode,
636 )
637 .await
638 }
639 });
640 } else {
641 debug!("RPC disabled.");
642 };
643 Ok(())
644}
645
646fn maybe_start_f3_service(opts: &CliOpts, config: &Config, ctx: &AppContext) -> anyhow::Result<()> {
647 if crate::rpc::f3::F3_LEASE_MANAGER.get().is_some() {
649 return Ok(());
650 }
651
652 if !config.client.enable_rpc {
653 if crate::f3::is_sidecar_ffi_enabled(ctx.state_manager.chain_config()) {
654 tracing::warn!("F3 sidecar is enabled but not run because RPC is disabled. ")
655 }
656 return Ok(());
657 }
658
659 if !opts.halt_after_import && !opts.stateless {
660 let rpc_endpoint = config.client.rpc_v1_endpoint()?;
661 let state_manager = &ctx.state_manager;
662 let p2p_peer_id = ctx.p2p_peer_id;
663 let admin_jwt = ctx.admin_jwt.clone();
664 tokio::task::spawn_blocking({
665 crate::rpc::f3::F3_LEASE_MANAGER
666 .set(crate::rpc::f3::F3LeaseManager::new(
667 state_manager.chain_config().network.clone(),
668 p2p_peer_id,
669 ))
670 .expect("F3 lease manager should not have been initialized before");
671 let chain_config = state_manager.chain_config().clone();
672 let f3_root = crate::f3::get_f3_root(config);
673 let crate::f3::F3Options {
674 chain_finality,
675 bootstrap_epoch,
676 initial_power_table,
677 } = crate::f3::get_f3_sidecar_params(&chain_config);
678 move || {
679 crate::f3::run_f3_sidecar_if_enabled(
680 &chain_config,
681 rpc_endpoint.to_string(),
682 admin_jwt,
683 crate::rpc::f3::get_f3_rpc_endpoint().to_string(),
684 initial_power_table
685 .map(|i| i.to_string())
686 .unwrap_or_default(),
687 bootstrap_epoch,
688 chain_finality,
689 f3_root.display().to_string(),
690 );
691 }
692 });
693 tokio::task::spawn({
694 let chain_store = ctx.chain_store().shallow_clone();
695 async move {
696 tokio::time::sleep(Duration::from_secs(1)).await;
698 match (|| crate::rpc::f3::F3GetLatestCertificate::get())
699 .retry(ExponentialBuilder::default())
700 .await
701 {
702 Ok(f3_finalized_cert) => {
703 let f3_finalized_head = f3_finalized_cert.chain_head();
704 match chain_store
705 .chain_index()
706 .load_required_tipset(&f3_finalized_head.key)
707 {
708 Ok(ts) => {
709 chain_store.set_f3_finalized_tipset(ts);
710 tracing::info!(
711 "Set F3 finalized tipset to epoch {} and key {}",
712 f3_finalized_head.epoch,
713 f3_finalized_head.key,
714 );
715 }
716 Err(e) => {
717 tracing::error!(
718 "Failed to get F3 finalized tipset epoch {} and key {}: {e}",
719 f3_finalized_head.epoch,
720 f3_finalized_head.key
721 );
722 }
723 }
724 }
725 Err(e) => {
726 tracing::error!("Failed to get F3 latest certificate: {e:#}");
727 }
728 }
729 }
730 });
731 }
732
733 Ok(())
734}
735
736fn maybe_start_indexer_service(
737 services: &mut JoinSet<anyhow::Result<()>>,
738 opts: &CliOpts,
739 config: &Config,
740 ctx: &AppContext,
741) {
742 if config.chain_indexer.enable_indexer
743 && !opts.stateless
744 && !ctx.state_manager.chain_config().is_devnet()
745 {
746 let head_changes_rx = ctx.state_manager.chain_store().subscribe_head_changes();
747 let chain_store = ctx.state_manager.chain_store().shallow_clone();
748 services.spawn(async move {
749 tracing::info!("Starting indexer service");
750
751 while let Ok(changes) = head_changes_rx.recv_async().await {
753 for ts in changes.applies {
754 tracing::debug!("Indexing tipset {}", ts.key());
755 let delegated_messages =
756 chain_store.headers_delegated_messages(ts.block_headers().iter())?;
757 chain_store.process_signed_messages(&delegated_messages, false)?;
760 }
761 }
762 Ok(())
763 });
764
765 if let Some(retention_epochs) = config.chain_indexer.gc_retention_epochs {
767 let chain_store = ctx.state_manager.chain_store().shallow_clone();
768 let chain_config = ctx.state_manager.chain_config().clone();
769 services.spawn(async move {
770 tracing::info!("Starting collector for eth_mappings");
771 let mut collector = EthMappingCollector::new(
772 chain_store.db_owned(),
773 chain_config.eth_chain_id,
774 retention_epochs.into(),
775 );
776 collector.run().await
777 });
778 }
779 }
780}
781
782pub(super) async fn start(
784 start_time: chrono::DateTime<chrono::Utc>,
785 opts: CliOpts,
786 config: Config,
787 shutdown_send: mpsc::Sender<()>,
788 rpc_stop_handle: jsonrpsee::server::StopHandle,
789) -> anyhow::Result<()> {
790 startup_init(&config)?;
791 if opts.remove_existing_chain {
792 warn!("Deleting existing chain data for {}", config.chain());
793 delete_chain_data(&config)?;
794 }
795 start_services(
796 start_time,
797 &opts,
798 config.clone(),
799 shutdown_send.clone(),
800 rpc_stop_handle,
801 )
802 .await
803}
804
805pub(super) async fn start_services(
806 start_time: chrono::DateTime<chrono::Utc>,
807 opts: &CliOpts,
808 mut config: Config,
809 shutdown_send: mpsc::Sender<()>,
810 rpc_stop_handle: jsonrpsee::server::StopHandle,
811) -> anyhow::Result<()> {
812 crate::metrics::reset_collector_registry();
814 let mut services = JoinSet::new();
815 let network = config.chain();
816 let ctx = AppContext::init(opts, &config).await?;
817 info!("Using network :: {network}");
818 utils::misc::display_chain_logo(config.chain());
819 if opts.exit_after_init {
820 return Ok(());
821 }
822 if !opts.stateless
823 && !opts.skip_load_actors
824 && let Err(e) = ctx.state_manager.maybe_rewind_heaviest_tipset().await
825 {
826 tracing::warn!("error in maybe_rewind_heaviest_tipset: {e:#}");
827 }
828
829 let p2p_service = create_p2p_service(&mut services, &mut config, &ctx).await?;
830 let mpool = create_mpool(&mut services, &p2p_service, &ctx)?;
831 let chain_follower = create_chain_follower(opts, &p2p_service, mpool.shallow_clone(), &ctx)?;
832
833 maybe_start_rpc_service(
834 &mut services,
835 &config,
836 mpool.shallow_clone(),
837 &chain_follower,
838 start_time,
839 shutdown_send.clone(),
840 rpc_stop_handle,
841 &ctx,
842 )?;
843
844 maybe_import_snapshot(opts, &mut config, &ctx).await?;
845 if opts.halt_after_import {
846 services.shutdown().await;
848 return Ok(());
849 }
850
851 warmup_in_background(&ctx);
852 maybe_start_gc_service(&mut services, opts, &config, chain_follower.shallow_clone())?;
853 maybe_start_metrics_service(&mut services, &config, &ctx).await?;
854 maybe_start_f3_service(opts, &config, &ctx)?;
855 maybe_start_health_check_service(&mut services, &config, &p2p_service, &chain_follower, &ctx)
856 .await?;
857 maybe_start_indexer_service(&mut services, opts, &config, &ctx);
858 if !opts.stateless {
859 ensure_proof_params_downloaded().await?;
860 }
861 services.spawn(p2p_service.run());
862 start_chain_follower_service(&mut services, opts, &config, chain_follower);
863 propagate_error(&mut services)
865 .await
866 .context("services failure")
867 .map(|_| {})
868}
869
870fn warmup_in_background(ctx: &AppContext) {
871 let cs = ctx.chain_store().shallow_clone();
873 tokio::task::spawn_blocking(move || {
874 let start = Instant::now();
875 let head = cs.heaviest_tipset();
876 match cs.chain_index().repair_tipset_lookup_window(
877 &head,
878 head.epoch(),
879 cs.ec_calculator_finalized_epoch(),
880 ) {
881 Ok(n_repaired) => tracing::info!(
882 "Successfully verified tipset lookup table, {n_repaired} entries repaired, took {}.",
883 humantime::format_duration(start.elapsed()),
884 ),
885 Err(e) => warn!("failed to verify tipset lookup table: {e:#?}"),
886 }
887 });
888}
889
890async fn maybe_set_snapshot_path(
895 config: &mut Config,
896 chain_config: &ChainConfig,
897 epoch: ChainEpoch,
898 auto_download_snapshot: bool,
899 download_directory: &Path,
900) -> anyhow::Result<()> {
901 if !download_directory.is_dir() {
902 anyhow::bail!(
903 "`download_directory` does not exist: {}",
904 download_directory.display()
905 );
906 }
907
908 let vendor = snapshot::TrustedVendor::default();
909 let chain = config.chain();
910
911 let network_version = chain_config.network_version(epoch);
913 let network_version_is_small = network_version < NetworkVersion::V16;
914
915 let require_a_snapshot = network_version_is_small;
918 let have_a_snapshot = config.client.snapshot_path.is_some();
919
920 match (require_a_snapshot, have_a_snapshot, auto_download_snapshot) {
921 (false, _, _) => {} (true, true, _) => {} (true, false, true) => {
924 const AUTO_SNAPSHOT_PATH_ENV_KEY: &str = "FOREST_AUTO_DOWNLOAD_SNAPSHOT_PATH";
925 match std::env::var(AUTO_SNAPSHOT_PATH_ENV_KEY) {
926 Ok(path) if !path.is_empty() => {
927 tracing::info!(
928 "importing snapshot from {path} set by `{AUTO_SNAPSHOT_PATH_ENV_KEY}`"
929 );
930 config.client.snapshot_path = Some(path.into());
931 }
932 _ => {
933 let (resolved_url, _num_bytes, filename) =
937 crate::cli_shared::snapshot::peek(vendor, chain).await?;
938 tracing::info!("Downloading snapshot: {filename}");
939 config.client.snapshot_path = Some(resolved_url.to_string().into());
940 }
941 }
942 }
943 (true, false, false) => {
944 let (url, num_bytes, filename) = crate::cli_shared::snapshot::peek(vendor, chain)
946 .await
947 .context("couldn't get snapshot size")?;
948 println!(
951 "Forest requires a snapshot to sync with the network, but automatic fetching is disabled."
952 );
953 let message = format!(
954 "Fetch a {} snapshot? (denying will exit the program). ",
955 indicatif::HumanBytes(num_bytes)
956 );
957 let have_permission = asyncify(|| {
958 dialoguer::Confirm::with_theme(&ColorfulTheme::default())
959 .with_prompt(message)
960 .default(false)
961 .interact()
962 .unwrap_or(false)
964 })
965 .await;
966 if !have_permission {
967 bail!(
968 "Forest requires a snapshot to sync with the network, but automatic fetching is disabled."
969 )
970 }
971 tracing::info!("Downloading snapshot: {filename}");
972 config.client.snapshot_path = Some(url.to_string().into());
973 }
974 };
975
976 Ok(())
977}
978
979async fn propagate_error(
982 services: &mut JoinSet<anyhow::Result<()>>,
983) -> anyhow::Result<std::convert::Infallible> {
984 while let Some(result) = services.join_next().await {
985 if let Ok(Err(error_message)) = result {
986 return Err(error_message);
987 }
988 }
989 std::future::pending().await
990}
991
992fn asyncify<T>(f: impl FnOnce() -> T + Send + 'static) -> impl Future<Output = T>
997where
998 T: Send + 'static,
999{
1000 tokio::task::spawn_blocking(f).then(|res| async { res.expect("spawned task panicked") })
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005 use rstest::rstest;
1006
1007 use super::*;
1008
1009 #[rstest]
1010 #[case::current_non_positive(0, 1, anyhow::Result::Err(anyhow::anyhow!(
1011 "current head epoch 0 is invalid"
1012 )))]
1013 #[case::current_non_positive(-1, 1, anyhow::Result::Err(anyhow::anyhow!(
1014 "current head epoch 0 is invalid"
1015 )))]
1016 #[case::from_positive_beyond_head(10, 11, anyhow::Result::Err(anyhow::anyhow!(
1017 "requested validation start epoch 11 is beyond the current head at epoch 10"
1018 )))]
1019 #[case::from_positive_within_range(10, 5, anyhow::Result::Ok(5..=10))]
1020 #[case::from_zero(10, 0, anyhow::Result::Ok(0..=10))]
1021 #[case::from_negative_within_range(10, -5, anyhow::Result::Ok(5..=10))]
1022 #[case::from_negative_beyond_range(10, -15, anyhow::Result::Ok(0..=10))]
1023 fn test_validation_range(
1024 #[case] current: ChainEpoch,
1025 #[case] from: ChainEpoch,
1026 #[case] expected: anyhow::Result<std::ops::RangeInclusive<ChainEpoch>>,
1027 ) {
1028 let result = validation_range(current, from);
1029 match expected {
1030 Ok(expected_range) => {
1031 assert_eq!(result.unwrap(), expected_range);
1032 }
1033 Err(_) => {
1034 assert!(result.is_err());
1035 }
1036 }
1037 }
1038}