Skip to main content

forest/daemon/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4pub 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
60/// Increase the file descriptor limit to a reasonable number.
61/// This prevents the node from failing if the default soft limit is too low.
62/// Note that the value is only increased, never decreased.
63fn 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
81// Start the daemon and abort if we're interrupted by ctrl-c, SIGTERM, or `forest-cli shutdown`.
82pub 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
107/// This function initialize Forest with below steps
108/// - increase file descriptor limit (for parity-db)
109/// - setup proofs parameter cache directory
110/// - prints Forest version
111fn startup_init(config: &Config) -> anyhow::Result<()> {
112    maybe_increase_fd_limit()?;
113    // Sets proof parameter file download path early, the files will be checked and
114    // downloaded later right after snapshot import step
115    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    // Sets the latest snapshot if needed for downloading later
131    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    // Import chain if needed
144    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        // Explicitly set heaviest tipset here in case HEAD_KEY has already been set
161        // in the current setting store
162        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 the snapshot progress state is not completed,
170    // set the state to not required
171    if !snapshot_tracker.is_completed() {
172        snapshot_tracker.not_required();
173    }
174
175    if let Some(validate_from) = config.client.snapshot_height {
176        // We've been provided a snapshot and asked to validate it
177        ensure_proof_params_downloaded().await?;
178        // Use the specified HEAD, otherwise take the current HEAD.
179        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        // `validate_range` is CPU-bound (drives rayon-parallel VM execution) and
186        // can run for minutes. Safer to spawn it on a blocking thread.
187        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
197/// Returns the range of epochs to validate. This includes special handling for negative `from`
198/// values, which are interpreted as offsets from the current epoch.
199fn 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    // Negative values scroll back from the current head (e.g. --height=-1000).
209    // `saturating_add` + `.max(0)` keeps extreme negatives from underflowing or
210    // wrapping to a huge positive (which would silently produce an empty range).
211    let start = if from.is_negative() {
212        current.saturating_add(from).max(0)
213    } else {
214        from
215    };
216
217    // An absolute `--height` past the head would otherwise produce an empty
218    // range and silently succeed without validating anything.
219    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 bootstrap peers are not set, set them
297    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    // Libp2p service setup
304    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    // Prefill RPC method caches for newly validated tipsets to speed up subsequent RPC calls.
369    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                        // Skip if the node is catching up to avoid unnecessary work, as the head may be changing rapidly.
380                        continue;
381                    }
382                    Ok(tsk) if state_manager.chain_store().heaviest_tipset().key() != &tsk => {
383                        // Skip if the tipset has already been superseded
384                        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                // First, compute state for the ts as it's disallowed for RPC methods by default
418                if let Err(e) = state_manager.load_executed_tipset(&ts).await {
419                    warn!("failed to load executed tipset for cache warmup: {e:#}");
420                    return; // Skip when state computation fails
421                }
422            }
423            {
424                // Warms both the FVM-replay cache and the parity-trace cache,
425                // since `eth_trace_block` calls `execution_trace` internally.
426                // Note that we do not block the loop here as the trace computation can be expensive.
427                // Also, we skip this tipset when it has already been superseded
428                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 the node is stateless, GC shouldn't get triggered even on demand.
533    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    // GC shouldn't run periodically if the node is stateless or if the user has disabled it.
553    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    // already running
648    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                // wait 1s to let F3 RPC server start
697                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            // Continuously listen for head changes
752            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                    // Head indexing writes the newest tipset, so use the blind-write
758                    // fast path (no read-before-write timestamp comparison).
759                    chain_store.process_signed_messages(&delegated_messages, false)?;
760                }
761            }
762            Ok(())
763        });
764
765        // Run the collector only if chain indexer is enabled
766        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
782/// Starts daemon process
783pub(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    // Cleanup the collector prometheus metrics registry on start
813    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        // Cancel all async services
847        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    // blocking until any of the services returns an error,
864    propagate_error(&mut services)
865        .await
866        .context("services failure")
867        .map(|_| {})
868}
869
870fn warmup_in_background(ctx: &AppContext) {
871    // Verify and re-populate the `tipset_by_height` lookup table over the whole chain.
872    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
890/// If our current chain is below a supported height, we need a snapshot to bring it up
891/// to a supported height. If we've not been given a snapshot by the user, get one.
892///
893/// An [`Err`] should be considered fatal.
894async 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    // What height is our chain at right now, and what network version does that correspond to?
912    let network_version = chain_config.network_version(epoch);
913    let network_version_is_small = network_version < NetworkVersion::V16;
914
915    // We don't support small network versions (we can't validate from e.g genesis).
916    // So we need a snapshot (which will be from a recent network version)
917    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, _, _) => {}   // noop - don't need a snapshot
922        (true, true, _) => {} // noop - we need a snapshot, and we have one
923        (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                    // Resolve the redirect URL to get the actual snapshot URL
934                    // This ensures all chunks download from the same snapshot even if
935                    // a new snapshot is published during the download
936                    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            // we need a snapshot, don't have one, and don't have permission to download one, so ask the user
945            let (url, num_bytes, filename) = crate::cli_shared::snapshot::peek(vendor, chain)
946                .await
947                .context("couldn't get snapshot size")?;
948            // dialoguer will double-print long lines, so manually print the first clause ourselves,
949            // then let `Confirm` handle the second.
950            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                    // e.g not a tty (or some other error), so haven't got permission.
963                    .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
979/// returns the first error with which any of the services end, or never returns at all
980// This should return anyhow::Result<!> once the `Never` type is stabilized
981async 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
992/// Run the closure on a thread where blocking is allowed
993///
994/// # Panics
995/// If the closure panics
996fn 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}