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