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