Skip to main content

forest/chain_sync/
sync_status.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3use crate::blocks::TipsetKey;
4use crate::lotus_json::lotus_json_with_self;
5use crate::networks::calculate_expected_epoch;
6use crate::shim::clock::ChainEpoch;
7use crate::state_manager::StateManager;
8use chrono::{DateTime, Utc};
9use fvm_ipld_blockstore::Blockstore;
10use parking_lot::RwLock;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::sync::Arc;
14use tracing::log;
15
16// Node considered synced if the head is within this threshold.
17const SYNCED_EPOCH_THRESHOLD: u64 = 10;
18
19/// Represents the overall synchronization status of the Forest node.
20#[derive(
21    Serialize,
22    Deserialize,
23    Debug,
24    Clone,
25    Copy,
26    Default,
27    PartialEq,
28    Eq,
29    JsonSchema,
30    strum::Display,
31    strum::EnumString,
32)]
33pub enum NodeSyncStatus {
34    /// Node is initializing, status not yet determined.
35    #[default]
36    #[strum(to_string = "Intializing")]
37    Initializing,
38    /// Node is significantly behind the network head and actively downloading/validating.
39    #[strum(to_string = "Syncing")]
40    Syncing,
41    /// Node is close to the network head, within the `SYNCED_EPOCH_THRESHOLD`.
42    #[strum(to_string = "Synced")]
43    Synced,
44    /// An error occurred during the sync process.
45    #[strum(to_string = "Error")]
46    Error,
47    /// Node is configured to not sync (offline mode).
48    #[strum(to_string = "Offline")]
49    Offline,
50}
51
52/// Represents the stage of processing for a specific chain fork being tracked.
53#[derive(
54    Serialize,
55    Deserialize,
56    Debug,
57    Clone,
58    PartialEq,
59    Eq,
60    JsonSchema,
61    strum::Display,
62    strum::EnumString,
63)]
64pub enum ForkSyncStage {
65    /// Fetching necessary block headers for this fork.
66    #[strum(to_string = "Fetching Headers")]
67    FetchingHeaders,
68    /// Validating tipsets and messages for this fork.
69    #[strum(to_string = "Validating Tipsets")]
70    ValidatingTipsets,
71    /// This fork sync process is complete (e.g., reached target, merged, or deemed invalid).
72    #[strum(to_string = "Complete")]
73    Complete,
74    /// Progress is stalled, potentially waiting for dependencies.
75    #[strum(to_string = "Stalled")]
76    Stalled,
77    /// An error occurred processing this specific fork.
78    #[strum(to_string = "Error")]
79    Error,
80}
81
82/// Contains information about a specific chain/fork the node is actively tracking or syncing.
83#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
84pub struct ForkSyncInfo {
85    /// The target tipset key for this synchronization task.
86    #[schemars(with = "crate::lotus_json::LotusJson<TipsetKey>")]
87    #[serde(with = "crate::lotus_json")]
88    pub(crate) target_tipset_key: TipsetKey,
89    /// The target epoch for this synchronization task.
90    pub(crate) target_epoch: ChainEpoch,
91    /// The lowest epoch that still needs processing (fetching or validating) for this target.
92    /// This helps indicate the start of the current sync range.
93    pub(crate) target_sync_epoch_start: ChainEpoch,
94    /// The current stage of processing for this fork.
95    pub(crate) stage: ForkSyncStage,
96    /// The epoch of the heaviest fully validated tipset on the node's main chain.
97    /// This shows overall node progress, distinct from fork-specific progress.
98    pub(crate) validated_chain_head_epoch: ChainEpoch,
99    /// When processing for this fork started.
100    pub(crate) start_time: Option<DateTime<Utc>>,
101    /// Last time status for this fork was updated.
102    pub(crate) last_updated: Option<DateTime<Utc>>,
103}
104
105pub type SyncStatus = Arc<RwLock<SyncStatusReport>>;
106
107/// Contains information about the current status of the node's synchronization process.
108#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
109pub struct SyncStatusReport {
110    /// Overall status of the node's synchronization.
111    pub(crate) status: NodeSyncStatus,
112    /// The epoch of the heaviest validated tipset on the node's main chain.
113    pub(crate) current_head_epoch: ChainEpoch,
114    /// The tipset key of the current heaviest validated tipset.
115    #[schemars(with = "crate::lotus_json::LotusJson<TipsetKey>")]
116    #[serde(with = "crate::lotus_json")]
117    pub(crate) current_head_key: Option<TipsetKey>,
118    // Current highest epoch on the network.
119    pub(crate) network_head_epoch: ChainEpoch,
120    /// Estimated number of epochs the node is behind the network head.
121    /// Can be negative if the node is slightly ahead, due to estimation variance.
122    pub(crate) epochs_behind: i64,
123    /// List of active fork synchronization tasks the node is currently handling.
124    pub(crate) active_forks: Vec<ForkSyncInfo>,
125    /// When the node process started.
126    pub(crate) node_start_time: DateTime<Utc>,
127    /// Last time this status report was generated.
128    pub(crate) last_updated: DateTime<Utc>,
129}
130
131lotus_json_with_self!(SyncStatusReport);
132
133impl SyncStatusReport {
134    pub(crate) fn init() -> Self {
135        Self {
136            node_start_time: Utc::now(),
137            ..Default::default()
138        }
139    }
140
141    /// Updates the sync status report based on the current state of the node and network.
142    /// This does not modify the existing report but returns a new one with updated information.
143    pub(crate) fn update<DB: Blockstore + Sync + Send + 'static>(
144        &self,
145        state_manager: &StateManager<DB>,
146        active_forks: Vec<ForkSyncInfo>,
147        stateless_mode: bool,
148    ) -> Self {
149        let heaviest = state_manager.chain_store().heaviest_tipset();
150        let current_head_epoch = heaviest.epoch();
151        let current_head_key = Some(heaviest.key().clone());
152
153        let last_updated = Utc::now();
154        let last_updated_ts = last_updated.timestamp() as u64;
155        let seconds_per_epoch = state_manager.chain_config().block_delay_secs;
156        let network_head_epoch = calculate_expected_epoch(
157            last_updated_ts,
158            state_manager.chain_store().genesis_block_header().timestamp,
159            seconds_per_epoch,
160        );
161
162        let epochs_behind = network_head_epoch.saturating_sub(current_head_epoch);
163        log::trace!(
164            "Sync status report: current head epoch: {}, network head epoch: {}, epochs behind: {}",
165            current_head_epoch,
166            network_head_epoch,
167            epochs_behind
168        );
169
170        let time_diff = last_updated_ts.saturating_sub(heaviest.min_timestamp());
171        let status = match stateless_mode {
172            true => NodeSyncStatus::Offline,
173            false => {
174                if time_diff < seconds_per_epoch as u64 * SYNCED_EPOCH_THRESHOLD {
175                    NodeSyncStatus::Synced
176                } else {
177                    NodeSyncStatus::Syncing
178                }
179            }
180        };
181
182        Self {
183            node_start_time: self.node_start_time,
184            current_head_epoch,
185            current_head_key,
186            network_head_epoch,
187            epochs_behind,
188            status,
189            active_forks,
190            last_updated,
191        }
192    }
193
194    pub(crate) fn is_synced(&self) -> bool {
195        self.status == NodeSyncStatus::Synced
196    }
197
198    pub(crate) fn get_min_starting_block(&self) -> Option<ChainEpoch> {
199        self.active_forks
200            .iter()
201            .map(|fork_info| fork_info.target_sync_epoch_start)
202            .min()
203    }
204}