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::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
61/// Increase the file descriptor limit to a reasonable number.
62/// This prevents the node from failing if the default soft limit is too low.
63/// Note that the value is only increased, never decreased.
64fn 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
82// Start the daemon and abort if we're interrupted by ctrl-c, SIGTERM, or `forest-cli shutdown`.
83pub 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
108/// This function initialize Forest with below steps
109/// - increase file descriptor limit (for parity-db)
110/// - setup proofs parameter cache directory
111/// - prints Forest version
112fn startup_init(config: &Config) -> anyhow::Result<()> {
113    maybe_increase_fd_limit()?;
114    // Sets proof parameter file download path early, the files will be checked and
115    // downloaded later right after snapshot import step
116    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    // Sets the latest snapshot if needed for downloading later
132    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    // Import chain if needed
145    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        // Explicitly set heaviest tipset here in case HEAD_KEY has already been set
162        // in the current setting store
163        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 the snapshot progress state is not completed,
171    // set the state to not required
172    if !snapshot_tracker.is_completed() {
173        snapshot_tracker.not_required();
174    }
175
176    if let Some(validate_from) = config.client.snapshot_height {
177        // We've been provided a snapshot and asked to validate it
178        ensure_proof_params_downloaded().await?;
179        // Use the specified HEAD, otherwise take the current HEAD.
180        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        // `validate_range` is CPU-bound (drives rayon-parallel VM execution) and
187        // can run for minutes. Safer to spawn it on a blocking thread.
188        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
198/// Returns the range of epochs to validate. This includes special handling for negative `from`
199/// values, which are interpreted as offsets from the current epoch.
200fn 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    // Negative values scroll back from the current head (e.g. --height=-1000).
210    // `saturating_add` + `.max(0)` keeps extreme negatives from underflowing or
211    // wrapping to a huge positive (which would silently produce an empty range).
212    let start = if from.is_negative() {
213        current.saturating_add(from).max(0)
214    } else {
215        from
216    };
217
218    // An absolute `--height` past the head would otherwise produce an empty
219    // range and silently succeed without validating anything.
220    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 bootstrap peers are not set, set them
298    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    // Libp2p service setup
305    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    // Prefill RPC method caches for newly validated tipsets to speed up subsequent RPC calls.
370    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                        // Skip if the node is catching up to avoid unnecessary work, as the head may be changing rapidly.
381                        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                // First, compute state for the ts as it's disallowed for RPC methods by default
410                if let Err(e) = state_manager.load_executed_tipset(&ts).await {
411                    warn!("failed to load executed tipset for cache warmup: {e:#}");
412                    return; // Skip when state computation fails
413                }
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                // Warms both the FVM-replay cache and the parity-trace cache,
428                // since `eth_trace_block` calls `execution_trace` internally.
429                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 the node is stateless, GC shouldn't get triggered even on demand.
508    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    // GC shouldn't run periodically if the node is stateless or if the user has disabled it.
528    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    // already running
623    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                // wait 1s to let F3 RPC server start
672                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            // Continuously listen for head changes
727            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        // Run the collector only if chain indexer is enabled
746        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
762/// Starts daemon process
763pub(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    // Cleanup the collector prometheus metrics registry on start
793    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        // Cancel all async services
827        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    // blocking until any of the services returns an error,
844    propagate_error(&mut services)
845        .await
846        .context("services failure")
847        .map(|_| {})
848}
849
850fn warmup_in_background(ctx: &AppContext) {
851    // Re-populate `tipset_by_height` cache
852    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        // the chain ends when `ts.epoch() == 1` but it's OK as lookup for genesis is not needed
859        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 // already exists, skip
864                    }
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                        // treat lookup error as None here and try to rewrite the entry
876                        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            // Fix stale lookup mappings at null rounds which is caused by chain reorg
889            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
903/// If our current chain is below a supported height, we need a snapshot to bring it up
904/// to a supported height. If we've not been given a snapshot by the user, get one.
905///
906/// An [`Err`] should be considered fatal.
907async 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    // What height is our chain at right now, and what network version does that correspond to?
925    let network_version = chain_config.network_version(epoch);
926    let network_version_is_small = network_version < NetworkVersion::V16;
927
928    // We don't support small network versions (we can't validate from e.g genesis).
929    // So we need a snapshot (which will be from a recent network version)
930    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, _, _) => {}   // noop - don't need a snapshot
935        (true, true, _) => {} // noop - we need a snapshot, and we have one
936        (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                    // Resolve the redirect URL to get the actual snapshot URL
947                    // This ensures all chunks download from the same snapshot even if
948                    // a new snapshot is published during the download
949                    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            // we need a snapshot, don't have one, and don't have permission to download one, so ask the user
958            let (url, num_bytes, filename) = crate::cli_shared::snapshot::peek(vendor, chain)
959                .await
960                .context("couldn't get snapshot size")?;
961            // dialoguer will double-print long lines, so manually print the first clause ourselves,
962            // then let `Confirm` handle the second.
963            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                    // e.g not a tty (or some other error), so haven't got permission.
976                    .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
992/// returns the first error with which any of the services end, or never returns at all
993// This should return anyhow::Result<!> once the `Never` type is stabilized
994async 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
1005/// Run the closure on a thread where blocking is allowed
1006///
1007/// # Panics
1008/// If the closure panics
1009fn 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}